Skip to content

Commit 600a145

Browse files
feat: New App Logs Filters
1 parent 8b0d95c commit 600a145

File tree

12 files changed

+591
-54
lines changed

12 files changed

+591
-54
lines changed

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

Lines changed: 91 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
22
import type { App, SettingValue } from '@rocket.chat/core-typings';
33
import { Button, ButtonGroup, Box } from '@rocket.chat/fuselage';
4-
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
4+
import { useBreakpoints, useEffectEvent } from '@rocket.chat/fuselage-hooks';
55
import { useTranslation, useRouteParameter, useToastMessageDispatch, usePermission, useRouter } from '@rocket.chat/ui-contexts';
66
import type { ReactElement } from 'react';
77
import { useMemo, useCallback } from 'react';
@@ -14,6 +14,7 @@ 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';
1718
import AppReleases from './tabs/AppReleases';
1819
import AppRequests from './tabs/AppRequests/AppRequests';
1920
import AppSecurity from './tabs/AppSecurity/AppSecurity';
@@ -23,6 +24,17 @@ import { Page, PageFooter, PageHeader, PageScrollableContentWithShadow } from '.
2324

2425
type AppDetailsPageFormData = Record<string, SettingValue>;
2526

27+
type AppLogsFilterFormData = {
28+
startDate?: string;
29+
endDate?: string;
30+
startTime?: string;
31+
endTime?: string;
32+
instance?: string;
33+
severity?: string;
34+
event?: string;
35+
timeFilter?: string;
36+
};
37+
2638
type AppDetailsPageProps = {
2739
id: App['id'];
2840
};
@@ -35,7 +47,10 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
3547

3648
const tab = useRouteParameter('tab');
3749
const context = useRouteParameter('context');
50+
const contextualBar = useRouteParameter('contextualBar');
3851
const appData = useAppInfo(id, context || '');
52+
const breakpoint = useBreakpoints(); // ["xs", "sm", "md", "lg", "xl", xxl"]
53+
const compactMode = !breakpoint.includes('lg');
3954

4055
const handleReturn = useEffectEvent((): void => {
4156
if (!context) {
@@ -48,6 +63,20 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
4863
});
4964
});
5065

66+
const handleReturnToLogs = useEffectEvent((): void => {
67+
if (!context) {
68+
return;
69+
}
70+
71+
router.navigate(
72+
{
73+
name: 'marketplace',
74+
params: { ...router.getRouteParameters(), contextualBar: '' },
75+
},
76+
{ replace: true },
77+
);
78+
});
79+
5180
const { installed, settings, privacyPolicySummary, permissions, tosLink, privacyLink, name } = appData || {};
5281
const isSecurityVisible = Boolean(privacyPolicySummary || permissions || tosLink || privacyLink);
5382

@@ -58,12 +87,14 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
5887
);
5988
}, [settings]);
6089

61-
const methods = useForm<AppDetailsPageFormData>({ values: reducedSettings });
90+
const settingsFormMethods = useForm<AppDetailsPageFormData>({ values: reducedSettings });
6291
const {
6392
handleSubmit,
6493
reset,
6594
formState: { isDirty, isSubmitting },
66-
} = methods;
95+
} = settingsFormMethods;
96+
97+
const logsFilterFormMethods = useForm<AppLogsFilterFormData>({ defaultValues: { severity: 'all', instance: 'all', timeFilter: 'all' } });
6798

6899
const saveAppSettings = useCallback(
69100
async (data: AppDetailsPageFormData) => {
@@ -81,56 +112,67 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => {
81112
handleAPIError(e);
82113
}
83114
},
84-
[dispatchToastMessage, id, name, settings, reset],
115+
[id, settings, reset, dispatchToastMessage, t, name],
85116
);
86117

