mirror of
https://github.com/RocketChat/Rocket.Chat.git
synced 2025-12-28 06:47:25 +00:00
feat: [FE] New App Logs Filters (#36288)
Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com>
This commit is contained in:
parent
a00729f114
commit
484d36dc26
5
.changeset/curvy-dancers-try.md
Normal file
5
.changeset/curvy-dancers-try.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@rocket.chat/meteor": minor
|
||||
---
|
||||
|
||||
Adds a new filter to the Logs tab of the App Details page.
|
||||
@ -14,10 +14,13 @@ import { handleAPIError } from '../helpers/handleAPIError';
|
||||
import { useAppInfo } from '../hooks/useAppInfo';
|
||||
import AppDetails from './tabs/AppDetails';
|
||||
import AppLogs from './tabs/AppLogs';
|
||||
import { AppLogsFilterContextualBar } from './tabs/AppLogs/Filters/AppLogsFilterContextualBar';
|
||||
import { useAppLogsFilterForm } from './tabs/AppLogs/useAppLogsFilterForm';
|
||||
import AppReleases from './tabs/AppReleases';
|
||||
import AppRequests from './tabs/AppRequests/AppRequests';
|
||||
import AppSecurity from './tabs/AppSecurity/AppSecurity';
|
||||
import AppSettings from './tabs/AppSettings';
|
||||
import { useCompactMode } from './useCompactMode';
|
||||
import { AppClientOrchestratorInstance } from '../../../apps/orchestrator';
|
||||
import { Page, PageFooter, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page';
|
||||
|
||||
@ -35,7 +38,9 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
|
||||
|
||||
const tab = useRouteParameter('tab');
|
||||
const context = useRouteParameter('context');
|
||||
const contextualBar = useRouteParameter('contextualBar');
|
||||
const appData = useAppInfo(id, context || '');
|
||||
const compactMode = useCompactMode();
|
||||
|
||||
const handleReturn = useEffectEvent((): void => {
|
||||
if (!context) {
|
||||
@ -48,6 +53,20 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
|
||||
});
|
||||
});
|
||||
|
||||
const handleReturnToLogs = useEffectEvent((): void => {
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.navigate(
|
||||
{
|
||||
name: 'marketplace',
|
||||
params: { ...router.getRouteParameters(), contextualBar: '' },
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
});
|
||||
|
||||
const { installed, settings, privacyPolicySummary, permissions, tosLink, privacyLink, name } = appData || {};
|
||||
const isSecurityVisible = Boolean(privacyPolicySummary || permissions || tosLink || privacyLink);
|
||||
|
||||
@ -58,12 +77,14 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
|
||||
);
|
||||
}, [settings]);
|
||||
|
||||
const methods = useForm<AppDetailsPageFormData>({ values: reducedSettings });
|
||||
const settingsFormMethods = useForm<AppDetailsPageFormData>({ values: reducedSettings });
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isDirty, isSubmitting },
|
||||
} = methods;
|
||||
} = settingsFormMethods;
|
||||
|
||||
const logsFilterFormMethods = useAppLogsFilterForm();
|
||||
|
||||
const saveAppSettings = useCallback(
|
||||
async (data: AppDetailsPageFormData) => {
|
||||
@ -81,56 +102,67 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
|
||||
handleAPIError(e);
|
||||
}
|
||||
},
|
||||
[dispatchToastMessage, id, name, settings, reset],
|
||||
[id, settings, reset, dispatchToastMessage, t, name],
|
||||
);
|
||||
|
||||
return (
|
||||
<Page flexDirection='column' h='full'>
|
||||
<PageHeader title={t('App_Info')} onClickBack={handleReturn} />
|
||||
<PageScrollableContentWithShadow pi={24} pbs={24} pbe={0} h='full'>
|
||||
<Box w='full' alignSelf='center' h='full' display='flex' flexDirection='column'>
|
||||
{!appData && <AppDetailsPageLoading />}
|
||||
{appData && (
|
||||
<>
|
||||
<AppDetailsPageHeader app={appData} />
|
||||
<AppDetailsPageTabs
|
||||
context={context || ''}
|
||||
installed={installed}
|
||||
isSecurityVisible={isSecurityVisible}
|
||||
settings={settings}
|
||||
tab={tab}
|
||||
/>
|
||||
{Boolean(!tab || tab === 'details') && <AppDetails app={appData} />}
|
||||
{tab === 'requests' && <AppRequests id={id} isAdminUser={isAdminUser} />}
|
||||
{tab === 'security' && isSecurityVisible && (
|
||||
<AppSecurity
|
||||
privacyPolicySummary={privacyPolicySummary}
|
||||
appPermissions={permissions}
|
||||
tosLink={tosLink}
|
||||
privacyLink={privacyLink}
|
||||
<Page flexDirection='row'>
|
||||
<Page flexDirection='column' h='full'>
|
||||
<PageHeader title={t('App_Info')} onClickBack={handleReturn} />
|
||||
<PageScrollableContentWithShadow pi={24} pbs={24} pbe={0} h='full'>
|
||||
<Box w='full' alignSelf='center' h='full' display='flex' flexDirection='column'>
|
||||
{!appData && <AppDetailsPageLoading />}
|
||||
{appData && (
|
||||
<>
|
||||
<AppDetailsPageHeader app={appData} />
|
||||
<AppDetailsPageTabs
|
||||
context={context || ''}
|
||||
installed={installed}
|
||||
isSecurityVisible={isSecurityVisible}
|
||||
settings={settings}
|
||||
tab={tab}
|
||||
/>
|
||||
)}
|
||||
{tab === 'releases' && <AppReleases id={id} />}
|
||||
{Boolean(tab === 'settings' && settings && Object.values(settings).length) && (
|
||||
<FormProvider {...methods}>
|
||||
<AppSettings settings={settings || {}} />
|
||||
</FormProvider>
|
||||
)}
|
||||
{tab === 'logs' && <AppLogs id={id} />}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</PageScrollableContentWithShadow>
|
||||
<PageFooter isDirty={isDirty}>
|
||||
<ButtonGroup>
|
||||
<Button onClick={() => reset()}>{t('Cancel')}</Button>
|
||||
{installed && isAdminUser && (
|
||||
<Button primary loading={isSubmitting} onClick={handleSubmit(saveAppSettings)}>
|
||||
{t('Save_changes')}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</PageFooter>
|
||||
{Boolean(!tab || tab === 'details') && <AppDetails app={appData} />}
|
||||
{tab === 'requests' && <AppRequests id={id} isAdminUser={isAdminUser} />}
|
||||
{tab === 'security' && isSecurityVisible && (
|
||||
<AppSecurity
|
||||
privacyPolicySummary={privacyPolicySummary}
|
||||
appPermissions={permissions}
|
||||
tosLink={tosLink}
|
||||
privacyLink={privacyLink}
|
||||
/>
|
||||
)}
|
||||
{tab === 'releases' && <AppReleases id={id} />}
|
||||
{Boolean(tab === 'settings' && settings && Object.values(settings).length) && (
|
||||
<FormProvider {...settingsFormMethods}>
|
||||
<AppSettings settings={settings || {}} />
|
||||
</FormProvider>
|
||||
)}
|
||||
{(tab === 'logs' || tab === 'logs-filter') && (
|
||||
<FormProvider {...logsFilterFormMethods}>
|
||||
<AppLogs id={id} />
|
||||
</FormProvider>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</PageScrollableContentWithShadow>
|
||||
<PageFooter isDirty={isDirty}>
|
||||
<ButtonGroup>
|
||||
<Button onClick={() => reset()}>{t('Cancel')}</Button>
|
||||
{installed && isAdminUser && (
|
||||
<Button primary loading={isSubmitting} onClick={handleSubmit(saveAppSettings)}>
|
||||
{t('Save_changes')}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</PageFooter>
|
||||
</Page>
|
||||
{compactMode && contextualBar === 'filter-logs' && (
|
||||
<FormProvider {...logsFilterFormMethods}>
|
||||
<AppLogsFilterContextualBar onClose={handleReturnToLogs} />
|
||||
</FormProvider>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,30 +1,64 @@
|
||||
import { Box, Pagination } from '@rocket.chat/fuselage';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
|
||||
import { useMemo, type ReactElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import AppLogsItem from './AppLogsItem';
|
||||
import { CollapsiblePanel } from './Components/CollapsiblePanel';
|
||||
import { AppLogsFilter } from './Filters/AppLogsFilter';
|
||||
import { useAppLogsFilterFormContext } from './useAppLogsFilterForm';
|
||||
import { CustomScrollbars } from '../../../../../components/CustomScrollbars';
|
||||
import GenericError from '../../../../../components/GenericError';
|
||||
import GenericNoResults from '../../../../../components/GenericNoResults';
|
||||
import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination';
|
||||
import AccordionLoading from '../../../components/AccordionLoading';
|
||||
import { useLogs } from '../../../hooks/useLogs';
|
||||
|
||||
const AppLogs = ({ id }: { id: string }): ReactElement => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { watch } = useAppLogsFilterFormContext();
|
||||
|
||||
const { startTime, endTime, startDate, endDate, event, severity, instance } = watch();
|
||||
|
||||
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
|
||||
const { data, isSuccess, isError, isLoading } = useLogs({ appId: id, current, itemsPerPage });
|
||||
|
||||
const debouncedEvent = useDebouncedValue(event, 500);
|
||||
|
||||
const { data, isSuccess, isError, isFetching, error } = useLogs({
|
||||
appId: id,
|
||||
current,
|
||||
itemsPerPage,
|
||||
...(instance !== 'all' && { instanceId: instance }),
|
||||
...(severity !== 'all' && { logLevel: severity }),
|
||||
method: debouncedEvent,
|
||||
...(startTime && startDate && { startDate: new Date(`${startDate}T${startTime}`).toISOString() }),
|
||||
...(endTime && endDate && { endDate: new Date(`${endDate}T${endTime}`).toISOString() }),
|
||||
});
|
||||
|
||||
const parsedError = useMemo(() => {
|
||||
if (error) {
|
||||
// TODO: Check why tanstack expects a default Error but we return {error: string}
|
||||
if ((error as unknown as { error: string }).error === 'Invalid date range') {
|
||||
return t('error-invalid-dates');
|
||||
}
|
||||
|
||||
return t('Something_Went_Wrong');
|
||||
}
|
||||
}, [error, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <AccordionLoading />}
|
||||
{isError && (
|
||||
<Box maxWidth='x600' alignSelf='center'>
|
||||
{t('App_not_found')}
|
||||
</Box>
|
||||
)}
|
||||
{isSuccess && (
|
||||
<Box pb={16}>
|
||||
<AppLogsFilter />
|
||||
</Box>
|
||||
{isFetching && <AccordionLoading />}
|
||||
{isError && <GenericError title={parsedError} />}
|
||||
{isSuccess && data?.logs?.length === 0 ? (
|
||||
<GenericNoResults />
|
||||
) : (
|
||||
<CustomScrollbars>
|
||||
<CollapsiblePanel width='100%' alignSelf='center'>
|
||||
<CollapsiblePanel aria-busy={isFetching || event !== debouncedEvent} width='100%' alignSelf='center'>
|
||||
{data?.logs?.map((log, index) => <AppLogsItem regionId={log._id} key={`${index}-${log._createdAt}`} {...log} />)}
|
||||
</CollapsiblePanel>
|
||||
</CustomScrollbars>
|
||||
|
||||
@ -5,7 +5,7 @@ import AppLogsItem from './AppLogsItem';
|
||||
import { CollapsiblePanel } from './Components/CollapsiblePanel';
|
||||
|
||||
export default {
|
||||
title: 'Components/AppLogsItem',
|
||||
title: 'Marketplace/AppDetailsPage/AppLogs/AppLogsItem',
|
||||
component: AppLogsItem,
|
||||
decorators: [(fn) => <CollapsiblePanel style={{ padding: 24 }}>{fn()}</CollapsiblePanel>],
|
||||
args: {
|
||||
|
||||
@ -5,7 +5,7 @@ import { CollapsiblePanel } from './CollapsiblePanel';
|
||||
import { CollapsibleRegion } from './CollapsibleRegion';
|
||||
|
||||
export default {
|
||||
title: 'Components/CollapsiblePanel',
|
||||
title: 'Marketplace/AppDetailsPage/AppLogs/Components/CollapsiblePanel',
|
||||
component: CollapsiblePanel,
|
||||
|
||||
args: {
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
|
||||
import { AppLogsFilter } from './AppLogsFilter';
|
||||
import { useAppLogsFilterForm } from '../useAppLogsFilterForm';
|
||||
|
||||
export default {
|
||||
title: 'Marketplace/AppDetailsPage/AppLogs/Filters/AppLogsFilter',
|
||||
component: AppLogsFilter,
|
||||
args: {},
|
||||
decorators: [
|
||||
mockAppRoot()
|
||||
// @ts-expect-error The endpoint is to be merged in https://github.com/RocketChat/Rocket.Chat/pull/36245
|
||||
.withEndpoint('GET', '/apps/logs/instanceIds', () => ({
|
||||
success: true,
|
||||
instanceIds: ['instance-1', 'instance-2', 'instance-3'],
|
||||
}))
|
||||
.buildStoryDecorator(),
|
||||
(fn) => {
|
||||
const methods = useAppLogsFilterForm();
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<Box p={16}>{fn()}</Box>
|
||||
</FormProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} satisfies Meta<typeof AppLogsFilter>;
|
||||
|
||||
export const Default = () => <AppLogsFilter />;
|
||||
@ -0,0 +1,80 @@
|
||||
import { Box, Button, Icon, Label, Palette, TextInput } from '@rocket.chat/fuselage';
|
||||
import { useRouter } from '@rocket.chat/ui-contexts';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { InstanceFilterSelect } from './InstanceFilterSelect';
|
||||
import { SeverityFilterSelect } from './SeverityFilterSelect';
|
||||
import { TimeFilterSelect } from './TimeFilterSelect';
|
||||
import { useCompactMode } from '../../../useCompactMode';
|
||||
import { useAppLogsFilterFormContext } from '../useAppLogsFilterForm';
|
||||
|
||||
export const AppLogsFilter = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { control } = useAppLogsFilterFormContext();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const openContextualBar = () => {
|
||||
router.navigate(
|
||||
{
|
||||
name: 'marketplace',
|
||||
params: { ...router.getRouteParameters(), contextualBar: 'filter-logs' },
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
};
|
||||
|
||||
const compactMode = useCompactMode();
|
||||
|
||||
return (
|
||||
<Box display='flex' flexDirection='row' width='full' flexWrap='wrap' alignContent='flex-end'>
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label htmlFor='eventFilter'>{t('Event')}</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name='event'
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
addon={<Icon color={Palette.text['font-secondary-info']} name='magnifier' size={20} />}
|
||||
id='eventFilter'
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
{!compactMode && (
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label id='timeFilterLabel' htmlFor='timeFilter'>
|
||||
{t('Time')}
|
||||
</Label>
|
||||
<TimeFilterSelect id='timeFilter' aria-labelledby='timeFilterLabel' />
|
||||
</Box>
|
||||
)}
|
||||
{!compactMode && (
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label id='instanceFilterLabel' htmlFor='instanceFilter'>
|
||||
{t('Instance')}
|
||||
</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name='instance'
|
||||
render={({ field }) => <InstanceFilterSelect aria-labelledby='instanceFilterLabel' id='instanceFilter' {...field} />}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{!compactMode && (
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label>{t('Severity')}</Label>
|
||||
<Controller control={control} name='severity' render={({ field }) => <SeverityFilterSelect id='severityFilter' {...field} />} />
|
||||
</Box>
|
||||
)}
|
||||
{compactMode && (
|
||||
<Button alignSelf='flex-end' icon='customize' secondary mie={10} onClick={() => openContextualBar()}>
|
||||
{t('Filters')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { composeStories } from '@storybook/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { axe } from 'jest-axe';
|
||||
|
||||
import * as stories from './AppLogsFilter.stories';
|
||||
|
||||
const { Default } = composeStories(stories);
|
||||
|
||||
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
|
||||
|
||||
test.each(testCases)(`renders AppLogsItem without crashing`, async (_storyname, Story) => {
|
||||
const view = render(<Story />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
expect(view.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test.each(testCases)('AppLogsItem should have no a11y violations', async (_storyname, Story) => {
|
||||
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });
|
||||
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('Should show filter button in contextual bar', async () => {
|
||||
render(<Default />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Filters' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('Should not show instance, time, and severity filters', async () => {
|
||||
render(<Default />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Time' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Severity' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Instance' })).not.toBeInTheDocument();
|
||||
});
|
||||
@ -0,0 +1,114 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { composeStories } from '@storybook/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { axe } from 'jest-axe';
|
||||
|
||||
import * as stories from './AppLogsFilterContextualBar.stories';
|
||||
|
||||
const { Default } = composeStories(stories);
|
||||
|
||||
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
|
||||
|
||||
// UserEvents will throw a timeout without the advanceTimers option
|
||||
jest.useFakeTimers({ advanceTimers: true });
|
||||
jest.setSystemTime(new Date('2017-05-19T12:20:00'));
|
||||
|
||||
test.each(testCases)(`renders AppLogsItem without crashing`, async (_storyname, Story) => {
|
||||
const view = render(<Story />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
expect(view.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test.each(testCases)('AppLogsItem should have no a11y violations', async (_storyname, Story) => {
|
||||
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });
|
||||
|
||||
/**
|
||||
** Disable 'nested-interactive' rule because our `Select` component is still not a11y compliant
|
||||
**/
|
||||
const results = await axe(container, { rules: { 'nested-interactive': { enabled: false } } });
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('Instance select should have correct options', async () => {
|
||||
render(<Default />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
|
||||
const select = screen.getByLabelText('Instance');
|
||||
|
||||
await userEvent.click(select);
|
||||
|
||||
expect(screen.getByRole('option', { name: 'All' })).toBeVisible();
|
||||
expect(screen.getByRole('option', { name: 'instance-1' })).toBeVisible();
|
||||
expect(screen.getByRole('option', { name: 'instance-2' })).toBeVisible();
|
||||
expect(screen.getByRole('option', { name: 'instance-3' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('Time select should not have custom time range', async () => {
|
||||
render(<Default />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
|
||||
const select = screen.getByLabelText('Time');
|
||||
|
||||
await userEvent.click(select);
|
||||
|
||||
expect(screen.queryByRole('option', { name: 'Custom_time_range' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const defaultTimeRangesAndValues = [
|
||||
{ name: 'Last_5_minutes', value: ['2017-05-19', '2017-05-19', '12:15', '12:20'] },
|
||||
{ name: 'Last_15_minutes', value: ['2017-05-19', '2017-05-19', '12:05', '12:20'] },
|
||||
{ name: 'Last_30_minutes', value: ['2017-05-19', '2017-05-19', '11:50', '12:20'] },
|
||||
{ name: 'Last_1_hour', value: ['2017-05-19', '2017-05-19', '11:20', '12:20'] },
|
||||
{ name: 'This_week', value: ['2017-05-14', '2017-05-20', '00:00', '23:59'] },
|
||||
];
|
||||
|
||||
describe('Time range', () => {
|
||||
defaultTimeRangesAndValues.forEach(({ name, value }) => {
|
||||
it(`Should update time range when ${name} is selected`, async () => {
|
||||
render(<Default />, { wrapper: mockAppRoot().build() });
|
||||
|
||||
const startDate = screen.getByLabelText('Start Date');
|
||||
const endDate = screen.getByLabelText('End Date');
|
||||
const startTime = screen.getByLabelText('Start Time');
|
||||
const endTime = screen.getByLabelText('End Time');
|
||||
const timeSelect = screen.getByLabelText('Time');
|
||||
|
||||
await userEvent.click(timeSelect);
|
||||
|
||||
expect(screen.getByRole('option', { name })).toBeVisible();
|
||||
await userEvent.click(screen.getByRole('option', { name }));
|
||||
|
||||
expect(startDate).toHaveValue(value[0]);
|
||||
expect(endDate).toHaveValue(value[1]);
|
||||
expect(startTime).toHaveValue(value[2]);
|
||||
expect(endTime).toHaveValue(value[3]);
|
||||
});
|
||||
});
|
||||
|
||||
defaultTimeRangesAndValues.forEach(({ name, value }) => {
|
||||
it(`Should manually set ${name}`, async () => {
|
||||
render(<Default />, { wrapper: mockAppRoot().build() });
|
||||
|
||||
const startDate = screen.getByLabelText('Start Date');
|
||||
const endDate = screen.getByLabelText('End Date');
|
||||
const startTime = screen.getByLabelText('Start Time');
|
||||
const endTime = screen.getByLabelText('End Time');
|
||||
|
||||
await userEvent.type(startDate, value[0]);
|
||||
await userEvent.type(endDate, value[1]);
|
||||
await userEvent.type(startTime, value[2]);
|
||||
await userEvent.type(endTime, value[3]);
|
||||
|
||||
expect(startDate).toHaveValue(value[0]);
|
||||
expect(endDate).toHaveValue(value[1]);
|
||||
expect(startTime).toHaveValue(value[2]);
|
||||
expect(endTime).toHaveValue(value[3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,36 @@
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { AppLogsFilterContextualBar } from './AppLogsFilterContextualBar';
|
||||
import { Contextualbar } from '../../../../../../components/Contextualbar';
|
||||
|
||||
export default {
|
||||
title: 'Marketplace/AppDetailsPage/AppLogs/Filters/AppLogsFilterContextualBar',
|
||||
component: AppLogsFilterContextualBar,
|
||||
args: {},
|
||||
decorators: [
|
||||
mockAppRoot()
|
||||
// @ts-expect-error The endpoint is to be merged in https://github.com/RocketChat/Rocket.Chat/pull/36245
|
||||
.withEndpoint('GET', '/apps/logs/instanceIds', () => ({
|
||||
success: true,
|
||||
instanceIds: ['instance-1', 'instance-2', 'instance-3'],
|
||||
}))
|
||||
.buildStoryDecorator(),
|
||||
(fn) => {
|
||||
const methods = useForm({});
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<Contextualbar height='100vh'>{fn()}</Contextualbar>
|
||||
</FormProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} satisfies Meta<typeof AppLogsFilterContextualBar>;
|
||||
|
||||
export const Default = () => <AppLogsFilterContextualBar onClose={action('onClose')} />;
|
||||
@ -0,0 +1,73 @@
|
||||
import { Box, Label } from '@rocket.chat/fuselage';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DateTimeFilter from './DateTimeFilter';
|
||||
import { InstanceFilterSelect } from './InstanceFilterSelect';
|
||||
import { SeverityFilterSelect } from './SeverityFilterSelect';
|
||||
import { TimeFilterSelect } from './TimeFilterSelect';
|
||||
import {
|
||||
ContextualbarHeader,
|
||||
ContextualbarIcon,
|
||||
ContextualbarTitle,
|
||||
ContextualbarClose,
|
||||
ContextualbarScrollableContent,
|
||||
ContextualbarDialog,
|
||||
} from '../../../../../../components/Contextualbar';
|
||||
import { useAppLogsFilterFormContext } from '../useAppLogsFilterForm';
|
||||
|
||||
type AppLogsFilterContextualBarProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const AppLogsFilterContextualBar = ({ onClose = () => undefined }: AppLogsFilterContextualBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { control } = useAppLogsFilterFormContext();
|
||||
|
||||
return (
|
||||
<ContextualbarDialog onClose={onClose}>
|
||||
<ContextualbarHeader>
|
||||
<ContextualbarIcon name='customize' />
|
||||
<ContextualbarTitle>{t('Filters')}</ContextualbarTitle>
|
||||
<ContextualbarClose onClick={onClose} />
|
||||
</ContextualbarHeader>
|
||||
<ContextualbarScrollableContent is='form'>
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label id='timeFilterLabel' htmlFor='timeFilter'>
|
||||
{t('Time')}
|
||||
</Label>
|
||||
<TimeFilterSelect aria-labelledby='timeFilterLabel' id='timeFilter' compactView={true} />
|
||||
</Box>
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label htmlFor='timeFilter'>{t('Logs_from')}</Label>
|
||||
<DateTimeFilter control={control} type='start' />
|
||||
</Box>
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label htmlFor='timeFilter'>{t('Until')}</Label>
|
||||
<DateTimeFilter control={control} type='end' />
|
||||
</Box>
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label id='instanceFilterLabel' htmlFor='instanceFilter'>
|
||||
{t('Instance')}
|
||||
</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name='instance'
|
||||
render={({ field }) => <InstanceFilterSelect aria-labelledby='instanceFilterLabel' id='instanceFilter' {...field} />}
|
||||
/>
|
||||
</Box>
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label id='severityFilterLabel' htmlFor='severityFilter'>
|
||||
{t('Severity')}
|
||||
</Label>
|
||||
<Controller
|
||||
control={control}
|
||||
name='severity'
|
||||
render={({ field }) => <SeverityFilterSelect aria-labelledby='severityFilterLabel' id='severityFilter' {...field} />}
|
||||
/>
|
||||
</Box>
|
||||
</ContextualbarScrollableContent>
|
||||
</ContextualbarDialog>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { composeStories } from '@storybook/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { axe } from 'jest-axe';
|
||||
|
||||
import * as stories from './AppLogsFilter.stories';
|
||||
|
||||
const { Default } = composeStories(stories);
|
||||
|
||||
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
|
||||
|
||||
jest.mock('@rocket.chat/fuselage-hooks', () => {
|
||||
const originalModule = jest.requireActual('@rocket.chat/fuselage-hooks');
|
||||
return {
|
||||
...originalModule,
|
||||
useBreakpoints: () => ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'],
|
||||
};
|
||||
});
|
||||
|
||||
test.each(testCases)(`renders AppLogsItem without crashing`, async (_storyname, Story) => {
|
||||
const view = render(<Story />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
expect(view.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test.each(testCases)('AppLogsItem should have no a11y violations', async (_storyname, Story) => {
|
||||
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });
|
||||
|
||||
/**
|
||||
** Disable 'nested-interactive' rule because our `Select` component is still not a11y compliant
|
||||
**/
|
||||
const results = await axe(container, { rules: { 'nested-interactive': { enabled: false } } });
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('Instance select should have correct options', async () => {
|
||||
render(<Default />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
|
||||
const select = screen.getByLabelText('Instance');
|
||||
|
||||
await userEvent.click(select);
|
||||
|
||||
expect(screen.getByRole('option', { name: 'All' })).toBeVisible();
|
||||
expect(screen.getByRole('option', { name: 'instance-1' })).toBeVisible();
|
||||
expect(screen.getByRole('option', { name: 'instance-2' })).toBeVisible();
|
||||
expect(screen.getByRole('option', { name: 'instance-3' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('Time select should open modal when custom time range is selected', async () => {
|
||||
render(<Default />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
|
||||
const select = screen.getByLabelText('Time');
|
||||
|
||||
await userEvent.click(select);
|
||||
|
||||
expect(screen.getByRole('option', { name: 'Custom_time_range' })).toBeVisible();
|
||||
await userEvent.click(screen.getByRole('option', { name: 'Custom_time_range' }));
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeVisible();
|
||||
});
|
||||
@ -0,0 +1,44 @@
|
||||
import { Box, InputBox, Margins } from '@rocket.chat/fuselage';
|
||||
import type { Control } from 'react-hook-form';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type DateTimeFilterProps = {
|
||||
type: 'start' | 'end';
|
||||
id?: string;
|
||||
control: Control<{ startDate?: string; startTime?: string; endDate?: string; endTime?: string }>;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
const DateTimeFilter = ({ type, control, id, error }: DateTimeFilterProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box display='flex' flexDirection='row' id={id}>
|
||||
<Margins inlineEnd={4}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={type === 'start' ? 'startDate' : 'endDate'}
|
||||
render={({ field }) => (
|
||||
<InputBox
|
||||
aria-label={type === 'start' ? 'Start Date' : 'End Date'}
|
||||
type='date'
|
||||
{...field}
|
||||
error={error ? t('Required_field') : undefined}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Margins>
|
||||
|
||||
<Margins inlineStart={4}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={type === 'start' ? 'startTime' : 'endTime'}
|
||||
render={({ field }) => <InputBox aria-label={type === 'start' ? 'Start Time' : 'End Time'} type='time' {...field} />}
|
||||
/>
|
||||
</Margins>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTimeFilter;
|
||||
@ -0,0 +1,50 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { composeStories } from '@storybook/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { axe } from 'jest-axe';
|
||||
|
||||
import * as stories from './DateTimeModal.stories';
|
||||
|
||||
const { Default } = composeStories(stories);
|
||||
|
||||
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
|
||||
|
||||
test.each(testCases)(`renders AppLogsItem without crashing`, async (_storyname, Story) => {
|
||||
const view = render(<Story />, {
|
||||
wrapper: mockAppRoot().build(),
|
||||
});
|
||||
expect(view.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test.each(testCases)('AppLogsItem should have no a11y violations', async (_storyname, Story) => {
|
||||
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });
|
||||
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should not enable apply button when start Date and end Date are not selected', async () => {
|
||||
render(<Default />, { wrapper: mockAppRoot().build() });
|
||||
const startDate = screen.getByLabelText('Start Date');
|
||||
const endDate = screen.getByLabelText('End Date');
|
||||
const startTime = screen.getByLabelText('Start Time');
|
||||
const endTime = screen.getByLabelText('End Time');
|
||||
|
||||
expect(startDate).toBeInTheDocument();
|
||||
expect(endDate).toBeInTheDocument();
|
||||
expect(startTime).toBeInTheDocument();
|
||||
expect(endTime).toBeInTheDocument();
|
||||
|
||||
await userEvent.type(startTime, '00:00');
|
||||
await userEvent.type(endTime, '00:00');
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Apply' });
|
||||
expect(button).toBeDisabled();
|
||||
|
||||
await userEvent.type(startDate, '2022-01-01');
|
||||
await userEvent.type(endDate, '2022-01-02');
|
||||
expect(button).toBeEnabled();
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { DateTimeModal } from './DateTimeModal';
|
||||
|
||||
export default {
|
||||
title: 'Marketplace/AppDetailsPage/AppLogs/Filters/DateTimeModal',
|
||||
component: DateTimeModal,
|
||||
decorators: [
|
||||
mockAppRoot().buildStoryDecorator(),
|
||||
(fn) => {
|
||||
const methods = useForm({});
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<Box p={16}>{fn()}</Box>
|
||||
</FormProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} satisfies Meta<typeof DateTimeModal>;
|
||||
|
||||
export const Default: StoryFn<ComponentProps<typeof DateTimeModal>> = () => (
|
||||
<DateTimeModal onClose={action('onClose')} onSave={action('onSave')} />
|
||||
);
|
||||
@ -0,0 +1,62 @@
|
||||
import { Box, Button, Label, Modal } from '@rocket.chat/fuselage';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DateTimeFilter from './DateTimeFilter';
|
||||
|
||||
export type DateTimeModalFormData = {
|
||||
startDate?: string;
|
||||
startTime?: string;
|
||||
endDate?: string;
|
||||
endTime?: string;
|
||||
};
|
||||
|
||||
type DateTimeModalProps = {
|
||||
onClose: () => void;
|
||||
onSave: (value: DateTimeModalFormData) => void;
|
||||
confirmDisabled?: boolean;
|
||||
defaultValues?: DateTimeModalFormData;
|
||||
};
|
||||
|
||||
export const DateTimeModal = ({ onSave, onClose, defaultValues }: DateTimeModalProps): ReactNode => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { control, getValues, watch } = useForm<DateTimeModalFormData>({ defaultValues });
|
||||
|
||||
const handleSave = (): void => {
|
||||
onSave({
|
||||
startDate: getValues('startDate'),
|
||||
startTime: getValues('startTime'),
|
||||
endDate: getValues('endDate'),
|
||||
endTime: getValues('endTime'),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={t('Custom_time_range')}>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('Custom_time_range')}</Modal.Title>
|
||||
<Modal.Close onClick={onClose} />
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Box display='flex' flexDirection='column' mie={10} mbe={24} flexGrow={1}>
|
||||
<Label htmlFor='timeFilter'>{t('Logs_from')}</Label>
|
||||
<DateTimeFilter control={control} type='start' />
|
||||
</Box>
|
||||
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
|
||||
<Label htmlFor='timeFilter'>{t('Until')}</Label>
|
||||
<DateTimeFilter control={control} type='end' />
|
||||
</Box>
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
<Modal.FooterControllers>
|
||||
<Button onClick={onClose}>{t('Cancel')}</Button>
|
||||
<Button primary disabled={!watch('startDate') || !watch('endDate')} onClick={handleSave}>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
</Modal.FooterControllers>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import type { SelectOption } from '@rocket.chat/fuselage';
|
||||
import { InputBoxSkeleton, Select } from '@rocket.chat/fuselage';
|
||||
import { useEndpoint } from '@rocket.chat/ui-contexts';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type InstanceFilterSelectProps = Omit<ComponentProps<typeof Select>, 'options'>;
|
||||
|
||||
export const InstanceFilterSelect = ({ ...props }: InstanceFilterSelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
// @ts-expect-error The endpoint is to be merged in https://github.com/RocketChat/Rocket.Chat/pull/36245
|
||||
const getOptions = useEndpoint('GET', '/apps/logs/instanceIds');
|
||||
|
||||
const { data, isPending } = useQuery({
|
||||
queryKey: ['app-logs-filter-instances'],
|
||||
// @ts-expect-error The endpoint is to be merged in https://github.com/RocketChat/Rocket.Chat/pull/36245
|
||||
queryFn: async () => getOptions(),
|
||||
});
|
||||
|
||||
const options: SelectOption[] = useMemo(() => {
|
||||
// @ts-expect-error The endpoint is to be merged in https://github.com/RocketChat/Rocket.Chat/pull/36245
|
||||
const mappedData: [string, string][] = data?.instanceIds?.map((id: string) => [id, id]) || [];
|
||||
return [['all', t('All')], ...mappedData];
|
||||
}, [data, t]);
|
||||
|
||||
if (isPending) {
|
||||
return <InputBoxSkeleton aria-labelledby={props['aria-labelledby']} aria-busy />;
|
||||
}
|
||||
|
||||
return <Select options={options} {...props} />;
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
import type { SelectOption } from '@rocket.chat/fuselage';
|
||||
import { Select } from '@rocket.chat/fuselage';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const SeverityFilterSelect = ({ ...props }: Omit<ComponentProps<typeof Select>, 'options'>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = [
|
||||
['all', t('All')],
|
||||
['0', t('Warning')],
|
||||
['1', t('Info')],
|
||||
['2', t('Debug')],
|
||||
] as SelectOption[];
|
||||
return <Select {...props} aria-label={t('Severity')} options={options} />;
|
||||
};
|
||||
@ -0,0 +1,157 @@
|
||||
import type { SelectOption } from '@rocket.chat/fuselage';
|
||||
import { Select } from '@rocket.chat/fuselage';
|
||||
import { useSetModal } from '@rocket.chat/ui-contexts';
|
||||
import { endOfDay, endOfWeek, startOfDay, startOfWeek, subMinutes, format } from 'date-fns';
|
||||
import { useState, type ComponentProps } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { DateTimeModalFormData } from './DateTimeModal';
|
||||
import { DateTimeModal } from './DateTimeModal';
|
||||
import { useAppLogsFilterFormContext } from '../useAppLogsFilterForm';
|
||||
|
||||
type DateRange = {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
|
||||
type DateRangeAction = 'all' | 'today' | 'last5Minutes' | 'last15Minutes' | 'last30Minutes' | 'last1Hour' | 'thisWeek' | 'custom';
|
||||
|
||||
type TimeFilterSelectProps = { compactView?: boolean } & Omit<ComponentProps<typeof Select>, 'onChange' | 'options'>;
|
||||
|
||||
export const TimeFilterSelect = ({ compactView = false, ...props }: TimeFilterSelectProps) => {
|
||||
const { setValue, control, getValues } = useAppLogsFilterFormContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const setModal = useSetModal();
|
||||
|
||||
const [customTimeRangeLabel, setCustomTimeRangeOptionLabel] = useState<string>(t('Custom_time_range'));
|
||||
|
||||
const getTimeZoneOffset = (): string => {
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
const absOffset = Math.abs(offset);
|
||||
return `${offset < 0 ? '+' : '-'}${`00${Math.floor(absOffset / 60)}`.slice(-2)}:${`00${absOffset % 60}`.slice(-2)}`;
|
||||
};
|
||||
|
||||
const dateRangeReducer = (action: DateRangeAction): DateRange | undefined => {
|
||||
const now = new Date();
|
||||
switch (action) {
|
||||
case 'today': {
|
||||
return {
|
||||
start: startOfDay(now),
|
||||
end: endOfDay(now),
|
||||
};
|
||||
}
|
||||
case 'last5Minutes': {
|
||||
return {
|
||||
start: subMinutes(now, 5),
|
||||
end: now,
|
||||
};
|
||||
}
|
||||
case 'last15Minutes': {
|
||||
return {
|
||||
start: subMinutes(now, 15),
|
||||
end: now,
|
||||
};
|
||||
}
|
||||
case 'last30Minutes': {
|
||||
return {
|
||||
start: subMinutes(now, 30),
|
||||
end: now,
|
||||
};
|
||||
}
|
||||
case 'last1Hour': {
|
||||
return {
|
||||
start: subMinutes(now, 60),
|
||||
end: now,
|
||||
};
|
||||
}
|
||||
case 'thisWeek': {
|
||||
return {
|
||||
start: startOfWeek(now),
|
||||
end: endOfWeek(now),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onModalSave = (values: DateTimeModalFormData): void => {
|
||||
const { startDate, startTime, endDate, endTime } = values;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
setCustomTimeRangeOptionLabel(t('Custom_time_range'));
|
||||
setModal(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setValue('startDate', startDate);
|
||||
setValue('startTime', startTime || '00:00');
|
||||
setValue('endDate', endDate);
|
||||
setValue('endTime', endTime || '00:00');
|
||||
|
||||
const startIso = new Date(`${startDate}T${startTime || '00:00'}${getTimeZoneOffset()}`);
|
||||
const endIso = new Date(`${endDate}T${endTime || '00:00'}${getTimeZoneOffset()}`);
|
||||
const formattedStartDate = format(new Date(startIso), 'MMM dd, yyyy');
|
||||
const formattedEndDate = format(new Date(endIso), 'MMM dd, yyyy');
|
||||
|
||||
setCustomTimeRangeOptionLabel(
|
||||
`${formattedStartDate}${startTime && startTime !== '00:00' ? `, ${startTime}` : ''} - ${formattedEndDate}${endTime && endTime !== '00:00' ? `, ${endTime}` : ''}`,
|
||||
);
|
||||
|
||||
setModal(null);
|
||||
};
|
||||
|
||||
const onModalClose = (): void => {
|
||||
setValue('timeFilter', 'all');
|
||||
setCustomTimeRangeOptionLabel(t('Custom_time_range'));
|
||||
setModal(null);
|
||||
};
|
||||
|
||||
const handleChange = (action: DateRangeAction): void => {
|
||||
setValue('timeFilter', action);
|
||||
if (action === 'all') {
|
||||
setValue('startTime', undefined);
|
||||
setValue('startDate', undefined);
|
||||
setValue('endTime', undefined);
|
||||
setValue('endDate', undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'custom') {
|
||||
const { startDate = '', startTime = '00:00', endDate = '', endTime = '00:00' } = getValues();
|
||||
// Doing this since the modal is not in the form context, and it is simpler just to pass the default values to a new useForm call
|
||||
setModal(<DateTimeModal onSave={onModalSave} onClose={onModalClose} defaultValues={{ startDate, startTime, endDate, endTime }} />);
|
||||
return;
|
||||
}
|
||||
|
||||
const newRange = dateRangeReducer(action);
|
||||
if (newRange?.start) {
|
||||
setValue('startTime', format(newRange?.start, 'HH:mm'));
|
||||
setValue('startDate', format(newRange?.start, 'yyyy-MM-dd'));
|
||||
}
|
||||
|
||||
if (newRange?.end) {
|
||||
setValue('endTime', format(newRange?.end, 'HH:mm'));
|
||||
setValue('endDate', format(newRange?.end, 'yyyy-MM-dd'));
|
||||
}
|
||||
};
|
||||
|
||||
const options = [
|
||||
['all', t('All_time'), true],
|
||||
['today', t('Today')],
|
||||
['last5Minutes', t('Last_5_minutes')],
|
||||
['last15Minutes', t('Last_15_minutes')],
|
||||
['last30Minutes', t('Last_30_minutes')],
|
||||
['last1Hour', t('Last_1_hour')],
|
||||
['thisWeek', t('This_week')],
|
||||
...(compactView ? [] : [['custom', customTimeRangeLabel]]),
|
||||
] as SelectOption[];
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
name='timeFilter'
|
||||
render={({ field }) => <Select {...props} {...field} onChange={(val) => handleChange(val as DateRangeAction)} options={options} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,64 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`renders AppLogsItem without crashing 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-4jy2w6"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1w4asan"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1rfw4fq"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="eventFilter"
|
||||
>
|
||||
Event
|
||||
</label>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper"
|
||||
>
|
||||
<input
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-text rcx-input-box"
|
||||
id="eventFilter"
|
||||
name="event"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-magnifier rcx-icon rcx-css-1bepdyv"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
class="rcx-box rcx-box--full rcx-button--secondary rcx-button rcx-css-qv9v2f"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="rcx-button--content"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-customize rcx-icon rcx-css-1hdf9ok"
|
||||
>
|
||||
|
||||
</i>
|
||||
Filters
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
@ -0,0 +1,395 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`renders AppLogsItem without crashing 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-vertical-bar rcx-css-ucmcg1"
|
||||
>
|
||||
<span
|
||||
data-focus-scope-start="true"
|
||||
hidden=""
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="contextualbarTitle"
|
||||
class="rcx-box rcx-box--full rcx-vertical-bar rcx-css-1n0hsd8"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-ftwpdg"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1sl6k6j"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-customize rcx-icon rcx-css-x7bl3q rcx-css-g86psg"
|
||||
>
|
||||
|
||||
</i>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-x7bl3q rcx-css-1to6ka7"
|
||||
id="contextualbarTitle"
|
||||
>
|
||||
Filters
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button rcx-css-x7bl3q rcx-css-1yzvz7u"
|
||||
data-qa="ContextualbarActionClose"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-cross rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1svuzur"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-pln26h rcx-css-1cb6i7s"
|
||||
data-overlayscrollbars="host"
|
||||
>
|
||||
<div
|
||||
class="os-size-observer"
|
||||
>
|
||||
<div
|
||||
class="os-size-observer-listener"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class=""
|
||||
data-overlayscrollbars-viewport="scrollbarHidden overflowXHidden overflowYHidden"
|
||||
style="margin-right: 0px; margin-bottom: 0px; margin-left: 0px; top: 0px; left: 0px; width: calc(100% + 0px); padding: 0px 0px 0px 0px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<form
|
||||
class="rcx-box rcx-box--full rcx-css-iag4sp"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1jggbrp rcx-css-1hr7rzd"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="timeFilter"
|
||||
id="timeFilterLabel"
|
||||
>
|
||||
Time
|
||||
</label>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="react-aria-:r8: timeFilterLabel"
|
||||
class="rcx-box rcx-box--full rcx-select rcx-css-1vw6rc6"
|
||||
id="timeFilter"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-a11y-ignore="aria-hidden-focus"
|
||||
data-react-aria-prevent-focus="true"
|
||||
data-testid="hidden-select-container"
|
||||
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
>
|
||||
<label>
|
||||
<select
|
||||
name="timeFilter"
|
||||
tabindex="-1"
|
||||
>
|
||||
<option />
|
||||
<option
|
||||
value="all"
|
||||
>
|
||||
all
|
||||
</option>
|
||||
<option
|
||||
value="today"
|
||||
>
|
||||
today
|
||||
</option>
|
||||
<option
|
||||
value="last5Minutes"
|
||||
>
|
||||
last5Minutes
|
||||
</option>
|
||||
<option
|
||||
value="last15Minutes"
|
||||
>
|
||||
last15Minutes
|
||||
</option>
|
||||
<option
|
||||
value="last30Minutes"
|
||||
>
|
||||
last30Minutes
|
||||
</option>
|
||||
<option
|
||||
value="last1Hour"
|
||||
>
|
||||
last1Hour
|
||||
</option>
|
||||
<option
|
||||
value="thisWeek"
|
||||
>
|
||||
thisWeek
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-css-uyvtjh"
|
||||
id="react-aria-:r8:"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-chevron-down rcx-icon rcx-css-6vi44e"
|
||||
>
|
||||
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1jggbrp rcx-css-1hr7rzd"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="timeFilter"
|
||||
>
|
||||
Logs_from
|
||||
</label>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-omrq7i"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper rcx-css-sdt442"
|
||||
>
|
||||
<input
|
||||
aria-label="Start Date"
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-date rcx-input-box"
|
||||
name="startDate"
|
||||
size="1"
|
||||
type="date"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-calendar rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper rcx-css-nc7kyx"
|
||||
>
|
||||
<input
|
||||
aria-label="Start Time"
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-time rcx-input-box"
|
||||
name="startTime"
|
||||
size="1"
|
||||
type="time"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-clock rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1jggbrp rcx-css-1hr7rzd"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="timeFilter"
|
||||
>
|
||||
Until
|
||||
</label>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-omrq7i"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper rcx-css-sdt442"
|
||||
>
|
||||
<input
|
||||
aria-label="End Date"
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-date rcx-input-box"
|
||||
name="endDate"
|
||||
size="1"
|
||||
type="date"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-calendar rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper rcx-css-nc7kyx"
|
||||
>
|
||||
<input
|
||||
aria-label="End Time"
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-time rcx-input-box"
|
||||
name="endTime"
|
||||
size="1"
|
||||
type="time"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-clock rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1jggbrp rcx-css-1hr7rzd"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="instanceFilter"
|
||||
id="instanceFilterLabel"
|
||||
>
|
||||
Instance
|
||||
</label>
|
||||
<div
|
||||
aria-busy="true"
|
||||
aria-labelledby="instanceFilterLabel"
|
||||
class="rcx-box rcx-box--full rcx-skeleton__input"
|
||||
>
|
||||
<span
|
||||
class="rcx-skeleton rcx-skeleton--text rcx-css-1qcz93u"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1jggbrp rcx-css-1hr7rzd"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="severityFilter"
|
||||
id="severityFilterLabel"
|
||||
>
|
||||
Severity
|
||||
</label>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Severity"
|
||||
aria-labelledby="react-aria-:rg: react-aria-:rb: severityFilterLabel"
|
||||
class="rcx-box rcx-box--full rcx-select rcx-css-1vw6rc6"
|
||||
id="severityFilter"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-a11y-ignore="aria-hidden-focus"
|
||||
data-react-aria-prevent-focus="true"
|
||||
data-testid="hidden-select-container"
|
||||
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
>
|
||||
<label>
|
||||
<select
|
||||
name="severity"
|
||||
tabindex="-1"
|
||||
>
|
||||
<option />
|
||||
<option
|
||||
value="all"
|
||||
>
|
||||
all
|
||||
</option>
|
||||
<option
|
||||
value="0"
|
||||
>
|
||||
0
|
||||
</option>
|
||||
<option
|
||||
value="1"
|
||||
>
|
||||
1
|
||||
</option>
|
||||
<option
|
||||
value="2"
|
||||
>
|
||||
2
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-css-uyvtjh"
|
||||
id="react-aria-:rg:"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-chevron-down rcx-icon rcx-css-6vi44e"
|
||||
>
|
||||
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
class="os-scrollbar os-scrollbar-horizontal os-theme-dark os-scrollbar-auto-hide os-scrollbar-auto-hide-hidden os-scrollbar-handle-interactive os-scrollbar-cornerless os-scrollbar-unusable"
|
||||
style="--os-scroll-percent: 0; --os-viewport-percent: 0; --os-scroll-direction: 0;"
|
||||
>
|
||||
<div
|
||||
class="os-scrollbar-track"
|
||||
>
|
||||
<div
|
||||
class="os-scrollbar-handle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="os-scrollbar os-scrollbar-vertical os-theme-dark os-scrollbar-auto-hide os-scrollbar-auto-hide-hidden os-scrollbar-handle-interactive os-scrollbar-cornerless os-scrollbar-unusable"
|
||||
style="--os-scroll-percent: 0; --os-viewport-percent: 0; --os-scroll-direction: 0;"
|
||||
>
|
||||
<div
|
||||
class="os-scrollbar-track"
|
||||
>
|
||||
<div
|
||||
class="os-scrollbar-handle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
data-focus-scope-end="true"
|
||||
hidden=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
@ -0,0 +1,223 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`renders AppLogsItem without crashing 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-4jy2w6"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1w4asan"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1rfw4fq"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="eventFilter"
|
||||
>
|
||||
Event
|
||||
</label>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper"
|
||||
>
|
||||
<input
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-text rcx-input-box"
|
||||
id="eventFilter"
|
||||
name="event"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-magnifier rcx-icon rcx-css-1bepdyv"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1rfw4fq"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="timeFilter"
|
||||
id="timeFilterLabel"
|
||||
>
|
||||
Time
|
||||
</label>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="react-aria-:r7: timeFilterLabel"
|
||||
class="rcx-box rcx-box--full rcx-select rcx-css-1vw6rc6"
|
||||
id="timeFilter"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-a11y-ignore="aria-hidden-focus"
|
||||
data-react-aria-prevent-focus="true"
|
||||
data-testid="hidden-select-container"
|
||||
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
>
|
||||
<label>
|
||||
<select
|
||||
name="timeFilter"
|
||||
tabindex="-1"
|
||||
>
|
||||
<option />
|
||||
<option
|
||||
value="all"
|
||||
>
|
||||
all
|
||||
</option>
|
||||
<option
|
||||
value="today"
|
||||
>
|
||||
today
|
||||
</option>
|
||||
<option
|
||||
value="last5Minutes"
|
||||
>
|
||||
last5Minutes
|
||||
</option>
|
||||
<option
|
||||
value="last15Minutes"
|
||||
>
|
||||
last15Minutes
|
||||
</option>
|
||||
<option
|
||||
value="last30Minutes"
|
||||
>
|
||||
last30Minutes
|
||||
</option>
|
||||
<option
|
||||
value="last1Hour"
|
||||
>
|
||||
last1Hour
|
||||
</option>
|
||||
<option
|
||||
value="thisWeek"
|
||||
>
|
||||
thisWeek
|
||||
</option>
|
||||
<option
|
||||
value="custom"
|
||||
>
|
||||
custom
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-css-4w7o7u"
|
||||
id="react-aria-:r7:"
|
||||
>
|
||||
All_time
|
||||
</span>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-chevron-down rcx-icon rcx-css-6vi44e"
|
||||
>
|
||||
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1rfw4fq"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="instanceFilter"
|
||||
id="instanceFilterLabel"
|
||||
>
|
||||
Instance
|
||||
</label>
|
||||
<div
|
||||
aria-busy="true"
|
||||
aria-labelledby="instanceFilterLabel"
|
||||
class="rcx-box rcx-box--full rcx-skeleton__input"
|
||||
>
|
||||
<span
|
||||
class="rcx-skeleton rcx-skeleton--text rcx-css-1qcz93u"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1rfw4fq"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
>
|
||||
Severity
|
||||
</label>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Severity"
|
||||
aria-labelledby="react-aria-:rf: react-aria-:ra:"
|
||||
class="rcx-box rcx-box--full rcx-select rcx-css-1vw6rc6"
|
||||
id="severityFilter"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
data-a11y-ignore="aria-hidden-focus"
|
||||
data-react-aria-prevent-focus="true"
|
||||
data-testid="hidden-select-container"
|
||||
style="border: 0px; clip-path: inset(50%); height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap;"
|
||||
>
|
||||
<label>
|
||||
<select
|
||||
name="severity"
|
||||
tabindex="-1"
|
||||
>
|
||||
<option />
|
||||
<option
|
||||
value="all"
|
||||
>
|
||||
all
|
||||
</option>
|
||||
<option
|
||||
value="0"
|
||||
>
|
||||
0
|
||||
</option>
|
||||
<option
|
||||
value="1"
|
||||
>
|
||||
1
|
||||
</option>
|
||||
<option
|
||||
value="2"
|
||||
>
|
||||
2
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-css-4w7o7u"
|
||||
id="react-aria-:rf:"
|
||||
>
|
||||
All
|
||||
</span>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-chevron-down rcx-icon rcx-css-6vi44e"
|
||||
>
|
||||
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
@ -0,0 +1,202 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`renders AppLogsItem without crashing 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-4jy2w6"
|
||||
>
|
||||
<dialog
|
||||
aria-modal="true"
|
||||
class="rcx-box rcx-box--full rcx-modal"
|
||||
open=""
|
||||
title="Custom_time_range"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-modal__inner rcx-css-1e2ego0"
|
||||
>
|
||||
<header
|
||||
class="rcx-box rcx-box--full rcx-modal__header"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-modal__header-inner"
|
||||
>
|
||||
<h2
|
||||
class="rcx-box rcx-box--full rcx-modal__title rcx-css-trljwa rcx-css-lma364"
|
||||
>
|
||||
Custom_time_range
|
||||
</h2>
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button rcx-css-trljwa rcx-css-lma364"
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-cross rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-modal__content rcx-css-1vw7itl"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-modal__content-wrapper rcx-css-56vo9k"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-6ua2r5"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="timeFilter"
|
||||
>
|
||||
Logs_from
|
||||
</label>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-omrq7i"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper rcx-css-sdt442"
|
||||
>
|
||||
<input
|
||||
aria-label="Start Date"
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-date rcx-input-box"
|
||||
name="startDate"
|
||||
size="1"
|
||||
type="date"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-calendar rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper rcx-css-nc7kyx"
|
||||
>
|
||||
<input
|
||||
aria-label="Start Time"
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-time rcx-input-box"
|
||||
name="startTime"
|
||||
size="1"
|
||||
type="time"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-clock rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1rfw4fq"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label"
|
||||
for="timeFilter"
|
||||
>
|
||||
Until
|
||||
</label>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-omrq7i"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper rcx-css-sdt442"
|
||||
>
|
||||
<input
|
||||
aria-label="End Date"
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-date rcx-input-box"
|
||||
name="endDate"
|
||||
size="1"
|
||||
type="date"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-calendar rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-label rcx-box rcx-box--full rcx-box--animated rcx-input-box__wrapper rcx-css-nc7kyx"
|
||||
>
|
||||
<input
|
||||
aria-label="End Time"
|
||||
class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box--type-time rcx-input-box"
|
||||
name="endTime"
|
||||
size="1"
|
||||
type="time"
|
||||
value=""
|
||||
/>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-input-box__addon"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-icon--name-clock rcx-icon rcx-css-4pvxx3"
|
||||
>
|
||||
|
||||
</i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-modal__footer rcx-css-17mu816"
|
||||
>
|
||||
<div
|
||||
class="rcx-button-group rcx-button-group--align-end"
|
||||
role="group"
|
||||
>
|
||||
<button
|
||||
class="rcx-box rcx-box--full rcx-button rcx-button-group__item"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="rcx-button--content"
|
||||
>
|
||||
Cancel
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="rcx-box rcx-box--full rcx-button--primary rcx-button rcx-button-group__item"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="rcx-button--content"
|
||||
>
|
||||
Apply
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
@ -0,0 +1,17 @@
|
||||
import { useForm, useFormContext } from 'react-hook-form';
|
||||
|
||||
export type AppLogsFilterFormData = {
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
instance?: string;
|
||||
severity?: 'all' | '0' | '1' | '2';
|
||||
event?: string;
|
||||
timeFilter?: string;
|
||||
};
|
||||
|
||||
export const useAppLogsFilterForm = () =>
|
||||
useForm<AppLogsFilterFormData>({ defaultValues: { severity: 'all', instance: 'all', timeFilter: 'all' } });
|
||||
|
||||
export const useAppLogsFilterFormContext = () => useFormContext<AppLogsFilterFormData>();
|
||||
@ -0,0 +1,6 @@
|
||||
import { useBreakpoints } from '@rocket.chat/fuselage-hooks';
|
||||
|
||||
export const useCompactMode = () => {
|
||||
const breakpoint = useBreakpoints();
|
||||
return !breakpoint.includes('lg');
|
||||
};
|
||||
@ -8,17 +8,32 @@ export const useLogs = ({
|
||||
appId,
|
||||
current,
|
||||
itemsPerPage,
|
||||
logLevel,
|
||||
method,
|
||||
startDate,
|
||||
endDate,
|
||||
instanceId,
|
||||
}: {
|
||||
appId: string;
|
||||
current: number;
|
||||
itemsPerPage: number;
|
||||
logLevel?: '0' | '1' | '2';
|
||||
method?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
instanceId?: string;
|
||||
}): UseQueryResult<OperationResult<'GET', '/apps/:id/logs'>> => {
|
||||
const query = useMemo(
|
||||
() => ({
|
||||
count: itemsPerPage,
|
||||
offset: current,
|
||||
...(logLevel && { logLevel }),
|
||||
...(method && { method }),
|
||||
...(startDate && { startDate }),
|
||||
...(endDate && { endDate }),
|
||||
...(instanceId && { instanceId }),
|
||||
}),
|
||||
[itemsPerPage, current],
|
||||
[itemsPerPage, current, logLevel, method, startDate, endDate, instanceId],
|
||||
);
|
||||
const logs = useEndpoint('GET', '/apps/:id/logs', { id: appId });
|
||||
|
||||
|
||||
@ -9,8 +9,8 @@ declare module '@rocket.chat/ui-contexts' {
|
||||
pathname: '/marketplace';
|
||||
};
|
||||
'marketplace': {
|
||||
pattern: '/marketplace/:context?/:page?/:id?/:version?/:tab?';
|
||||
pathname: `/marketplace${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}`;
|
||||
pattern: '/marketplace/:context?/:page?/:id?/:version?/:tab?/:contextualBar?';
|
||||
pathname: `/marketplace${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}${`/${string}` | ''}`;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ export const registerMarketplaceRoute = createRouteGroup(
|
||||
lazy(() => import('./MarketplaceRouter')),
|
||||
);
|
||||
|
||||
registerMarketplaceRoute('/:context?/:page?/:id?/:version?/:tab?', {
|
||||
registerMarketplaceRoute('/:context?/:page?/:id?/:version?/:tab?/:contextualBar?', {
|
||||
name: 'marketplace',
|
||||
component: lazy(() => import('./AppsRoute')),
|
||||
});
|
||||
|
||||
@ -428,6 +428,7 @@
|
||||
"All_roles": "All roles",
|
||||
"All_rooms": "All rooms",
|
||||
"All_status": "All status",
|
||||
"All_time": "All time",
|
||||
"All_users": "All users",
|
||||
"All_users_in_the_channel_can_write_new_messages": "All users in the channel can write new messages",
|
||||
"All_visible": "All visible",
|
||||
@ -1517,6 +1518,7 @@
|
||||
"Custom_Sound_Info": "Custom Sound Info",
|
||||
"Custom_Sound_Saved_Successfully": "Custom sound saved successfully",
|
||||
"Custom_Status": "Custom Status",
|
||||
"Custom_time_range": "Custom time range",
|
||||
"Custom_Translations": "Custom Translations",
|
||||
"Custom_Translations_Description": "Should be a valid JSON where keys are languages containing a dictionary of key and translations. Example: `{\"en\": {\"Channels\": \"Rooms\"},\"pt\": {\"Channels\": \"Salas\"}}`",
|
||||
"Custom_User_Status": "Custom User Status",
|
||||
@ -2860,6 +2862,10 @@
|
||||
"Language_Version": "English Version",
|
||||
"Language_setting_warning": "<strong>Server language setting does not affect user's client</strong><br/>Each user has their own preference for language, that will be kept if this setting is changed.",
|
||||
"Larger_amounts_of_active_connections": "For larger amounts of active connections you can consider our <1>multiple instance solutions</1>.",
|
||||
"Last_5_minutes": "Last 5 minutes",
|
||||
"Last_15_minutes": "Last 15 minutes",
|
||||
"Last_30_minutes": "Last 30 minutes",
|
||||
"Last_1_hour": "Last 1 hour",
|
||||
"Last_15_days": "Last 15 Days",
|
||||
"Last_30_days": "Last 30 Days",
|
||||
"Last_6_months": "Last 6 months",
|
||||
@ -3117,6 +3123,7 @@
|
||||
"Logout_Device": "Log out device",
|
||||
"Logout_Others": "Logout From Other Logged In Locations",
|
||||
"Logs": "Logs",
|
||||
"Logs_from": "Logs from",
|
||||
"Logs_Description": "Configure how server logs are received.",
|
||||
"Long_press_to_do_x": "Long press to do {{action}}",
|
||||
"Longest_chat_duration": "Longest Chat Duration",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user