diff --git a/.changeset/serious-apricots-compare.md b/.changeset/serious-apricots-compare.md new file mode 100644 index 0000000000000..b2e9e3e4fab52 --- /dev/null +++ b/.changeset/serious-apricots-compare.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/apps-engine': minor +'@rocket.chat/meteor': minor +--- + +Adds a button to the apps logs UI that exports logs as a downloadable file diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx index baee5a9cc3d7a..ddc18e540216f 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx @@ -25,7 +25,7 @@ const AppLogs = ({ id }: { id: string }): ReactElement => { const debouncedEvent = useDebouncedValue(event, 500); - const { data, isSuccess, isError, isFetching, error } = useLogs({ + const { data, isSuccess, isError, error, isFetching } = useLogs({ appId: id, current, itemsPerPage, @@ -50,13 +50,12 @@ const AppLogs = ({ id }: { id: string }): ReactElement => { return ( <> - + {isFetching && } {isError && } - {isSuccess && data?.logs?.length === 0 ? ( - - ) : ( + {!isFetching && isSuccess && data?.logs?.length === 0 && } + {!isFetching && isSuccess && data?.logs?.length > 0 && ( {data?.logs?.map((log, index) => )} diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItem.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItem.tsx index 9a13b40b81a26..7bb6f9b65a04c 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItem.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItem.tsx @@ -32,13 +32,17 @@ const AppLogsItem = ({ regionId, ...props }: AppLogsItemProps) => { ); + const handleClick = () => { + setExpanded(!expanded); + }; + const anchorRef = useRef(null); const formatDateAndTime = useFormatDateAndTime(); return ( <> - setExpanded(!expanded)}> + {title} diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilter.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilter.tsx index 2f61a71071a19..6622ec0add02b 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilter.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilter.tsx @@ -1,18 +1,27 @@ -import { Box, Button, Icon, Label, Palette, TextInput } from '@rocket.chat/fuselage'; -import { useRouter } from '@rocket.chat/ui-contexts'; +import { Box, Button, Icon, IconButton, Label, Palette, TextInput } from '@rocket.chat/fuselage'; +import { useRouter, useSetModal } from '@rocket.chat/ui-contexts'; import { Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import CompactFilterOptions from './AppsLogsFilterOptionsCompact'; import { InstanceFilterSelect } from './InstanceFilterSelect'; import { SeverityFilterSelect } from './SeverityFilterSelect'; import { TimeFilterSelect } from './TimeFilterSelect'; import { useCompactMode } from '../../../useCompactMode'; import { useAppLogsFilterFormContext } from '../useAppLogsFilterForm'; +import { ExportLogsModal } from './ExportLogsModal'; -export const AppLogsFilter = () => { +type AppsLogsFilterProps = { + isLoading?: boolean; + noResults?: boolean; +}; + +export const AppLogsFilter = ({ isLoading = false, noResults = false }: AppsLogsFilterProps) => { const { t } = useTranslation(); - const { control } = useAppLogsFilterFormContext(); + const { control, getValues } = useAppLogsFilterFormContext(); + + const setModal = useSetModal(); const router = useRouter(); @@ -26,6 +35,10 @@ export const AppLogsFilter = () => { ); }; + const openExportModal = () => { + setModal( setModal(null)} filterValues={getValues()} />); + }; + const compactMode = useCompactMode(); return ( @@ -70,11 +83,25 @@ export const AppLogsFilter = () => { } /> )} + {!compactMode && ( + openExportModal()} + aria-label={noResults ? t('No_data_to_export') : t('Export')} + aria-disabled={noResults} + /> + )} {compactMode && ( )} + {compactMode && } ); }; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterCompact.spec.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterCompact.spec.tsx index 6bf622b6b19f1..9ca525a51febd 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterCompact.spec.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterCompact.spec.tsx @@ -1,4 +1,3 @@ -/* 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'; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterContextualBar.spec.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterContextualBar.spec.tsx index 5d1c1f9f3f9e5..67572297984cb 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterContextualBar.spec.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterContextualBar.spec.tsx @@ -1,4 +1,3 @@ -/* 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'; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterExpanded.spec.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterExpanded.spec.tsx index 842a4e9f00c10..073504af83d87 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterExpanded.spec.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppLogsFilterExpanded.spec.tsx @@ -1,4 +1,3 @@ -/* 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'; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppsLogsFilterOptionsCompact.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppsLogsFilterOptionsCompact.tsx new file mode 100644 index 0000000000000..8b163b92092b5 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/AppsLogsFilterOptionsCompact.tsx @@ -0,0 +1,26 @@ +import { Box, Icon, Menu } from '@rocket.chat/fuselage'; +import { useTranslation } from 'react-i18next'; + +type CompactFilterOptionsProps = { + handleExportLogs: () => void; + isLoading: boolean; +}; + +const CompactFilterOptions = ({ handleExportLogs, ...props }: CompactFilterOptionsProps) => { + const { t } = useTranslation(); + + const menuOptions = { + exportLogs: { + label: ( + + + {t('Export')} + + ), + action: handleExportLogs, + }, + }; + return ; +}; + +export default CompactFilterOptions; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/DateTimeModal.spec.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/DateTimeModal.spec.tsx index e925f0f3191ea..75497569ea2ae 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/DateTimeModal.spec.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/DateTimeModal.spec.tsx @@ -1,4 +1,3 @@ -/* 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'; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/ExportLogsModal.spec.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/ExportLogsModal.spec.tsx new file mode 100644 index 0000000000000..8aabba5fe68be --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/ExportLogsModal.spec.tsx @@ -0,0 +1,23 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './ExportLogsModal.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +test.each(testCases)(`renders without crashing`, async (_storyname, Story) => { + const view = render(, { + wrapper: mockAppRoot().build(), + }); + expect(view.baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('Should have no a11y violations', async (_storyname, Story) => { + const { container } = render(, { wrapper: mockAppRoot().build() }); + + const results = await axe(container); + + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/ExportLogsModal.stories.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/ExportLogsModal.stories.tsx new file mode 100644 index 0000000000000..6dc1afdfebe0c --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/ExportLogsModal.stories.tsx @@ -0,0 +1,39 @@ +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 { ExportLogsModal } from './ExportLogsModal'; + +export default { + title: 'Marketplace/AppDetailsPage/AppLogs/Filters/ExportLogsModal', + component: ExportLogsModal, + args: { + onClose: action('onClose'), + filterValues: { + severity: 'all', + event: '', + startDate: '', + endDate: '', + }, + }, + decorators: [ + mockAppRoot().buildStoryDecorator(), + (fn) => { + const methods = useForm({}); + + return ( + + {fn()} + + ); + }, + ], + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export const Default: StoryFn> = (args) => ; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/ExportLogsModal.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/ExportLogsModal.tsx new file mode 100644 index 0000000000000..d7f51518cf9aa --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/ExportLogsModal.tsx @@ -0,0 +1,205 @@ +import { + Box, + Button, + Field, + FieldLabel, + FieldRow, + Label, + Modal, + ModalClose, + ModalContent, + ModalFooter, + ModalFooterControllers, + ModalHeader, + ModalTitle, + NumberInput, + RadioButton, +} from '@rocket.chat/fuselage'; +import { useRouteParameter } from '@rocket.chat/ui-contexts'; +import { Controller, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import type { AppLogsFilterFormData } from '../useAppLogsFilterForm'; + +type ExportLogsModalProps = { + onClose: () => void; + filterValues: AppLogsFilterFormData; +}; + +type FormDataType = { + type: 'json' | 'csv'; + count: 'max' | 'custom'; + customExportAmount: number; +}; + +export const ExportLogsModal = ({ onClose, filterValues }: ExportLogsModalProps) => { + const { t } = useTranslation(); + + const appId = useRouteParameter('id'); + + const { + control, + watch, + getValues, + formState: { isValid }, + } = useForm({ + defaultValues: { + type: 'json', + count: 'max', + customExportAmount: 100, + }, + }); + + const type = watch('type'); + const count = watch('count'); + + const getFileUrl = ({ + severity, + event, + startDate, + endDate, + count, + type, + startTime, + endTime, + }: AppLogsFilterFormData & { type: 'json' | 'csv'; count: number }): string => { + let baseUrl = `/api/apps/${appId}/export-logs?`; + if (severity && severity !== 'all') { + baseUrl += `logLevel=${severity}&`; + } + if (event) { + baseUrl += `method=${event}&`; + } + if (startDate) { + baseUrl += `startDate=${new Date(`${startDate}T${startTime}`).toISOString()}&`; + } + if (endDate) { + baseUrl += `endDate=${new Date(`${endDate}T${endTime}`).toISOString()}&`; + } + if (count) { + baseUrl += `count=${count}&`; + } + baseUrl += `type=${type}`; + return baseUrl; + }; + + const handleConfirm = (): void => { + const url = getFileUrl({ ...filterValues, type, count: count === 'max' ? 2000 : getValues('customExportAmount') }); + window.open(url, '_blank', 'noopener noreferrer'); + onClose(); + }; + + return ( + + + {t('Export')} + + + + + + + + {t('JSON')} + ( + onChange('json')} + aria-describedby='JSONField' + checked={value === 'json'} + /> + )} + /> + + + + + {t('CSV')} + ( + onChange('csv')} + aria-describedby='plainTextField' + checked={value === 'csv'} + /> + )} + /> + + + + + + + + {t('Max_logs_export')} + ( + onChange('max')} + aria-describedby='maxLogsExport' + checked={value === 'max'} + /> + )} + /> + + + + + {t('Limit_number_of_logs')} + ( + onChange('custom')} + aria-describedby='customMaxLogs' + checked={value === 'custom'} + /> + )} + /> + + ( + + )} + /> + + + + + + + + + + + ); +}; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/AppLogsFilterCompact.spec.tsx.snap b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/AppLogsFilterCompact.spec.tsx.snap index fef2400775f2e..d961a55584c29 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/AppLogsFilterCompact.spec.tsx.snap +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/AppLogsFilterCompact.spec.tsx.snap @@ -57,6 +57,19 @@ exports[`renders AppLogsItem without crashing 1`] = ` Filters + diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/AppLogsFilterExpanded.spec.tsx.snap b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/AppLogsFilterExpanded.spec.tsx.snap index e7d50dd0c75a4..0a1b43163c9fa 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/AppLogsFilterExpanded.spec.tsx.snap +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/AppLogsFilterExpanded.spec.tsx.snap @@ -216,6 +216,20 @@ exports[`renders AppLogsItem without crashing 1`] = ` + diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/ExportLogsModal.spec.tsx.snap b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/ExportLogsModal.spec.tsx.snap new file mode 100644 index 0000000000000..7fd1d9b924120 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Filters/__snapshots__/ExportLogsModal.spec.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`renders without crashing 1`] = ` + +
+
+ +
+
+
+

+ Export +

+ +
+
+
+
+
+ +
+ + +