87118
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}
119+
<Page flexDirection='row'>
120+
<Page flexDirection='column' h='full'>
121+
<PageHeader title={t('App_Info')} onClickBack={handleReturn} />
122+
<PageScrollableContentWithShadow pi={24} pbs={24} pbe={0} h='full'>
123+
<Box w='full' alignSelf='center' h='full' display='flex' flexDirection='column'>
124+
{!appData && <AppDetailsPageLoading />}
125+
{appData && (
126+
<>
127+
<AppDetailsPageHeader app={appData} />
128+
<AppDetailsPageTabs
129+
context={context || ''}
130+
installed={installed}
131+
isSecurityVisible={isSecurityVisible}
132+
settings={settings}
133+
tab={tab}
111134
/>
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>
135+
{Boolean(!tab || tab === 'details') && <AppDetails app={appData} />}
136+
{tab === 'requests' && <AppRequests id={id} isAdminUser={isAdminUser} />}
137+
{tab === 'security' && isSecurityVisible && (
138+
<AppSecurity
139+
privacyPolicySummary={privacyPolicySummary}
140+
appPermissions={permissions}
141+
tosLink={tosLink}
142+
privacyLink={privacyLink}
143+
/>
144+
)}
145+
{tab === 'releases' && <AppReleases id={id} />}
146+
{Boolean(tab === 'settings' && settings && Object.values(settings).length) && (
147+
<FormProvider {...settingsFormMethods}>
148+
<AppSettings settings={settings || {}} />
149+
</FormProvider>
150+
)}
151+
{(tab === 'logs' || tab === 'logs-filter') && (
152+
<FormProvider {...logsFilterFormMethods}>
153+
<AppLogs id={id} />
154+
</FormProvider>
155+
)}
156+
</>
157+
)}
158+
</Box>
159+
</PageScrollableContentWithShadow>
160+
<PageFooter isDirty={isDirty}>
161+
<ButtonGroup>
162+
<Button onClick={() => reset()}>{t('Cancel')}</Button>
163+
{installed && isAdminUser && (
164+
<Button primary loading={isSubmitting} onClick={handleSubmit(saveAppSettings)}>
165+
{t('Save_changes')}
166+
</Button>
167+
)}
168+
</ButtonGroup>
169+
</PageFooter>
170+
</Page>
171+
{compactMode && contextualBar === 'filter-logs' && (
172+
<FormProvider {...logsFilterFormMethods}>
173+
<AppLogsFilterContextualBar onClose={handleReturnToLogs} />
174+
</FormProvider>
175+
)}
134176
</Page>
135177
);
136178
};

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Accordion, Box, Pagination } from '@rocket.chat/fuselage';
22
import type { ReactElement } from 'react';
3+
import { useFormContext } from 'react-hook-form';
34
import { useTranslation } from 'react-i18next';
45

