Skip to content

Commit 484d36d

Browse files
feat: [FE] New App Logs Filters (#36288)
Co-authored-by: Tasso Evangelista <[email protected]>
1 parent a00729f commit 484d36d

28 files changed

+1911
-64
lines changed

.changeset/curvy-dancers-try.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@rocket.chat/meteor": minor
3+
---
4+
5+
Adds a new filter to the Logs tab of the App Details page.

apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx

Lines changed: 80 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ import { handleAPIError } from '../helpers/handleAPIError';
1414
import { useAppInfo } from '../hooks/useAppInfo';
1515
import AppDetails from './tabs/AppDetails';
1616
import AppLogs from './tabs/AppLogs';
17+
import { AppLogsFilterContextualBar } from './tabs/AppLogs/Filters/AppLogsFilterContextualBar';
18+
import { useAppLogsFilterForm } from './tabs/AppLogs/useAppLogsFilterForm';
1719
import AppReleases from './tabs/AppReleases';
1820
import AppRequests from './tabs/AppRequests/AppRequests';
1921
import AppSecurity from './tabs/AppSecurity/AppSecurity';
2022
import AppSettings from './tabs/AppSettings';
23+
import { useCompactMode } from './useCompactMode';
2124
import { AppClientOrchestratorInstance } from '../../../apps/orchestrator';
2225
import { Page, PageFooter, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page';
2326

@@ -35,7 +38,9 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
3538

3639
const tab = useRouteParameter('tab');
3740
const context = useRouteParameter('context');
41+
const contextualBar = useRouteParameter('contextualBar');
3842
const appData = useAppInfo(id, context || '');
43+
const compactMode = useCompactMode();
3944

4045
const handleReturn = useEffectEvent((): void => {
4146
if (!context) {
@@ -48,6 +53,20 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
4853
});
4954
});
5055

56+
const handleReturnToLogs = useEffectEvent((): void => {
57+
if (!context) {
58+
return;
59+
}
60+
61+
router.navigate(
62+
{
63+
name: 'marketplace',
64+
params: { ...router.getRouteParameters(), contextualBar: '' },
65+
},
66+
{ replace: true },
67+
);
68+
});
69+
5170
const { installed, settings, privacyPolicySummary, permissions, tosLink, privacyLink, name } = appData || {};
5271
const isSecurityVisible = Boolean(privacyPolicySummary || permissions || tosLink || privacyLink);
5372

@@ -58,12 +77,14 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
5877
);
5978
}, [settings]);
6079

61-
const methods = useForm<AppDetailsPageFormData>({ values: reducedSettings });
80+
const settingsFormMethods = useForm<AppDetailsPageFormData>({ values: reducedSettings });
6281
const {
6382
handleSubmit,
6483
reset,
6584
formState: { isDirty, isSubmitting },
66-
} = methods;
85+
} = settingsFormMethods;
86+
87+
const logsFilterFormMethods = useAppLogsFilterForm();
6788

6889
const saveAppSettings = useCallback(
6990
async (data: AppDetailsPageFormData) => {
@@ -81,56 +102,67 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
81102
handleAPIError(e);
82103
}
83104
},
84-
[dispatchToastMessage, id, name, settings, reset],
105+
[id, settings, reset, dispatchToastMessage, t, name],
85106
);
86107

