diff --git a/.changeset/nice-items-marry.md b/.changeset/nice-items-marry.md new file mode 100644 index 0000000000000..a86d2d4852c9c --- /dev/null +++ b/.changeset/nice-items-marry.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Implements new component for Apps Logs View 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 588b516ec0a72..1f336f043a0ad 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogs.tsx @@ -1,17 +1,16 @@ -import { Accordion, Box, Pagination } from '@rocket.chat/fuselage'; +import { Box, Pagination } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import AppLogsItem from './AppLogsItem'; +import { CollapsiblePanel } from './Components/CollapsiblePanel'; import { CustomScrollbars } from '../../../../../components/CustomScrollbars'; import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination'; -import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import AccordionLoading from '../../../components/AccordionLoading'; import { useLogs } from '../../../hooks/useLogs'; const AppLogs = ({ id }: { id: string }): ReactElement => { const { t } = useTranslation(); - const formatDateAndTime = useFormatDateAndTime(); const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); const { data, isSuccess, isError, isLoading } = useLogs({ appId: id, current, itemsPerPage }); @@ -25,16 +24,9 @@ const AppLogs = ({ id }: { id: string }): ReactElement => { )} {isSuccess && ( - - {data?.logs?.map((log) => ( - - ))} - + + {data?.logs?.map((log, index) => )} + )} [Story.storyName || 'Story', Story]); + +test.each(testCases)(`renders AppLogsItem without crashing`, async (_storyname, Story) => { + const view = render(, { wrapper: mockAppRoot().build() }); + expect(view.baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('AppLogsItem 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/AppLogsItem.stories.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItem.stories.tsx new file mode 100644 index 0000000000000..f459fdc881242 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItem.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryFn } from '@storybook/react'; +import type { ComponentProps } from 'react'; + +import AppLogsItem from './AppLogsItem'; +import { CollapsiblePanel } from './Components/CollapsiblePanel'; + +export default { + title: 'Components/AppLogsItem', + component: AppLogsItem, + decorators: [(fn) => {fn()}], + args: { + _id: '683da1e32025cfca7b3d8238', + appId: 'ce0e318b-ffc0-4ce4-832b-f1b464beb22a', + method: 'app:checkPostMessageSent', + entries: [ + { + caller: 'anonymous OR constructor -> handleApp', + severity: 'debug', + method: 'app:checkPostMessageSent', + timestamp: '2025-06-02T13:06:43.772Z', + args: ["'checkPostMessageSent' is being called..."], + }, + { + caller: 'anonymous OR constructor', + severity: 'debug', + method: 'app:checkPostMessageSent', + timestamp: '2025-06-02T13:06:43.777Z', + args: ["'checkPostMessageSent' was successfully called! The result is:", 'false'], + }, + ], + startTime: '2025-06-02T13:06:43.771Z', + endTime: '2025-06-02T13:06:43.777Z', + totalTime: 6, + _createdAt: '2025-06-02T13:06:43.777Z', + instanceId: 'b97ce445-b9ff-4513-8206-966afd799cd6', + _updatedAt: '2025-06-02T13:06:43.778Z', + }, + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta>; + +export const Simple: StoryFn> = (args) => ; 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 538b6ecf60c37..9a13b40b81a26 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItem.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItem.tsx @@ -1,29 +1,61 @@ -import type { ILogEntry } from '@rocket.chat/core-typings'; -import { Box, AccordionItem } from '@rocket.chat/fuselage'; +import type { ILogItem } from '@rocket.chat/core-typings'; +import { Box, Divider } from '@rocket.chat/fuselage'; +import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import AppLogsItemEntry from './AppLogsItemEntry'; +import { AppsLogItemField } from './AppLogsItemField'; +import { CollapseButton } from './Components/CollapseButton'; +import { CollapsibleRegion } from './Components/CollapsibleRegion'; +import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; -type AppLogsItemProps = { - entries: ILogEntry[]; - instanceId: string; - title: string; -}; +export type AppLogsItemProps = { + regionId: string; +} & ILogItem; -const AppLogsItem = ({ entries, instanceId, title, ...props }: AppLogsItemProps) => { +const AppLogsItem = ({ regionId, ...props }: AppLogsItemProps) => { const { t } = useTranslation(); + const [expanded, setExpanded] = useState(false); + const title = ( + <> + {props.entries.map(({ severity, timestamp, caller, args }, index) => { + const parsedArgs = args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' '); + return ( + {`${timestamp} ${severity} ${caller} ${parsedArgs}`} + ); + })} + + ); + + const anchorRef = useRef(null); + + const formatDateAndTime = useFormatDateAndTime(); return ( - - {instanceId && ( - - {t('Instance')}: {instanceId} + <> + setExpanded(!expanded)}> + {title} + + + + {props.instanceId && } + {props.totalTime !== undefined && } + {props.startTime && } + {props.method && } + + {t('Full_log')} - )} - {entries.map(({ severity, timestamp, caller, args }, i) => ( - - ))} - + + + + + + ); }; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx index 1743fa6013e7e..6e541d476b75a 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemEntry.tsx @@ -1,29 +1,21 @@ +import type { ILogItem } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import DOMPurify from 'dompurify'; -import { useTranslation } from 'react-i18next'; import { useHighlightedCode } from '../../../../../hooks/useHighlightedCode'; type AppLogsItemEntryProps = { - severity: string; - timestamp: string; - caller: string; - args: unknown; + fullLog: ILogItem; }; -const AppLogsItemEntry = ({ severity, timestamp, caller, args }: AppLogsItemEntryProps) => { - const { t } = useTranslation(); - +const AppLogsItemEntry = ({ fullLog }: AppLogsItemEntryProps) => { return ( - - {severity}: {timestamp} {t('Caller')}: {caller} -
 					
 				
diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemField.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemField.tsx new file mode 100644 index 0000000000000..9cc149af7df77 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/AppLogsItemField.tsx @@ -0,0 +1,16 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactNode } from 'react'; + +type AppsLogItemFieldProps = { + field: ReactNode | string; + label: string; +} & ComponentProps; + +export const AppsLogItemField = ({ field, label, ...props }: AppsLogItemFieldProps) => { + return ( + + {label} + {field} + + ); +}; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapseButton.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapseButton.tsx new file mode 100644 index 0000000000000..0d3678e8546ee --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapseButton.tsx @@ -0,0 +1,43 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Chevron, Palette } from '@rocket.chat/fuselage'; +import type { CSSProperties, ReactNode } from 'react'; + +type CollapseButtonProps = { + children: ReactNode; + regionId: string; + expanded?: boolean; + onClick: () => void; +}; + +export const CollapseButton = ({ regionId, children, expanded, onClick }: CollapseButtonProps) => { + const clickable = css` + background: ${Palette.surface['surface-light']}; + + &:hover { + background: ${Palette.surface['surface-tint']}; + } + `; + const style: CSSProperties = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }; + return ( + + + + + {children} + + + + ); +}; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsiblePanel.stories.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsiblePanel.stories.tsx new file mode 100644 index 0000000000000..00ffa82b088b3 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsiblePanel.stories.tsx @@ -0,0 +1,41 @@ +import type { StoryFn } from '@storybook/react'; + +import { CollapseButton } from './CollapseButton'; +import { CollapsiblePanel } from './CollapsiblePanel'; +import { CollapsibleRegion } from './CollapsibleRegion'; + +export default { + title: 'Components/CollapsiblePanel', + component: CollapsiblePanel, + + args: { + expanded: true, + }, + + parameters: { + layout: 'centered', + }, +}; + +const Template: StoryFn = (args) => { + return ( + + { + args.expanded = !args.expanded; + }} + expanded={args.expanded} + regionId='collapse-item' + > + Click Me + + +

This is the content of the panel that can be activated.

+ +

More content can go here.

+
+
+ ); +}; + +export const Default = Template.bind({}); diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsiblePanel.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsiblePanel.tsx new file mode 100644 index 0000000000000..c44464f4a4717 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsiblePanel.tsx @@ -0,0 +1,12 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; + +type CollapsiblePanelProps = ComponentProps; + +export const CollapsiblePanel = (props: CollapsiblePanelProps) => { + return ( + + {props.children} + + ); +}; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsibleRegion.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsibleRegion.tsx new file mode 100644 index 0000000000000..70b51dde6bae2 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/Components/CollapsibleRegion.tsx @@ -0,0 +1,26 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentProps, ReactNode } from 'react'; + +type CollapsibleRegionProps = { + children: ReactNode; + expanded?: boolean; +} & ComponentProps; + +export const CollapsibleRegion = ({ children, expanded, ...props }: CollapsibleRegionProps) => { + return ( + + {children} + + ); +}; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/__snapshots__/AppLogsItem.spec.tsx.snap b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/__snapshots__/AppLogsItem.spec.tsx.snap new file mode 100644 index 0000000000000..07e24d58a9824 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/__snapshots__/AppLogsItem.spec.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders AppLogsItem without crashing 1`] = ` + +
+
+
+ +
+
+
+
+
+ Instance +
+ b97ce445-b9ff-4513-8206-966afd799cd6 +
+
+
+ Total_time +
+ 6ms +
+
+
+ Time +
+ June 2, 2025 1:06 PM +
+
+
+ Event +
+ app:checkPostMessageSent +
+
+
+ Full_log +
+
+
+
+
+                
+                  Loading
+                
+              
+
+
+
+
+
+
+
+
+
+ +`; diff --git a/packages/apps-engine/src/definition/accessors/ILogEntry.ts b/packages/apps-engine/src/definition/accessors/ILogEntry.ts index 55d6dcfec6b25..2d5b16a953432 100644 --- a/packages/apps-engine/src/definition/accessors/ILogEntry.ts +++ b/packages/apps-engine/src/definition/accessors/ILogEntry.ts @@ -19,4 +19,6 @@ export interface ILogEntry { timestamp: Date; /** The items which were logged. */ args: Array; + /** The method which was logged. */ + method?: string; } diff --git a/packages/core-typings/src/ILogs.ts b/packages/core-typings/src/ILogs.ts index 2cac48e1912bb..5cb802e8e9bff 100644 --- a/packages/core-typings/src/ILogs.ts +++ b/packages/core-typings/src/ILogs.ts @@ -3,6 +3,7 @@ export interface ILogEntry { caller: string; severity: string; timestamp: string; + method?: string; } export interface ILogItem { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index f7528f93f6b0f..7f2f87bb65ebf 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2285,6 +2285,7 @@ "From_Email": "From Email", "From_email_warning": "Warning: The field From is subject to your mail server settings.", "Full_Name": "Full Name", + "Full_log": "Full log", "Full_Screen": "Full Screen", "Fully_integrated_voip_receive_internal_external_calls_without_switching_between_apps_external_systems": "Fully-integrated Rocket.Chat VoIP allows your team to make and receive internal and external calls without switching between apps or external systems.", "Gaming": "Gaming", @@ -5111,6 +5112,7 @@ "Top_5_agents_with_the_most_conversations": "Top 5 agents with the most conversations", "Topic": "Topic", "Total": "Total", + "Total_time": "Total time", "Total_Discussions": "Discussions", "Total_Threads": "Threads", "Total_abandoned_chats": "Total Abandoned Chats", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index 27004240058c8..979705fa546c7 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -2282,6 +2282,7 @@ "From_Email": "E-mail de", "From_email_warning": "Aviso: O campo De está sujeito às configurações do seu servidor de e-mails.", "Full_Name": "Nome completo", + "Full_log": "Log completo", "Full_Screen": "Tela cheia", "Fully_integrated_voip_receive_internal_external_calls_without_switching_between_apps_external_systems": "O Rocket.Chat VoIP totalmente integrado permite que sua equipe faça e receba chamadas internas e externas sem alternar entre aplicativos ou sistemas externos.", "Gaming": "Jogos", @@ -5097,6 +5098,7 @@ "Top_5_agents_with_the_most_conversations": "Top 5 agentes com mais conversas", "Topic": "Tópico", "Total": "Total", + "Total_time": "Tempo total", "Total_Discussions": "Discussões", "Total_Threads": "Tópicos", "Total_abandoned_chats": "Total de conversas abandonadas",