Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
510e427
feat: New App Logs Filters
MartinSchoeler Jun 6, 2025
5b3c248
chore: Translations
MartinSchoeler Jun 23, 2025
3408780
chore: change values
MartinSchoeler Jun 24, 2025
86ec758
chore: touchup components
MartinSchoeler Jun 25, 2025
cc8c67e
test: add unit tests
MartinSchoeler Jun 26, 2025
7fa0f87
test: Contextual Bar tests
MartinSchoeler Jun 26, 2025
ba36d2e
chore: touchup tests
MartinSchoeler Jun 26, 2025
b1091e0
Extract custom hook
tassoevan Jul 7, 2025
55b2cc0
Rearrange stories
tassoevan Jul 7, 2025
4f3fb23
Remove extra fragment
tassoevan Jul 7, 2025
510f34f
chore: :)
MartinSchoeler Jul 7, 2025
921463a
qa-finding: Fix issue when selecting dates with no time
MartinSchoeler Jul 7, 2025
4d9e44c
qa-finding: Add proper Noresults and GenericError to list
MartinSchoeler Jul 7, 2025
3473ea1
qa-finding: Fix crash on empty result
MartinSchoeler Jul 7, 2025
590a1f5
qa-finding: fix missing margin
MartinSchoeler Jul 7, 2025
9a94d95
qa-finding: Filter order
MartinSchoeler Jul 7, 2025
652db66
qa-finding: Retain previous value when opening modal
MartinSchoeler Jul 7, 2025
30ddce0
Use form types
tassoevan Jul 8, 2025
8d6eb2f
Update snapshots
tassoevan Jul 8, 2025
ac2b28a
Create curvy-dancers-try.md
MartinSchoeler Jul 10, 2025
86b8bec
fix: timezone offset
MartinSchoeler Jul 10, 2025
24b8aed
chore: suppress ts errors on unmerged endpoints
MartinSchoeler Jul 11, 2025
96eb04f
chore: debounce text
MartinSchoeler Jul 11, 2025
36e2916
Update .changeset/curvy-dancers-try.md
MartinSchoeler Jul 14, 2025
1e802b9
Merge branch 'develop' into feat/apps-log-page
MartinSchoeler Jul 14, 2025
28ab74e
Merge branch 'develop' into feat/apps-log-page
tassoevan Jul 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curvy-dancers-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": minor
---

Adds a new filter to the Logs tab of the App Details page.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) {
Expand All @@ -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);

Expand All @@ -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) => {
Expand All @@ -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>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 />;
Original file line number Diff line number Diff line change
@@ -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>
);
};
Loading
Loading