87108
return (
88-
<Page flexDirection='column' h='full'>
89-
<PageHeader title={t('App_Info')} onClickBack={handleReturn} />
90-
<PageScrollableContentWithShadow pi={24} pbs={24} pbe={0} h='full'>
91-
<Box w='full' alignSelf='center' h='full' display='flex' flexDirection='column'>
92-
{!appData && <AppDetailsPageLoading />}
93-
{appData && (
94-
<>
95-
<AppDetailsPageHeader app={appData} />
96-
<AppDetailsPageTabs
97-
context={context || ''}
98-
installed={installed}
99-
isSecurityVisible={isSecurityVisible}
100-
settings={settings}
101-
tab={tab}
102-
/>
103-
{Boolean(!tab || tab === 'details') && <AppDetails app={appData} />}
104-
{tab === 'requests' && <AppRequests id={id} isAdminUser={isAdminUser} />}
105-
{tab === 'security' && isSecurityVisible && (
106-
<AppSecurity
107-
privacyPolicySummary={privacyPolicySummary}
108-
appPermissions={permissions}
109-
tosLink={tosLink}
110-
privacyLink={privacyLink}
109+
<Page flexDirection='row'>
110+
<Page flexDirection='column' h='full'>
111+
<PageHeader title={t('App_Info')} onClickBack={handleReturn} />
112+
<PageScrollableContentWithShadow pi={24} pbs={24} pbe={0} h='full'>
113+
<Box w='full' alignSelf='center' h='full' display='flex' flexDirection='column'>
114+
{!appData && <AppDetailsPageLoading />}
115+
{appData && (
116+
<>
117+
<AppDetailsPageHeader app={appData} />
118+
<AppDetailsPageTabs
119+
context={context || ''}
120+
installed={installed}
121+
isSecurityVisible={isSecurityVisible}
122+
settings={settings}
123+
tab={tab}
111124
/>
112-
)}
113-
{tab === 'releases' && <AppReleases id={id} />}
114-
{Boolean(tab === 'settings' && settings && Object.values(settings).length) && (
115-
<FormProvider {...methods}>
116-
<AppSettings settings={settings || {}} />
117-
</FormProvider>
118-
)}
119-
{tab === 'logs' && <AppLogs id={id} />}
120-
</>
121-
)}
122-
</Box>
123-
</PageScrollableContentWithShadow>
124-
<PageFooter isDirty={isDirty}>
125-
<ButtonGroup>
126-
<Button onClick={() => reset()}>{t('Cancel')}</Button>
127-
{installed && isAdminUser && (
128-
<Button primary loading={isSubmitting} onClick={handleSubmit(saveAppSettings)}>
129-
{t('Save_changes')}
130-
</Button>
131-
)}
132-
</ButtonGroup>
133-
</PageFooter>
125+
{Boolean(!tab || tab === 'details') && <AppDetails app={appData} />}
126+
{tab === 'requests' && <AppRequests id={id} isAdminUser={isAdminUser} />}
127+
{tab === 'security' && isSecurityVisible && (
128+
<AppSecurity
129+
privacyPolicySummary={privacyPolicySummary}
130+
appPermissions={permissions}
131+
tosLink={tosLink}
132+
privacyLink={privacyLink}
133+
/>
134+
)}
135+
{tab === 'releases' && <AppReleases id={id} />}
136+
{Boolean(tab === 'settings' && settings && Object.values(settings).length) && (
137+
<FormProvider {...settingsFormMethods}>
138+
<AppSettings settings={settings || {}} />
139+
</FormProvider>
140+
)}
141+
{(tab === 'logs' || tab === 'logs-filter') && (
142+
<FormProvider {...logsFilterFormMethods}>
143+
<AppLogs id={id} />
144+
</FormProvider>
145+
)}
146+
</>
147+
)}
148+
</Box>
149+
</PageScrollableContentWithShadow>
150+
<PageFooter isDirty={isDirty}>
151+
<ButtonGroup>
152+
<Button onClick={() => reset()}>{t('Cancel')}</Button>
153+
{installed && isAdminUser && (
154+
<Button primary loading={isSubmitting} onClick={handleSubmit(saveAppSettings)}>
155+
{t('Save_changes')}
156+
</Button>
157+
)}
158+
</ButtonGroup>
159+
</PageFooter>
160+
</Page>
161+
{compactMode && contextualBar === 'filter-logs' && (
162+
<FormProvider {...logsFilterFormMethods}>
163+
<AppLogsFilterContextualBar onClose={handleReturnToLogs} />
164+
</FormProvider>
165+
)}
134166
</Page>
135167
);
136168
};

apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,64 @@
11
import { Box, Pagination } from '@rocket.chat/fuselage';
2-
import type { ReactElement } from 'react';
2+
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
3+
import { useMemo, type ReactElement } from 'react';
34
import { useTranslation } from 'react-i18next';
45

