Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/praveen5959/console into to…
Browse files Browse the repository at this point in the history
…olbar-collapse
  • Loading branch information
praveen5959 committed Dec 5, 2024
2 parents 5339cc2 + e6d532f commit f1e9b2c
Show file tree
Hide file tree
Showing 37 changed files with 894 additions and 485 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
VITE_PARSEABLE_URL="https://demo.parseable.com"
VITE_USE_BASIC_AUTH=true
VITE_USERNAME=admin
VITE_PASSWORD=admin
7 changes: 2 additions & 5 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ jobs:
node-version: lts/*
- name: Install dependencies
run: npm install -g pnpm && pnpm install
- name: Build Console
run: pnpm run build
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Start the development server
run: pnpm run dev &
env:
PORT: 3001
VITE_PARSEABLE_URL: 'https://demo.parseable.com'
- name: Run Playwright tests
run: pnpm exec playwright test
- uses: actions/upload-artifact@v4
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"start": "vite preview --host --port 3002",
"tsCheck": "tsc --noEmit",
"pq": "pretty-quick"
"pq": "pretty-quick",
"test": "playwright test",
"test-ui": "playwright test --ui"
},
"dependencies": {
"@apache-arrow/ts": "^14.0.2",
Expand Down
40 changes: 23 additions & 17 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests',
testIgnore: '**/login.spec.ts',
/* Run tests in files in parallel */
fullyParallel: true,
fullyParallel: true, // Set this to false to ensure sequential execution of files
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: 'line',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
Expand All @@ -27,22 +28,27 @@ export default defineConfig({
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// {
// name: 'Firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'Safari',
// use: { ...devices['Desktop Safari'] },
// },
],

/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
webServer: {
command: 'pnpm run dev',
url: 'http://localhost:3001',
reuseExistingServer: false,
env: {
PORT: '3001',
VITE_PARSEABLE_URL: 'https://demo.parseable.com',
VITE_USE_BASIC_AUTH: 'true',
VITE_USERNAME: 'admin',
VITE_PASSWORD: 'admin',
},
},
});
3 changes: 3 additions & 0 deletions src/components/Button/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import classes from './Button.module.css';
type IconButtonProps = {
onClick?: () => void;
renderIcon: () => ReactNode;
data_id?: string;
icon?: ReactNode;
active?: boolean;
tooltipLabel?: string;
Expand All @@ -17,6 +18,7 @@ const IconButton: FC<IconButtonProps> = (props) => {
return (
<Tooltip label={tooltipLabel}>
<ActionIcon
data-testid={props.data_id}
size={props.size ? props.size : 'xl'}
className={`${classes.iconBtn} ${props.active && classes.iconBtnActive}`}
onClick={props.onClick && props.onClick}>
Expand All @@ -27,6 +29,7 @@ const IconButton: FC<IconButtonProps> = (props) => {
} else {
return (
<ActionIcon
data-testid={props.data_id}
size={props.size ? props.size : 'xl'}
className={`${classes.iconBtn} ${props.active && classes.iconBtnActive}`}
onClick={props.onClick && props.onClick}>
Expand Down
130 changes: 130 additions & 0 deletions src/components/Misc/DeleteOrResetModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Box, Button, Modal, Stack, Text, TextInput } from '@mantine/core';
import classes from './styles/DeleteOrResetModal.module.css';
import { ChangeEvent, useCallback, useState } from 'react';

type BaseProps = {
isModalOpen: boolean;
onClose: () => void;
modalHeader: string;
specialContent?: React.ReactNode;
modalContent: string;
actionProcessingContent?: React.ReactNode;
isActionInProgress?: boolean;
onConfirm: () => void;
};

// Note: The `confirmationText` and `placeholder` props are required for 'delete' and 'reset' types, but not for 'simple' type.
type DeleteOrResetModalProps =
| (BaseProps & {
type: 'simple';
confirmationText?: never; // Will throw an error if `confirmationText` is passed
placeholder?: never;
})
| (BaseProps & {
type: 'delete' | 'reset';
confirmationText: string;
placeholder: string;
});

/**
* Confirmation modal for deleting or resetting an item.
* @param type - Specifies the type of modal ('simple', 'delete', or 'reset').
* @param isModalOpen - Controls whether the modal is visible.
* @param onClose - Callback to close the modal and reset the state.
* @param modalHeader - Header text displayed in the modal title.
* @param specialContent - Optional content for additional context or customization.
* @param modalContent - Main descriptive content of the modal.
* @param placeholder - Input placeholder for confirmation text (applicable to 'delete' and 'reset').
* @param confirmationText - Text required to confirm the action (applicable to 'delete' and 'reset').
* @param actionProcessingContent - Optional content below text input for showing progress status or related information.
* @param isActionInProgress - Disables the confirm button when action is in progress.
* @param onConfirm - Callback function to be executed when the confirm button is clicked.
*/
const DeleteOrResetModal = ({
type,
isModalOpen,
onClose,
modalHeader,
specialContent,
modalContent,
placeholder,
confirmationText,
actionProcessingContent,
isActionInProgress,
onConfirm,
}: DeleteOrResetModalProps) => {
const [confirmText, setConfirmText] = useState<string>('');

// Handler for the confirmation input field
const onChangeHandler = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setConfirmText(e.target.value);
}, []);

// Function to validate and trigger confirmation logic
const tryConfirm = useCallback(() => {
if (type === 'simple' || confirmationText === confirmText) {
setConfirmText('');
onConfirm();
}
}, [type, confirmationText, confirmText, onConfirm]);

// Function to close the modal and reset the confirmation text state.
const closeModal = useCallback(() => {
setConfirmText('');
onClose();
}, [onClose]);

return (
<Modal
withinPortal
opened={isModalOpen}
onClose={closeModal}
size="auto"
centered
classNames={{
body: classes.modalBody,
header: classes.modalHeader,
}}
title={<Text className={classes.headerText}>{modalHeader}</Text>}>
<Stack>
<Stack gap={8}>
{specialContent}
<Text className={classes.warningText}>{modalContent}</Text>

{/* Render confirmation field for 'delete' or 'reset' types */}
{type !== 'simple' && (
<>
<Text className={classes.confirmationText}>
Please type <span className={classes.confirmationTextHighlight}>{`"${confirmationText}"`}</span> to
confirm {type === 'delete' ? 'deletion' : 'reset'}.
</Text>
<TextInput value={confirmText} onChange={onChangeHandler} placeholder={placeholder} required />
</>
)}

{/* Renders the action processing content if provided */}
{actionProcessingContent}
</Stack>

{/* Action buttons */}
<Stack className={classes.actionButtonsContainer}>
<Box>
<Button variant="outline" onClick={closeModal}>
Cancel
</Button>
</Box>
<Box>
{/* Disable the button if the confirmation text is not correct or the action is processing. */}
<Button
disabled={(type !== 'simple' && confirmationText !== confirmText) || isActionInProgress}
onClick={tryConfirm}>
{type === 'reset' ? 'Reset' : 'Delete'}
</Button>
</Box>
</Stack>
</Stack>
</Modal>
);
};

export default DeleteOrResetModal;
37 changes: 37 additions & 0 deletions src/components/Misc/styles/DeleteOrResetModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.modalBody {
padding: 0 1rem 1rem 1rem;
width: 400px;
}

.modalHeader {
padding: 1rem;
padding-bottom: 0.4rem;
}

.headerText {
font-size: 0.9rem;
font-weight: 600;
}

.warningText {
margin-top: 0.4rem;
font-size: 0.7rem;
font-weight: 500;
color: var(--mantine-color-gray-8);
}

.confirmationText {
font-size: 0.7rem;
color: var(--mantine-color-gray-7);
}

.confirmationTextHighlight {
font-weight: 500;
}

.actionButtonsContainer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
66 changes: 35 additions & 31 deletions src/hooks/useGetStreamMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,42 +15,46 @@ export type MetaData = {

// until dedicated endpoint been provided - fetch one by one
export const useGetStreamMetadata = () => {
const [isLoading, setLoading] = useState<Boolean>(true);
const [error, setError] = useState<Boolean>(false);
const [isLoading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<boolean>(false);
const [metaData, setMetadata] = useState<MetaData | null>(null);
const [userRoles] = useAppStore((store) => store.userRoles);

const getStreamMetadata = useCallback(async (streams: string[]) => {
setLoading(true);
try {
// stats
const allStatsReqs = streams.map((stream) => getLogStreamStats(stream));
const allStatsRes = await Promise.all(allStatsReqs);
const getStreamMetadata = useCallback(
async (streams: string[]) => {
if (!userRoles) return;
setLoading(true);
try {
// stats
const allStatsReqs = streams.map((stream) => getLogStreamStats(stream));
const allStatsRes = await Promise.all(allStatsReqs);

// retention
const streamsWithSettingsAccess = _.filter(streams, (stream) =>
_.includes(getStreamsSepcificAccess(userRoles, stream), 'StreamSettings'),
);
const allretentionReqs = streamsWithSettingsAccess.map((stream) => getLogStreamRetention(stream));
const allretentionRes = await Promise.all(allretentionReqs);
// retention
const streamsWithSettingsAccess = _.filter(streams, (stream) =>
_.includes(getStreamsSepcificAccess(userRoles, stream), 'StreamSettings'),
);
const allretentionReqs = streamsWithSettingsAccess.map((stream) => getLogStreamRetention(stream));
const allretentionRes = await Promise.all(allretentionReqs);

const metadata = streams.reduce((acc, stream, index) => {
return {
...acc,
[stream]: { stats: allStatsRes[index]?.data || {}, retention: allretentionRes[index]?.data || [] },
};
}, {});
setMetadata(metadata);
} catch {
setError(true);
setMetadata(null);
notifyError({
message: 'Unable to fetch stream data',
});
} finally {
setLoading(false);
}
}, []);
const metadata = streams.reduce((acc, stream, index) => {
return {
...acc,
[stream]: { stats: allStatsRes[index]?.data || {}, retention: allretentionRes[index]?.data || [] },
};
}, {});
setMetadata(metadata);
} catch {
setError(true);
setMetadata(null);
notifyError({
message: 'Unable to fetch stream data',
});
} finally {
setLoading(false);
}
},
[userRoles],
);

return {
isLoading,
Expand Down
12 changes: 10 additions & 2 deletions src/hooks/useQueryLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@ const { setLogData } = logsStoreReducers;
const { parseQuery } = filterStoreReducers;

const appendOffsetToQuery = (query: string, offset: number) => {
const hasOffset = query.toLowerCase().includes('offset');
return !hasOffset ? query.replace(/offset\s+\d+/i, `OFFSET ${offset}`) : `${query}`;
const offsetRegex = /offset\s+\d+/i;
const limitRegex = /limit\s+\d+/i;

if (offsetRegex.test(query)) {
// Replace the existing OFFSET with the new one
return query.replace(offsetRegex, `OFFSET ${offset}`);
} else {
// Insert OFFSET before LIMIT if OFFSET is not present
return query.replace(limitRegex, `OFFSET ${offset} $&`);
}
};

export const useQueryLogs = () => {
Expand Down
Loading

0 comments on commit f1e9b2c

Please sign in to comment.