Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/nice-items-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/i18n": patch
---
Comment thread
tassoevan marked this conversation as resolved.

Implements new component for Apps Logs View
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { Accordion, Box, Pagination } from '@rocket.chat/fuselage';
import { Box, Pagination } from '@rocket.chat/fuselage';
import { useId } from 'react';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import AppLogsItem from './AppLogsItem';
import { Collapse } from './Components/Collapse';
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 });
const collapseId = useId();
Comment thread
MartinSchoeler marked this conversation as resolved.
Outdated

return (
<>
Expand All @@ -25,16 +26,9 @@ const AppLogs = ({ id }: { id: string }): ReactElement => {
)}
{isSuccess && (
<CustomScrollbars>
<Accordion width='100%' alignSelf='center'>
{data?.logs?.map((log) => (
<AppLogsItem
key={log._createdAt}
title={`${formatDateAndTime(log._createdAt)}: "${log.method}" (${log.totalTime}ms)`}
instanceId={log.instanceId}
entries={log.entries}
/>
))}
</Accordion>
<Collapse width='100%' alignSelf='center'>
{data?.logs?.map((log) => <AppLogsItem collapseId={collapseId} key={log._createdAt} {...log} />)}
</Collapse>
</CustomScrollbars>
)}
<Pagination
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/naming-convention */
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 './AppLogsItem.stories';

const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);

test.each(testCases)(`renders AppLogsItem without crashing`, async (_storyname, Story) => {
const view = render(<Story />, { wrapper: mockAppRoot().build() });
expect(view.baseElement).toMatchSnapshot();
});

test.each(testCases)('AppLogsItem should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });

const results = await axe(container);
expect(results).toHaveNoViolations();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Meta, StoryFn } from '@storybook/react';
import type { ComponentProps } from 'react';

import AppLogsItem from './AppLogsItem';
import { Collapse } from './Components/Collapse';

export default {
title: 'Components/AppLogsItem',
component: AppLogsItem,
decorators: [(fn) => <Collapse style={{ padding: 24 }}>{fn()}</Collapse>],
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<ComponentProps<typeof AppLogsItem>>;

export const Simple: StoryFn<ComponentProps<typeof AppLogsItem>> = (args) => <AppLogsItem {...args} />;
Original file line number Diff line number Diff line change
@@ -1,29 +1,59 @@
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 { format, parseISO } from 'date-fns';
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';

type AppLogsItemProps = {
entries: ILogEntry[];
instanceId: string;
title: string;
};
export type AppLogsItemProps = {
collapseId: string;
} & ILogItem;

const AppLogsItem = ({ entries, instanceId, title, ...props }: AppLogsItemProps) => {
const AppLogsItem = ({ collapseId, ...props }: AppLogsItemProps) => {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
const title = (
<>
{props.entries.map(({ severity, timestamp, caller, args }) => {
const parsedArgs = args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ');
return (
<Box
lineHeight={20}
mbe={4}
fontFamily='mono'
key={`${severity}-${timestamp}-${caller}`}
>{`${timestamp} ${severity} ${caller} ${parsedArgs}`}</Box>
);
})}
</>
);

const anchorRef = useRef<HTMLDivElement>(null);

return (
<AccordionItem title={title} {...props}>
{instanceId && (
<Box color='default'>
{t('Instance')}: {instanceId}
<>
<CollapseButton _id={collapseId} expanded={expanded} onClick={() => setExpanded(!expanded)}>
<Box ref={anchorRef}>{title}</Box>
</CollapseButton>

<CollapsibleRegion expanded={expanded} _id={collapseId} pbs={expanded ? 16 : '0px'} mis={36}>
{props.instanceId && <AppsLogItemField mbs={0} field={props.instanceId} label='Instance' />}
{props.totalTime !== undefined && <AppsLogItemField field={`${props.totalTime}ms`} label={t('Total_time')} />}
{props.startTime && <AppsLogItemField field={format(parseISO(props.startTime), 'MMMM d, yyyy h:mm a')} label={t('Time')} />}
{props.method && <AppsLogItemField field={props.method} label={t('Event')} />}
<Box mbs={16} display='flex' color='default' flexDirection='column'>
<Box fontWeight={700}>{t('Full_log')}</Box>
</Box>
)}
{entries.map(({ severity, timestamp, caller, args }, i) => (
<AppLogsItemEntry key={i} severity={severity} timestamp={timestamp} caller={caller} args={args} />
))}
</AccordionItem>
<AppLogsItemEntry fullLog={props} />
</CollapsibleRegion>
<Box is='dt'>
<Divider mb={0} />
</Box>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Box color='default'>
<Box>
{severity}: {timestamp} {t('Caller')}: {caller}
</Box>
<Box withRichContent width='full'>
<pre>
<code
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(useHighlightedCode('json', JSON.stringify(args, null, 2))),
__html: DOMPurify.sanitize(useHighlightedCode('json', JSON.stringify(fullLog, null, 2))),
}}
/>
</pre>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Box } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactNode } from 'react';

type AppsLogItemFieldProps = {
field: ReactNode | string;
label: string;
} & ComponentProps<typeof Box>;

export const AppsLogItemField = ({ field, label, ...props }: AppsLogItemFieldProps) => {
return (
<Box mb={16} display='flex' color='default' flexDirection='column' {...props}>
<Box fontWeight={700}>{label}</Box>
{field}
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { StoryFn } from '@storybook/react';

import { Collapse } from './Collapse';
import { CollapseButton } from './CollapseButton';
import { CollapsibleRegion } from './CollapsibleRegion';

export default {
title: 'Components/Collapse',
component: Collapse,

args: {
expanded: true,
},

parameters: {
layout: 'centered',
},
};

const Template: StoryFn = (args) => {
return (
<Collapse>
<CollapseButton
onClick={() => {
args.expanded = !args.expanded;
}}
expanded={args.expanded}
_id='collapse-item'
>
Click Me
</CollapseButton>
<CollapsibleRegion expanded={args.expanded} _id='collapse-item'>
<p>This is the content of the panel that can be activated.</p>
<button>Click Me</button>
<p>More content can go here.</p>
</CollapsibleRegion>
</Collapse>
);
};

export const Default = Template.bind({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Box } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';

export const Collapse = (props: ComponentProps<typeof Box>) => {
return (
<Box {...props} is='dl'>
{props.children}
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -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;
_id: string;
expanded?: boolean;
onClick: () => void;
};

export const CollapseButton = ({ _id, 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 (
<Box is='dt' style={style}>
<Box
is='button'
role='button'
onClick={onClick}
className={clickable}
aria-expanded={expanded}
aria-controls={_id}
display='flex'
flexDirection='row'
width='full'
focusable
color={Palette.text['font-default']}
>
<Chevron size={32} down={!expanded} up={expanded} style={{ alignSelf: 'flex-start' }} />
<Box pb='x4' pi='x4' fontWeight='700'>
{children}
</Box>
</Box>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Box } from '@rocket.chat/fuselage';
import type { ComponentProps, ReactNode } from 'react';

type CollapsibleRegionProps = {
children: ReactNode;
_id: string;
expanded?: boolean;
} & ComponentProps<typeof Box>;

export const CollapsibleRegion = ({ _id, children, expanded, ...props }: CollapsibleRegionProps) => {
return (
<Box {...props} id={_id} maxHeight={expanded ? 'fit-content' : 0} style={{ transition: 'all 0.18s ease', overflowY: 'hidden' }} is='dd'>
<Box aria-labelledby={_id} role='region'>
{children}
</Box>
</Box>
);
};
Loading