56
import AppLogsItem from './AppLogsItem';
7+
import { AppLogsFilter } from './Filters/AppLogsFilter';
68
import { CustomScrollbars } from '../../../../../components/CustomScrollbars';
79
import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination';
810
import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime';
@@ -12,11 +14,29 @@ import { useLogs } from '../../../hooks/useLogs';
1214
const AppLogs = ({ id }: { id: string }): ReactElement => {
1315
const { t } = useTranslation();
1416
const formatDateAndTime = useFormatDateAndTime();
17+
18+
const { watch } = useFormContext();
19+
20+
const { startTime, endTime, startDate, endDate, event, severity, instance } = watch();
21+
1522
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
16-
const { data, isSuccess, isError, isLoading } = useLogs({ appId: id, current, itemsPerPage });
23+
24+
const { data, isSuccess, isError, isLoading } = useLogs({
25+
appId: id,
26+
current,
27+
itemsPerPage,
28+
...(instance !== 'all' && { instanceId: instance }),
29+
...(severity !== 'all' && { logLevel: severity }),
30+
method: event,
31+
...(startTime && startDate && { startDate: new Date(`${startDate}T${startTime}`).toISOString() }),
32+
...(endTime && endDate && { endDate: new Date(`${endDate}T${endTime}`).toISOString() }),
33+
});
1734

1835
return (
1936
<>
37+
<Box pb='x16'>
38+
<AppLogsFilter />
39+
</Box>
2040
{isLoading && <AccordionLoading />}
2141
{isError && (
2242
<Box maxWidth='x600' alignSelf='center'>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { mockAppRoot } from '@rocket.chat/mock-providers';
2+
import type { Meta } from '@storybook/react';
3+
import { FormProvider, useForm } from 'react-hook-form';
4+
5+
import { AppLogsFilter } from './AppLogsFilter';
6+
7+
export default {
8+
title: 'Components/AppLogsFilter',
9+
component: AppLogsFilter,
10+
args: {},
11+
decorators: [
12+
mockAppRoot()
13+
// @ts-expect-error mock endpoint while we do not have the real one
14+
.withEndpoint('GET', '/v1/apps/instances', () => [
15+
['instanceId', 'instanceName'],
16+
['node1', 'node1'],
17+
['node2', 'node2'],
18+
['node3', 'node3'],
19+
['node4', 'node4'],
20+
])
21+
.withTranslations('en', 'core', { App_name: 'App Name' })
22+
.buildStoryDecorator(),
23+
(fn) => {
24+
const methods = useForm({});
25+
26+
return <FormProvider {...methods}>{fn()}</FormProvider>;
27+
},
28+
],
29+
parameters: {
30+
layout: 'fullscreen',
31+
},
32+
} satisfies Meta<typeof AppLogsFilter>;
33+
34+
export const Simple = () => <AppLogsFilter />;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Box, Button, Icon, Label, Palette, TextInput } from '@rocket.chat/fuselage';
2+
import { useBreakpoints } from '@rocket.chat/fuselage-hooks';
3+
import { useRouter } from '@rocket.chat/ui-contexts';
4+
import { Controller, useFormContext } from 'react-hook-form';
5+
import { useTranslation } from 'react-i18next';
6+
7+
import { InstanceFilterSelect } from './InstanceFilterSelect';
8+
import { SeverityFilterSelect } from './SeverityFilterSelect';
9+
import { TimeFilterSelect } from './TimeFilterSelect';
10+
11+
export const AppLogsFilter = () => {
12+
const { t } = useTranslation();
13+
14+
const { control } = useFormContext();
15+
16+
const router = useRouter();
17+
18+
const openContextualBar = () => {
19+
router.navigate(
20+
{
21+
name: 'marketplace',
22+
params: { ...router.getRouteParameters(), contextualBar: 'filter-logs' },
23+
},
24+
{ replace: true },
25+
);
26+
};
27+
28+
const breakpoint = useBreakpoints(); // ["xs", "sm", "md", "lg", "xl", xxl"]
29+
const compactMode = !breakpoint.includes('lg');
30+
31+
return (
32+
<Box display='flex' flexDirection='row' width='full' flexWrap='wrap' alignContent='flex-end'>
33+
<Box display='flex' flexDirection='column' mie='x10' 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='x20' />}
41+
id='eventFilter'
42+
{...field}
43+
/>
44+
)}
45+
/>
46+
</Box>
47+
{!compactMode && (
48+
<Box display='flex' flexDirection='column' mie='x10' flexGrow={1}>
49+
<Label htmlFor='instanceFilter'>{t('Instance')}</Label>
50+
<Controller control={control} name='instance' render={({ field }) => <InstanceFilterSelect id='instanceFilter' {...field} />} />
51+
</Box>
52+
)}
53+
{!compactMode && (
54+
<Box display='flex' flexDirection='column' mie='x10' flexGrow={1}>
55+
<Label>{t('Severity')}</Label>
56+
<Controller control={control} name='severity' render={({ field }) => <SeverityFilterSelect id='severityFilter' {...field} />} />
57+
</Box>
58+
)}
59+
{!compactMode && (
60+
<Box display='flex' flexDirection='column' mie='x10' flexGrow={1}>
61+
<Label htmlFor='timeFilter'>{t('Time')}</Label>
62+
<TimeFilterSelect id='timeFilter' />
63+
</Box>
64+
)}
65+
{!compactMode && (
66+
<Button alignSelf='flex-end' secondary width={120} mie='x10'>
67+
{t('Expand_all')}
68+
</Button>
69+
)}
70+
{!compactMode && <Button alignSelf='flex-end' icon='download' secondary square mie='x10' onClick={() => undefined} />}
71+
{compactMode && (
72+
<Button alignSelf='flex-end' icon='customize' secondary mie='x10' onClick={() => openContextualBar()}>
73+
{t('Filters')}
74+
</Button>
75+
)}
76+
</Box>
77+
);
78+
};

0 commit comments

Comments
 (0)