56
import AppLogsItem from './AppLogsItem';
67
import { CollapsiblePanel } from './Components/CollapsiblePanel';
8+
import { AppLogsFilter } from './Filters/AppLogsFilter';
9+
import { useAppLogsFilterFormContext } from './useAppLogsFilterForm';
710
import { CustomScrollbars } from '../../../../../components/CustomScrollbars';
11+
import GenericError from '../../../../../components/GenericError';
12+
import GenericNoResults from '../../../../../components/GenericNoResults';
813
import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination';
914
import AccordionLoading from '../../../components/AccordionLoading';
1015
import { useLogs } from '../../../hooks/useLogs';
1116

1217
const AppLogs = ({ id }: { id: string }): ReactElement => {
1318
const { t } = useTranslation();
19+
20+
const { watch } = useAppLogsFilterFormContext();
21+
22+
const { startTime, endTime, startDate, endDate, event, severity, instance } = watch();
23+
1424
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
15-
const { data, isSuccess, isError, isLoading } = useLogs({ appId: id, current, itemsPerPage });
25+
26+
const debouncedEvent = useDebouncedValue(event, 500);
27+
28+
const { data, isSuccess, isError, isFetching, error } = useLogs({
29+
appId: id,
30+
current,
31+
itemsPerPage,
32+
...(instance !== 'all' && { instanceId: instance }),
33+
...(severity !== 'all' && { logLevel: severity }),
34+
method: debouncedEvent,
35+
...(startTime && startDate && { startDate: new Date(`${startDate}T${startTime}`).toISOString() }),
36+
...(endTime && endDate && { endDate: new Date(`${endDate}T${endTime}`).toISOString() }),
37+
});
38+
39+
const parsedError = useMemo(() => {
40+
if (error) {
41+
// TODO: Check why tanstack expects a default Error but we return {error: string}
42+
if ((error as unknown as { error: string }).error === 'Invalid date range') {
43+
return t('error-invalid-dates');
44+
}
45+
46+
return t('Something_Went_Wrong');
47+
}
48+
}, [error, t]);
1649

1750
return (
1851
<>
19-
{isLoading && <AccordionLoading />}
20-
{isError && (
21-
<Box maxWidth='x600' alignSelf='center'>
22-
{t('App_not_found')}
23-
</Box>
24-
)}
25-
{isSuccess && (
52+
<Box pb={16}>
53+
<AppLogsFilter />
54+
</Box>
55+
{isFetching && <AccordionLoading />}
56+
{isError && <GenericError title={parsedError} />}
57+
{isSuccess && data?.logs?.length === 0 ? (
58+
<GenericNoResults />
59+
) : (
2660
<CustomScrollbars>
27-
<CollapsiblePanel width='100%' alignSelf='center'>
61+
<CollapsiblePanel aria-busy={isFetching || event !== debouncedEvent} width='100%' alignSelf='center'>
2862
{data?.logs?.map((log, index) => <AppLogsItem regionId={log._id} key={`${index}-${log._createdAt}`} {...log} />)}
2963
</CollapsiblePanel>
3064
</CustomScrollbars>

apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItem.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import AppLogsItem from './AppLogsItem';
55
import { CollapsiblePanel } from './Components/CollapsiblePanel';
66

77
export default {
8-
title: 'Components/AppLogsItem',
8+
title: 'Marketplace/AppDetailsPage/AppLogs/AppLogsItem',
99
component: AppLogsItem,
1010
decorators: [(fn) => <CollapsiblePanel style={{ padding: 24 }}>{fn()}</CollapsiblePanel>],
1111
args: {

apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsiblePanel.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CollapsiblePanel } from './CollapsiblePanel';
55
import { CollapsibleRegion } from './CollapsibleRegion';
66

77
export default {
8-
title: 'Components/CollapsiblePanel',
8+
title: 'Marketplace/AppDetailsPage/AppLogs/Components/CollapsiblePanel',
99
component: CollapsiblePanel,
1010

1111
args: {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Box } from '@rocket.chat/fuselage';
2+
import { mockAppRoot } from '@rocket.chat/mock-providers';
3+
import type { Meta } from '@storybook/react';
4+
import { FormProvider } from 'react-hook-form';
5+
6+
import { AppLogsFilter } from './AppLogsFilter';
7+
import { useAppLogsFilterForm } from '../useAppLogsFilterForm';
8+
9+
export default {
10+
title: 'Marketplace/AppDetailsPage/AppLogs/Filters/AppLogsFilter',
11+
component: AppLogsFilter,
12+
args: {},
13+
decorators: [
14+
mockAppRoot()
15+
// @ts-expect-error The endpoint is to be merged in https://github.com/RocketChat/Rocket.Chat/pull/36245
16+
.withEndpoint('GET', '/apps/logs/instanceIds', () => ({
17+
success: true,
18+
instanceIds: ['instance-1', 'instance-2', 'instance-3'],
19+
}))
20+
.buildStoryDecorator(),
21+
(fn) => {
22+
const methods = useAppLogsFilterForm();
23+
24+
return (
25+
<FormProvider {...methods}>
26+
<Box p={16}>{fn()}</Box>
27+
</FormProvider>
28+
);
29+
},
30+
],
31+
parameters: {
32+
layout: 'fullscreen',
33+
},
34+
} satisfies Meta<typeof AppLogsFilter>;
35+
36+
export const Default = () => <AppLogsFilter />;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Box, Button, Icon, Label, Palette, TextInput } from '@rocket.chat/fuselage';
2+
import { useRouter } from '@rocket.chat/ui-contexts';
3+
import { Controller } from 'react-hook-form';
4+
import { useTranslation } from 'react-i18next';
5+
6+
import { InstanceFilterSelect } from './InstanceFilterSelect';
7+
import { SeverityFilterSelect } from './SeverityFilterSelect';
8+
import { TimeFilterSelect } from './TimeFilterSelect';
9+
import { useCompactMode } from '../../../useCompactMode';
10+
import { useAppLogsFilterFormContext } from '../useAppLogsFilterForm';
11+
12+
export const AppLogsFilter = () => {
13+
const { t } = useTranslation();
14+
15+
const { control } = useAppLogsFilterFormContext();
16+
17+
const router = useRouter();
18+
19+
const openContextualBar = () => {
20+
router.navigate(
21+
{
22+
name: 'marketplace',
23+
params: { ...router.getRouteParameters(), contextualBar: 'filter-logs' },
24+
},
25+
{ replace: true },
26+
);
27+
};
28+
29+
const compactMode = useCompactMode();
30+
31+
return (
32+
<Box display='flex' flexDirection='row' width='full' flexWrap='wrap' alignContent='flex-end'>
33+
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
34+
<Label htmlFor='eventFilter'>{t('Event')}</Label>
35+
<Controller
36+
control={control}
37+
name='event'
38+
render={({ field }) => (
39+
<TextInput
40+
addon={<Icon color={Palette.text['font-secondary-info']} name='magnifier' size={20} />}
41+
id='eventFilter'
42+
{...field}
43+
/>
44+
)}
45+
/>
46+
</Box>
47+
{!compactMode && (
48+
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
49+
<Label id='timeFilterLabel' htmlFor='timeFilter'>
50+
{t('Time')}
51+
</Label>
52+
<TimeFilterSelect id='timeFilter' aria-labelledby='timeFilterLabel' />
53+
</Box>
54+
)}
55+
{!compactMode && (
56+
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
57+
<Label id='instanceFilterLabel' htmlFor='instanceFilter'>
58+
{t('Instance')}
59+
</Label>
60+
<Controller
61+
control={control}
62+
name='instance'
63+
render={({ field }) => <InstanceFilterSelect aria-labelledby='instanceFilterLabel' id='instanceFilter' {...field} />}
64+
/>
65+
</Box>
66+
)}
67+
{!compactMode && (
68+
<Box display='flex' flexDirection='column' mie={10} flexGrow={1}>
69+
<Label>{t('Severity')}</Label>
70+
<Controller control={control} name='severity' render={({ field }) => <SeverityFilterSelect id='severityFilter' {...field} />} />
71+
</Box>
72+
)}
73+
{compactMode && (
74+
<Button alignSelf='flex-end' icon='customize' secondary mie={10} onClick={() => openContextualBar()}>
75+
{t('Filters')}
76+
</Button>
77+
)}
78+
</Box>
79+
);
80+
};

0 commit comments

Comments
 (0)