feat: Security Logs Page (#35218)

pull/35718/head^2
Martin Schoeler 1 year ago committed by GitHub
parent 3e34228268
commit 1eeb139158
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      .changeset/lovely-waves-sniff.md
  2. 2
      apps/meteor/client/providers/SettingsProvider.tsx
  3. 12
      apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx
  4. 19
      apps/meteor/client/startup/audit.tsx
  5. 20
      apps/meteor/client/views/audit/SecurityLogsPage.tsx
  6. 21
      apps/meteor/client/views/audit/components/AppInfoField.spec.tsx
  7. 89
      apps/meteor/client/views/audit/components/AppInfoField.stories.tsx
  8. 40
      apps/meteor/client/views/audit/components/AppInfoField.tsx
  9. 8
      apps/meteor/client/views/audit/components/AuditModalField.tsx
  10. 8
      apps/meteor/client/views/audit/components/AuditModalLabel.tsx
  11. 13
      apps/meteor/client/views/audit/components/AuditModalText.tsx
  12. 21
      apps/meteor/client/views/audit/components/SecurityLogDisplayModal.spec.tsx
  13. 31
      apps/meteor/client/views/audit/components/SecurityLogDisplayModal.stories.tsx
  14. 85
      apps/meteor/client/views/audit/components/SecurityLogDisplayModal.tsx
  15. 219
      apps/meteor/client/views/audit/components/SecurityLogsTable.tsx
  16. 34
      apps/meteor/client/views/audit/components/SettingSelect.tsx
  17. 77
      apps/meteor/client/views/audit/components/__snapshots__/AppInfoField.spec.tsx.snap
  18. 412
      apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap
  19. 78
      apps/meteor/client/views/audit/hooks/useSettingSelectOptions.spec.ts
  20. 41
      apps/meteor/client/views/audit/hooks/useSettingSelectOptions.ts
  21. 1
      apps/meteor/client/views/audit/utils/getAppTypeTranslation.ts
  22. 2
      packages/core-typings/src/IServerEvent.ts
  23. 17
      packages/i18n/src/locales/en.i18n.json
  24. 7
      packages/i18n/src/locales/pt-BR.i18n.json
  25. 10
      packages/mock-providers/src/MockedAppRootBuilder.tsx
  26. 2
      packages/ui-contexts/src/SettingsContext.ts

@ -0,0 +1,9 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
"@rocket.chat/mock-providers": minor
"@rocket.chat/ui-client": minor
"@rocket.chat/ui-contexts": minor
---
Adds a new admin page to audit settings changes in a server

@ -60,6 +60,8 @@ const SettingsProvider = ({ children }: SettingsProviderProps) => {
sorter: 1,
i18nLabel: 1,
},
...('skip' in query && typeof query.skip === 'number' && { skip: query.skip }),
...('limit' in query && typeof query.limit === 'number' && { limit: query.limit }),
},
)
.fetch(),

@ -13,6 +13,7 @@ export const useAuditItems = (): GenericMenuItemProps[] => {
const auditHomeRoute = useRoute('audit-home');
const auditSettingsRoute = useRoute('audit-log');
const securityLogsRoute = useRoute('security-logs');
if (!hasAuditPermission && !hasAuditLogPermission) {
return [];
@ -31,5 +32,14 @@ export const useAuditItems = (): GenericMenuItemProps[] => {
onClick: () => auditSettingsRoute.push(),
};
return [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem].filter(Boolean) as GenericMenuItemProps[];
const securityLogItem: GenericMenuItemProps = {
id: 'securityLog',
icon: 'document-eye',
content: t('Security_logs'),
onClick: () => securityLogsRoute.push(),
};
return [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem, hasAuditLogPermission && securityLogItem].filter(
Boolean,
) as GenericMenuItemProps[];
};

@ -5,11 +5,13 @@ import { hasAllPermission } from '../../app/authorization/client';
import { appLayout } from '../lib/appLayout';
import { onToggledFeature } from '../lib/onToggledFeature';
import { router } from '../providers/RouterProvider';
import SettingsProvider from '../providers/SettingsProvider';
import NotAuthorizedPage from '../views/notAuthorized/NotAuthorizedPage';
import MainLayout from '../views/root/MainLayout';
const AuditPage = lazy(() => import('../views/audit/AuditPage'));
const AuditLogPage = lazy(() => import('../views/audit/AuditLogPage'));
const SecurityLogsPage = lazy(() => import('../views/audit/SecurityLogsPage'));
declare module '@rocket.chat/ui-contexts' {
interface IRouterPaths {
@ -21,6 +23,10 @@ declare module '@rocket.chat/ui-contexts' {
pathname: '/audit-log';
pattern: '/audit-log';
};
'security-logs': {
pathname: '/security-logs';
pattern: '/security-logs';
};
}
}
@ -57,6 +63,19 @@ onToggledFeature('auditing', {
</MainLayout>,
),
},
{
path: '/security-logs',
id: 'security-logs',
element: appLayout.wrap(
<MainLayout>
<SettingsProvider>
<PermissionGuard permission='can-audit'>
<SecurityLogsPage />
</PermissionGuard>
</SettingsProvider>
</MainLayout>,
),
},
]);
},
down: () => {

@ -0,0 +1,20 @@
import { type ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import SecurityLogsTable from './components/SecurityLogsTable';
import { Page, PageHeader, PageContent } from '../../components/Page';
const SecurityLogsPage = (): ReactElement => {
const { t } = useTranslation();
return (
<Page>
<PageHeader title={t('Security_logs')} />
<PageContent>
<SecurityLogsTable />
</PageContent>
</Page>
);
};
export default SecurityLogsPage;

@ -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 './AppInfoField.stories';
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const view = render(<Story />, { wrapper: mockAppRoot().build() });
expect(view.baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });
const results = await axe(container);
expect(results).toHaveNoViolations();
});

@ -0,0 +1,89 @@
import type { AppSubscriptionStatus } from '@rocket.chat/core-typings';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import type { Meta, StoryFn } from '@storybook/react';
import { AppInfoField } from './AppInfoField';
export default {
title: 'views/Audit/AppInfoField',
component: AppInfoField,
args: {
appId: 'app-id',
},
decorators: [
mockAppRoot()
.withEndpoint('GET', '/apps/:id', () => ({
app: {
name: 'App Name',
id: '',
iconFileData: '',
appRequestStats: {
appId: '',
totalSeen: 0,
totalUnseen: 0,
},
author: {
name: '',
homepage: '',
support: '',
},
description: '',
privacyPolicySummary: '',
detailedDescription: {
raw: '',
rendered: '',
},
detailedChangelog: {
raw: '',
rendered: '',
},
categories: [],
version: '',
price: 0,
purchaseType: 'buy' as const,
pricingPlans: [],
iconFileContent: '',
isSubscribed: false,
bundledIn: [],
marketplaceVersion: '',
// Recursive typem expect an App type here
latest: undefined as any,
subscriptionInfo: {
typeOf: '',
status: 'Active' as AppSubscriptionStatus,
statusFromBilling: false,
isSeatBased: false,
seats: 0,
maxSeats: 0,
license: {
license: '',
version: 0,
expireDate: '',
},
startDate: '',
periodEnd: '',
endDate: '',
externallyManaged: false,
isSubscribedViaBundle: false,
},
tosLink: '',
privacyLink: '',
modifiedAt: '',
permissions: [],
languages: [],
createdDate: '',
private: false,
documentationUrl: '',
migrated: false,
},
}))
.withTranslations('en', 'core', { App_name: 'App Name' })
.buildStoryDecorator(),
],
} satisfies Meta<typeof AppInfoField>;
export const Default: StoryFn<typeof AppInfoField> = (args) => <AppInfoField {...args} />;
export const NoAppInfo: StoryFn<typeof AppInfoField> = (args) => <AppInfoField {...args} />;
NoAppInfo.decorators = [mockAppRoot().withTranslations('en', 'core', { App_id: 'App Id' }).buildStoryDecorator()];

@ -0,0 +1,40 @@
import { Skeleton } from '@rocket.chat/fuselage';
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import AuditModalField from './AuditModalField';
import AuditModalLabel from './AuditModalLabel';
import AuditModalText from './AuditModalText';
type AppInfoFieldProps = {
appId: string;
};
// This is a separate component to encapsulate its logic and in the future expand it to a field that shows more info on the App
export const AppInfoField = ({ appId }: AppInfoFieldProps) => {
const t = useTranslation();
const getAppInfo = useEndpoint('GET', `/apps/:id`, { id: appId });
const { data, isLoading, isSuccess } = useQuery({
queryKey: ['getAppInfo', appId],
queryFn: async () => {
return getAppInfo();
},
});
return (
<>
<AuditModalField>
<AuditModalLabel>{t('Actor')}</AuditModalLabel>
<AuditModalText>{t('App')}</AuditModalText>
</AuditModalField>
<AuditModalField>
<AuditModalLabel>{isSuccess && data ? t('App_name') : t('App_id')}</AuditModalLabel>
{isLoading && <Skeleton />}
{isSuccess && data ? <AuditModalText>{data.app.name}</AuditModalText> : <AuditModalText>{appId}</AuditModalText>}
</AuditModalField>
</>
);
};

@ -0,0 +1,8 @@
import { Box } from '@rocket.chat/fuselage';
import type { ComponentPropsWithoutRef } from 'react';
type AuditModalFieldProps = ComponentPropsWithoutRef<typeof Box>;
const AuditModalField = (props: AuditModalFieldProps) => <Box mb={12} {...props} />;
export default AuditModalField;

@ -0,0 +1,8 @@
import { Box } from '@rocket.chat/fuselage';
import type { ComponentPropsWithoutRef } from 'react';
type AuditModalLabelProps = ComponentPropsWithoutRef<typeof Box>;
const AuditModalLabel = (props: AuditModalLabelProps) => <Box mbe={4} fontScale='p2m' color='titles-labels' {...props} />;
export default AuditModalLabel;

@ -0,0 +1,13 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import type { ComponentPropsWithoutRef } from 'react';
const wordBreak = css`
word-break: break-word;
`;
type AuditModalTextProps = ComponentPropsWithoutRef<typeof Box>;
const AuditModalText = (props: AuditModalTextProps) => <Box fontScale='p2' color='default' className={wordBreak} {...props} />;
export default AuditModalText;

@ -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 './SecurityLogDisplayModal.stories';
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const view = render(<Story />, { wrapper: mockAppRoot().build() });
expect(view.baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });
const results = await axe(container);
expect(results).toHaveNoViolations();
});

@ -0,0 +1,31 @@
import type { Meta, StoryFn } from '@storybook/react';
import SecurityLogDisplayModal from './SecurityLogDisplayModal';
export default {
title: 'views/Audit/SecurityLogDisplay',
component: SecurityLogDisplayModal,
args: {
timestamp: 'Thursday, 20-Mar-25 17:17:46',
actor: {
type: 'user',
_id: 'user-id',
username: 'username',
useragent: 'useragent',
ip: '127.0.0.1',
},
setting: 'Show_message_in_email_notification',
changedFrom: 'false',
changedTo: 'true',
},
} satisfies Meta<typeof SecurityLogDisplayModal>;
export const Default: StoryFn<typeof SecurityLogDisplayModal> = (args) => <SecurityLogDisplayModal {...args} />;
export const system: StoryFn<typeof SecurityLogDisplayModal> = (args) => (
<SecurityLogDisplayModal {...args} actor={{ type: 'system', reason: 'update' }} />
);
export const app: StoryFn<typeof SecurityLogDisplayModal> = (args) => (
<SecurityLogDisplayModal {...args} actor={{ type: 'app', _id: 'app-id' }} />
);

@ -0,0 +1,85 @@
import type { IAuditServerUserActor, IAuditServerSystemActor, IAuditServerAppActor } from '@rocket.chat/core-typings';
import { Box } from '@rocket.chat/fuselage';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import { format } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { AppInfoField } from './AppInfoField';
import AuditModalField from './AuditModalField';
import AuditModalLabel from './AuditModalLabel';
import AuditModalText from './AuditModalText';
import GenericModal from '../../../components/GenericModal';
type SecurityLogDisplayProps = {
timestamp: string;
actor: IAuditServerUserActor | IAuditServerSystemActor | IAuditServerAppActor;
setting: string;
changedFrom: string;
changedTo: string;
onCancel: () => void;
};
const SecurityLogDisplayModal = ({ timestamp, actor, setting, changedFrom, changedTo, onCancel }: SecurityLogDisplayProps) => {
const { t } = useTranslation();
return (
<GenericModal maxHeight={550} icon={null} onClose={onCancel} title={t('Setting_change')}>
<AuditModalField>
<AuditModalLabel>{t('Timestamp')}</AuditModalLabel>
<AuditModalText>{format(new Date(timestamp), 'MMMM d yyyy, h:mm:ss a')}</AuditModalText>
</AuditModalField>
{actor.type === 'user' && (
<AuditModalField>
<AuditModalLabel>{t('Actor')}</AuditModalLabel>
<Box display='flex' alignItems='center'>
{actor.type === 'user' && <UserAvatar size='x24' userId={actor._id} />}
<Box
mi={actor.type === 'user' ? 8 : 0}
fontScale='p2m'
display='flex'
flexDirection='column'
alignSelf='center'
withTruncatedText
>
{actor.username}
</Box>
</Box>
</AuditModalField>
)}
{actor.type === 'app' && <AppInfoField appId={actor._id} />}
{actor.type === 'system' && (
<>
<AuditModalField>
<AuditModalLabel>{t('Actor')}</AuditModalLabel>
<AuditModalText>{t('System')}</AuditModalText>
</AuditModalField>
<AuditModalField>
<AuditModalLabel>{t('Reason')}</AuditModalLabel>
<AuditModalText>{actor.reason}</AuditModalText>
</AuditModalField>
</>
)}
<AuditModalField>
<AuditModalLabel>{t('Setting')}</AuditModalLabel>
<AuditModalText>{t(setting)}</AuditModalText>
</AuditModalField>
<AuditModalField>
<AuditModalLabel>{t('Changed_from')}</AuditModalLabel>
<AuditModalText>{changedFrom}</AuditModalText>
</AuditModalField>
<AuditModalField>
<AuditModalLabel>{t('Changed_to')}</AuditModalLabel>
<AuditModalText>{changedTo}</AuditModalText>
</AuditModalField>
</GenericModal>
);
};
export default SecurityLogDisplayModal;

@ -0,0 +1,219 @@
import type { IAuditServerAppActor, IAuditServerSystemActor, IAuditServerUserActor } from '@rocket.chat/core-typings';
import { Box, Button, ButtonGroup, Field, FieldLabel, Margins, Pagination } from '@rocket.chat/fuselage';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import { useEndpoint, useSetModal } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import { format } from 'date-fns';
import { useState, type ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import SecurityLogDisplayModal from './SecurityLogDisplayModal';
import { SettingSelect } from './SettingSelect';
import DateRangePicker from './forms/DateRangePicker';
import GenericNoResults from '../../../components/GenericNoResults';
import {
GenericTable,
GenericTableBody,
GenericTableCell,
GenericTableHeader,
GenericTableHeaderCell,
GenericTableLoadingRow,
GenericTableRow,
} from '../../../components/GenericTable';
import { usePagination } from '../../../components/GenericTable/hooks/usePagination';
import type { DateRange } from '../utils/dateRange';
import { getTypeTranslation } from '../utils/getAppTypeTranslation';
const SecurityLogsTable = (): ReactElement => {
const { t } = useTranslation();
const [setting, setSetting] = useState('');
const setModal = useSetModal();
const [dateRange, setDateRange] = useState<DateRange>(() => ({
start: undefined,
end: undefined,
}));
const [query, setQuery] = useState({
start: new Date(0).toISOString(),
end: new Date().toISOString(),
settingId: '',
});
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
const handleClearFilters = () => {
setSetting('');
setDateRange({ start: undefined, end: undefined });
setQuery({
start: new Date(0).toISOString(),
end: new Date().toISOString(),
settingId: '',
});
onSetCurrent(0);
};
const handleApplyFilters = () => {
const { start, end } = dateRange;
setQuery({
start: start?.toISOString() ?? new Date(0).toISOString(),
end: end?.toISOString() ?? new Date().toISOString(),
settingId: setting,
});
onSetCurrent(0);
};
const handleItemClick = ({
actor,
timestamp,
setting,
changedFrom,
changedTo,
}: {
actor: IAuditServerUserActor | IAuditServerSystemActor | IAuditServerAppActor;
timestamp: string;
setting: unknown;
changedFrom: string;
changedTo: string;
}) => {
setModal(
<SecurityLogDisplayModal
timestamp={timestamp}
actor={actor}
setting={String(setting)}
changedFrom={changedFrom}
changedTo={changedTo}
onCancel={() => setModal(null)}
/>,
);
};
const getAudits = useEndpoint('GET', '/v1/audit.settings');
const { data, isLoading, isSuccess } = useQuery({
queryKey: ['audit.settings', query, itemsPerPage, current],
queryFn: async () => {
return getAudits({ ...query, ...(itemsPerPage && { count: itemsPerPage }), ...(current && { offset: current }) });
},
});
return (
<>
<Box mb={16} display='flex' flexWrap='wrap' alignItems='flex-end'>
<Field width='unset' flexShrink={1} flexGrow={1}>
<Margins inline={6}>
<FieldLabel>{t('Date')}</FieldLabel>
<DateRangePicker display='flex' flexGrow={1} value={dateRange} onChange={setDateRange} />
</Margins>
</Field>
<Field width={300}>
<Margins inline={6}>
<FieldLabel>{t('Setting')}</FieldLabel>
<SettingSelect value={setting} onChange={setSetting} />
</Margins>
</Field>
<ButtonGroup>
<Margins blockStart={12} inline={6}>
<Button secondary onClick={handleClearFilters}>
{t('Clear_filters')}
</Button>
<Button primary onClick={handleApplyFilters}>
{t('Apply_filters')}
</Button>
</Margins>
</ButtonGroup>
</Box>
{isLoading && (
<GenericTable>
<GenericTableHeader>
<GenericTableHeaderCell>{t('Actor')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Timestamp')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Setting')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Changed_from')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Changed_to')}</GenericTableHeaderCell>
</GenericTableHeader>
<GenericTableBody>
<GenericTableLoadingRow cols={5} />
</GenericTableBody>
</GenericTable>
)}
{isSuccess && data.total === 0 && (
<GenericNoResults
title={t('No_results_found')}
description={t('Try_different_filters')}
buttonTitle={t('Clear_filters')}
buttonAction={handleClearFilters}
/>
)}
{isSuccess && data.total > 0 && (
<GenericTable striped>
<GenericTableHeader>
<GenericTableHeaderCell>{t('Actor')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Timestamp')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Setting')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Changed_from')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Changed_to')}</GenericTableHeaderCell>
</GenericTableHeader>
<GenericTableBody>
{data.events.map((item) => {
const setting = item.data.find((item) => item.key === 'id')?.value;
const previous = item.data.find((item) => item.key === 'previous')?.value || t('Empty');
const current = item.data.find((item) => item.key === 'current')?.value || t('Empty');
return (
<GenericTableRow
key={item._id}
role='link'
action
tabIndex={0}
height={44}
onClick={() =>
handleItemClick({
actor: item.actor,
timestamp: new Date(item.ts).toDateString(),
setting,
changedFrom: String(previous),
changedTo: String(current),
})
}
>
<GenericTableCell withTruncatedText>
<Box display='flex' alignItems='center'>
{item.actor.type === 'user' && (
<Box mie={4}>
<UserAvatar size='x24' userId={item.actor._id} />
</Box>
)}
<Box fontScale='p2m' withTruncatedText color='default'>
{item.actor.type === 'user' ? item.actor.username : t(getTypeTranslation(item.actor.type))}
</Box>
</Box>
</GenericTableCell>
<GenericTableCell withTruncatedText>{format(new Date(item.ts), 'MMMM d yyyy, h:mm:ss a')}</GenericTableCell>
<GenericTableCell withTruncatedText title={setting && String(setting)}>
{setting && String(setting)}
</GenericTableCell>
<GenericTableCell withTruncatedText>{String(previous)}</GenericTableCell>
<GenericTableCell withTruncatedText>{String(current)}</GenericTableCell>
</GenericTableRow>
);
})}
</GenericTableBody>
</GenericTable>
)}
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
onSetItemsPerPage={onSetItemsPerPage}
onSetCurrent={onSetCurrent}
{...paginationProps}
/>
</>
);
};
export default SecurityLogsTable;

@ -0,0 +1,34 @@
import { Option, PaginatedSelectFiltered } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSettingSelectOptions } from '../hooks/useSettingSelectOptions';
export const SettingSelect = ({ value, onChange }: { value: string; onChange: (value: string) => void }) => {
const { t } = useTranslation();
const [filter, setFilter] = useState<string>('');
const debouncedFilter = useDebouncedValue(filter, 500);
const { data, fetchNextPage, isFetchingNextPage } = useSettingSelectOptions(debouncedFilter);
const flattenedData = data?.pages.flatMap((page) => page) || [];
return (
<PaginatedSelectFiltered
flexShrink={1}
value={value}
onChange={(val) => onChange(val)}
placeholder={t('All_Settings')}
filter={filter}
setFilter={setFilter as (value: string | number | undefined) => void}
options={flattenedData}
endReached={() => !isFetchingNextPage && fetchNextPage({ cancelRefetch: true })}
renderItem={({ label, ...props }) => (
<Option {...props} title={t(label)}>
{label}
</Option>
)}
/>
);
};

@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders Default without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Actor
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
App
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
App_id
</div>
<span
class="rcx-skeleton rcx-skeleton--text"
/>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
app-id
</div>
</div>
</div>
</body>
`;
exports[`renders NoAppInfo without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Actor
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
App
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
App Id
</div>
<span
class="rcx-skeleton rcx-skeleton--text"
/>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
app-id
</div>
</div>
</div>
</body>
`;

@ -0,0 +1,412 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders Default without crashing 1`] = `
<body>
<div>
<dialog
aria-labelledby=":r0:-title"
aria-modal="true"
class="rcx-box rcx-box--full rcx-modal rcx-css-9ju0e2"
open=""
>
<div
class="rcx-box rcx-box--full rcx-modal__inner rcx-css-1e2ego0"
>
<header
class="rcx-box rcx-box--full rcx-modal__header"
>
<div
class="rcx-box rcx-box--full rcx-modal__header-inner"
>
<div
class="rcx-box rcx-box--full rcx-modal__header-text rcx-css-trljwa rcx-css-lma364"
>
<h2
class="rcx-box rcx-box--full rcx-modal__title"
id=":r0:-title"
>
Setting_change
</h2>
</div>
</div>
</header>
<div
class="rcx-box rcx-box--full rcx-modal__content rcx-css-1vw7itl"
>
<div
class="rcx-box rcx-box--full rcx-modal__content-wrapper rcx-css-r1bpeb"
>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Timestamp
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
March 20 2025, 5:17:46 PM
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Actor
</div>
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x24"
>
<img
alt=""
aria-hidden="true"
class="rcx-avatar__element rcx-avatar__element--x24"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg=="
/>
</figure>
<div
class="rcx-box rcx-box--full rcx-css-2f8n8y"
>
username
</div>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Setting
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
Show_message_in_email_notification
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Changed_from
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
false
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Changed_to
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
true
</div>
</div>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-modal__footer rcx-css-17mu816"
>
<div
class="rcx-button-group rcx-button-group--align-end"
role="group"
/>
</div>
</div>
</dialog>
</div>
</body>
`;
exports[`renders app without crashing 1`] = `
<body>
<div>
<dialog
aria-labelledby=":r1:-title"
aria-modal="true"
class="rcx-box rcx-box--full rcx-modal rcx-css-9ju0e2"
open=""
>
<div
class="rcx-box rcx-box--full rcx-modal__inner rcx-css-1e2ego0"
>
<header
class="rcx-box rcx-box--full rcx-modal__header"
>
<div
class="rcx-box rcx-box--full rcx-modal__header-inner"
>
<div
class="rcx-box rcx-box--full rcx-modal__header-text rcx-css-trljwa rcx-css-lma364"
>
<h2
class="rcx-box rcx-box--full rcx-modal__title"
id=":r1:-title"
>
Setting_change
</h2>
</div>
</div>
</header>
<div
class="rcx-box rcx-box--full rcx-modal__content rcx-css-1vw7itl"
>
<div
class="rcx-box rcx-box--full rcx-modal__content-wrapper rcx-css-r1bpeb"
>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Timestamp
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
March 20 2025, 5:17:46 PM
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Actor
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
App
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
App_id
</div>
<span
class="rcx-skeleton rcx-skeleton--text"
/>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
app-id
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Setting
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
Show_message_in_email_notification
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Changed_from
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
false
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Changed_to
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
true
</div>
</div>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-modal__footer rcx-css-17mu816"
>
<div
class="rcx-button-group rcx-button-group--align-end"
role="group"
/>
</div>
</div>
</dialog>
</div>
</body>
`;
exports[`renders system without crashing 1`] = `
<body>
<div>
<dialog
aria-labelledby=":r2:-title"
aria-modal="true"
class="rcx-box rcx-box--full rcx-modal rcx-css-9ju0e2"
open=""
>
<div
class="rcx-box rcx-box--full rcx-modal__inner rcx-css-1e2ego0"
>
<header
class="rcx-box rcx-box--full rcx-modal__header"
>
<div
class="rcx-box rcx-box--full rcx-modal__header-inner"
>
<div
class="rcx-box rcx-box--full rcx-modal__header-text rcx-css-trljwa rcx-css-lma364"
>
<h2
class="rcx-box rcx-box--full rcx-modal__title"
id=":r2:-title"
>
Setting_change
</h2>
</div>
</div>
</header>
<div
class="rcx-box rcx-box--full rcx-modal__content rcx-css-1vw7itl"
>
<div
class="rcx-box rcx-box--full rcx-modal__content-wrapper rcx-css-r1bpeb"
>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Timestamp
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
March 20 2025, 5:17:46 PM
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Actor
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
System
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Reason
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
update
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Setting
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
Show_message_in_email_notification
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Changed_from
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
false
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-op9h3g"
>
<div
class="rcx-box rcx-box--full rcx-css-pwio3x"
>
Changed_to
</div>
<div
class="rcx-box rcx-box--full rcx-css-6hwe8l rcx-css-1cuayu0"
>
true
</div>
</div>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-modal__footer rcx-css-17mu816"
>
<div
class="rcx-button-group rcx-button-group--align-end"
role="group"
/>
</div>
</div>
</dialog>
</div>
</body>
`;

@ -0,0 +1,78 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook, waitFor } from '@testing-library/react';
import { useSettingSelectOptions } from './useSettingSelectOptions';
// TODO: check if the return of items matches the settings we mocked
describe('useSettingSelectOptions', () => {
it('should return the ordered list of options', async () => {
const { result } = renderHook(() => useSettingSelectOptions(), {
wrapper: mockAppRoot()
.withSetting('test1', true)
.withSetting('test2', true)
.withSetting('test3', true)
.withSetting('test4', true)
.withSetting('test5', true)
.withSetting('test6', true)
.withSetting('test7', true)
.withSetting('test8', true)
.withSetting('test9', true)
.withSetting('test10', true)
.withSetting('test11', true)
.withSetting('test12', true)
.withSetting('test13', true)
.withSetting('test14', true)
.withSetting('test15', true)
.build(),
});
await waitFor(() => expect(result.current?.data?.pages[0][0]).toEqual({ _id: 'test1', label: 'test1', value: 'test1' }));
await waitFor(() => expect(result.current?.data?.pages[0][1]).toEqual({ _id: 'test2', label: 'test2', value: 'test2' }));
await waitFor(() => expect(result.current?.data?.pages[0][2]).toEqual({ _id: 'test3', label: 'test3', value: 'test3' }));
await waitFor(() => expect(result.current?.data?.pages[0][3]).toEqual({ _id: 'test4', label: 'test4', value: 'test4' }));
await waitFor(() => expect(result.current?.data?.pages[0][4]).toEqual({ _id: 'test5', label: 'test5', value: 'test5' }));
await waitFor(() => expect(result.current?.data?.pages[0][5]).toEqual({ _id: 'test6', label: 'test6', value: 'test6' }));
await waitFor(() => expect(result.current?.data?.pages[0][6]).toEqual({ _id: 'test7', label: 'test7', value: 'test7' }));
await waitFor(() => expect(result.current?.data?.pages[0][7]).toEqual({ _id: 'test8', label: 'test8', value: 'test8' }));
await waitFor(() => expect(result.current?.data?.pages[0][8]).toEqual({ _id: 'test9', label: 'test9', value: 'test9' }));
await waitFor(() => expect(result.current?.data?.pages[0][9]).toEqual({ _id: 'test10', label: 'test10', value: 'test10' }));
await waitFor(() => expect(result.current?.data?.pages[0][10]).toEqual({ _id: 'test11', label: 'test11', value: 'test11' }));
await waitFor(() => expect(result.current?.data?.pages[0][11]).toEqual({ _id: 'test12', label: 'test12', value: 'test12' }));
await waitFor(() => expect(result.current?.data?.pages[0][12]).toEqual({ _id: 'test13', label: 'test13', value: 'test13' }));
await waitFor(() => expect(result.current?.data?.pages[0][13]).toEqual({ _id: 'test14', label: 'test14', value: 'test14' }));
await waitFor(() => expect(result.current?.data?.pages[0][14]).toEqual({ _id: 'test15', label: 'test15', value: 'test15' }));
await waitFor(() => expect(result.current?.data?.pages[0]).toHaveLength(15));
});
it('should return the list of filtered options', async () => {
const { result } = renderHook(() => useSettingSelectOptions('TeSt1'), {
wrapper: mockAppRoot()
.withSetting('test1', true)
.withSetting('test2', true)
.withSetting('test3', true)
.withSetting('test4', true)
.withSetting('test5', true)
.withSetting('test6', true)
.withSetting('test7', true)
.withSetting('test8', true)
.withSetting('test9', true)
.withSetting('test10', true)
.withSetting('test11', true)
.withSetting('test12', true)
.withSetting('test13', true)
.withSetting('test14', true)
.withSetting('test15', true)
.build(),
});
await waitFor(() => expect(result.current?.data?.pages[0][0]).toEqual({ _id: 'test1', label: 'test1', value: 'test1' }));
await waitFor(() => expect(result.current?.data?.pages[0][1]).toEqual({ _id: 'test10', label: 'test10', value: 'test10' }));
await waitFor(() => expect(result.current?.data?.pages[0][2]).toEqual({ _id: 'test11', label: 'test11', value: 'test11' }));
await waitFor(() => expect(result.current?.data?.pages[0][3]).toEqual({ _id: 'test12', label: 'test12', value: 'test12' }));
await waitFor(() => expect(result.current?.data?.pages[0][4]).toEqual({ _id: 'test13', label: 'test13', value: 'test13' }));
await waitFor(() => expect(result.current?.data?.pages[0][5]).toEqual({ _id: 'test14', label: 'test14', value: 'test14' }));
await waitFor(() => expect(result.current?.data?.pages[0][6]).toEqual({ _id: 'test15', label: 'test15', value: 'test15' }));
await waitFor(() => expect(result.current?.data?.pages[0]).toHaveLength(7));
});
});

@ -0,0 +1,41 @@
import { useSettings } from '@rocket.chat/ui-contexts';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useCallback } from 'react';
type SettingSelectOption = {
label: string;
value: string;
_id: string;
};
export const useSettingSelectOptions = (filter = '') => {
const settings = useSettings();
const fetchData = useCallback(
async (start = 0): Promise<SettingSelectOption[]> => {
return settings
.map(({ _id }) => ({ label: _id, value: _id, _id }))
.filter(({ label }) => label.toUpperCase().includes(filter.toUpperCase()))
.slice(start, start + 50);
},
[filter, settings],
);
return useInfiniteQuery({
queryKey: ['settings', filter],
queryFn: ({ pageParam }) => fetchData(pageParam),
getNextPageParam: (lastPage, _allPages, lastPageParam) => {
if (lastPage.length === 0) {
return undefined;
}
return lastPageParam + 1;
},
getPreviousPageParam: (_firstPage, _allPages, firstPageParam) => {
if (firstPageParam <= 1) {
return undefined;
}
return firstPageParam - 1;
},
initialPageParam: 0,
});
};

@ -0,0 +1 @@
export const getTypeTranslation = (type: 'app' | 'system') => (type === 'app' ? 'App' : 'System');

@ -42,9 +42,9 @@ export interface IAuditServerAppActor {
export type IAuditServerActor = IAuditServerUserActor | IAuditServerSystemActor | IAuditServerAppActor;
interface IAuditServerEvent {
_id: string;
t: string;
ts: Date;
actor: IAuditServerActor;
}

@ -553,6 +553,7 @@
"Application_updated": "Application updated",
"Apply": "Apply",
"Apply_and_refresh_all_clients": "Apply and refresh all clients",
"Apply_filters": "Apply filters",
"Apps": "Apps",
"Apps_Engine_Version": "Apps Engine Version",
"API_Enable_Rate_Limiter": "Enable Rate Limiter",
@ -1095,6 +1096,8 @@
"color": "Color",
"changed_room_announcement_to__room_announcement_": "changed room announcement to: {{room_announcement}}",
"changed_room_description_to__room_description_": "changed room description to: {{room_description}}",
"Changed_from": "Changed from",
"Changed_to": "Changed to",
"Color": "Color",
"Colors": "Colors",
"change-livechat-room-visitor": "Change Livechat Room Visitors",
@ -4147,6 +4150,7 @@
"Microphone": "Microphone",
"Microphone_access_not_allowed": "Microphone access was not allowed, please check your browser settings.",
"RealName_Change_Disabled": "Your Rocket.Chat administrator has disabled the changing of names",
"Reason": "Reason",
"Reason_To_Join": "Reason to Join",
"Mic_off": "Mic Off",
"Receive_alerts": "Receive alerts",
@ -4675,7 +4679,9 @@
"Pages": "Pages",
"set-readonly_description": "Permission to set a channel to read only channel",
"Settings": "Settings",
"Setting": "Setting",
"Parent_channel_or_team": "Parent channel or team",
"Setting_change": "Setting change",
"Settings_updated": "Settings updated",
"Participants": "Participants",
"Setup_Wizard": "Setup Wizard",
@ -4913,6 +4919,7 @@
"Queue_delay_timeout": "Queue processing delay timeout",
"Queue_Time": "Queue Time",
"System_messages": "System Messages",
"System": "System",
"Queue_management": "Queue Management",
"Tag": "Tag",
"Quick_reactions": "Quick reactions",
@ -5579,6 +5586,7 @@
"Show_mentions": "Show badge for mentions",
"Accept_receive_inquiry_no_online_agents": "Allow department to receive forwarded inquiries even when there's no available agents",
"Accept_receive_inquiry_no_online_agents_Hint": "This method is effective only with automatic assignment routing methods, and does not apply to Manual Selection.",
"Actor": "Actor",
"view-livechat-manager": "View Omnichannel Manager",
"Show_Only_This_Content": "Show only this content",
"view-livechat-manager_description": "Permission to view other Omnichannel managers",
@ -5919,6 +5927,7 @@
"Timeout_in_miliseconds": "Timeout (in miliseconds)",
"Timeout_in_miliseconds_cant_be_negative_number": "Timeout (in miliseconds) can't a negative number",
"Timeout_in_miliseconds_hint": "The time in milliseconds to wait for an external service to respond before canceling the request.",
"Timestamp": "Timestamp",
"Timezone": "Timezone",
"To_prevent_seeing_this_message_again_allow_popups_from_workspace_URL": "To prevent seeing this message again, make sure your browser settings allow pop-ups to be opened from the workspace URL: ",
"toggle-room-e2e-encryption": "Toggle Room E2E Encryption",
@ -5963,6 +5972,7 @@
"Troubleshoot_Force_Caching_Version": "Force browsers to clear networking cache based on version change",
"Troubleshoot_Force_Caching_Version_Alert": "If the value provided is not empty and different from previous one the browsers will try to clear the cache. This setting should not be set for a long period since it affects the browser performance, please clear it as soon as possible.",
"Try_now": "Try now",
"Try_different_filters": "Try different filters",
"Try_searching_in_the_marketplace_instead": "Try searching in the Marketplace instead",
"Turn_on_video": "Turn on video",
"Turn_on_answer_chats": "Turn on answer chats",
@ -6075,6 +6085,9 @@
"Value_messages": "{{value}} messages",
"Value_users": "{{value}} users",
"Version_version": "Version {{version}}",
"App": "App",
"App_id": "App Id",
"App_name": "App name",
"App_Request_Admin_Message": "Hi {{admin_name}}, {{user_name}} submitted a request to install {{app_name}} app on this workspace. \n \n This is the message they included: \n>{{message}} \n \n To learn more and install the {{app_name}} app, [click here]({{learn_more}}).",
"App_version_incompatible_tooltip": "App incompatible with Rocket.Chat version",
"App_request_enduser_message": "The app you requested, {{appName}}, has just been installed on this workspace. \n [Click here]({{learnmore}}) to learn about the app.",
@ -6571,6 +6584,7 @@
"Disconnect_workspace": "Disconnect workspace",
"Awaiting_confirmation": "Awaiting confirmation",
"Security_code": "Security code",
"Security_logs": "Security logs",
"Registration_Token": "Registration Token",
"RegisterWorkspace_Button": "Register workspace",
"ConnectWorkspace_Button": "Connect workspace",
@ -6621,6 +6635,7 @@
"App_will_lose_grandfathered_status": "**This app will lose its app limit policy exemption.** \n \nWorkspaces on Community can have up to {{limit}} apps enabled. Uninstalling this app will cause it to lose its exemption policy.",
"App_will_lose_grandfathered_status_private": "**This app will lose its app limit policy exemption.** \n \nBecause Community workspaces cannot enable private apps, this workspace will require a premium plan in order to enable this app again in future.",
"All_rooms": "All rooms",
"All_Settings": "All Settings",
"All_visible": "All visible",
"all": "all",
"Filter_by_room": "Filter by room type",
@ -6767,6 +6782,8 @@
"Advanced_settings": "Advanced settings",
"Security_and_permissions": "Security and permissions",
"Security_and_privacy": "Security and privacy",
"Security_Log_App": "App ( {{appId}} )",
"Security_Log_System": "System ( {{reason}} )",
"Sidepanel_navigation": "Secondary navigation for teams",
"Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.",
"Show_channels_description": "Show team channels in second sidebar",

@ -72,6 +72,7 @@
"A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Um novo proprietário será atribuído automaticamente a estas <span style=\"font-weight: bold;\">{{count}}</span> salas:<br/> {{rooms}}.",
"A_secure_and_highly_private_self-managed_solution_for_conference_calls": "Uma solução autogerenciada segura e altamente privada para chamadas em conferência.",
"A_workspace_admin_needs_to_install_and_configure_a_conference_call_app": "Um administrador do workspace precisa instalar e configurar um aplicativo de chamada de vídeo.",
"Actor": "Autor",
"Accept": "Aceitar",
"Accept_Call": "Aceitar chamada",
"Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aceitar solicitações de omnichannel de entrada mesmo que não tenham agentes online",
@ -400,6 +401,7 @@
"Answer_call": "Receber chamada",
"Apiai_Key": "Api.ai Key",
"Apiai_Language": "Idioma Api.ai",
"App": "Aplicativo",
"App_Details": "Detalhes do aplicativo",
"App_Info": "Informação do aplicativo",
"App_Information": "Informações do aplicativo",
@ -758,6 +760,8 @@
"Categories*": "Categorias*",
"Certificates_and_Keys": "Certificados e chaves",
"Change_Room_Type": "Mudando o Tipo de Sala",
"Changed_from": "Mudou de",
"Changed_to": "Mudou para",
"Changing_email": "Alterando e-mail",
"Channel": "Canal",
"Channel_Archived": "Canal com o nome `#%s` foi arquivado com sucesso",
@ -3528,7 +3532,9 @@
"Set_as_owner": "Definir como proprietário",
"Set_random_password_and_send_by_email": "Definir senha aleatória e enviar por e-mail",
"Settings": "Configurações",
"Setting": "Configuração",
"Settings_updated": "Configurações atualizadas",
"Setting_change": "Configuração alterada",
"Setup_Wizard": "Assistente de configuração",
"Setup_Wizard_Description": "Informações básicas do seu workspace como organização, nome e país.",
"Setup_Wizard_Info": "Vamos apoiar na configuração do seu primeiro usuário administrador, na configuração da sua organização e no registro do servidor, para que possa receber notificações push gratuitas e muito mais.",
@ -3699,6 +3705,7 @@
"Sync_in_progress": "Sincronização em andamento",
"Sync_success": "Sincronizado com sucesso",
"System_messages": "Mensagens do sistema",
"System": "Sistema",
"TOTP Invalid [totp-invalid]": "Código ou senha invalida",
"TOTP_Reset_Other_Key_Warning": "Redefinir o TOTP de dois fatores atual vai desconectar o usuário. O usuário poderá definir os dois fatores mais tarde novamente.",
"TOTP_reset_email": "Notificação de redefinição TOTP de dois fatores",

@ -13,7 +13,7 @@ import { Emitter } from '@rocket.chat/emitter';
import languages from '@rocket.chat/i18n/dist/languages';
import { createFilterFromQuery } from '@rocket.chat/mongo-adapter';
import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
import type { Device, ModalContextValue, SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts';
import type { Device, ModalContextValue, SettingsContextQuery, SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts';
import {
AuthorizationContext,
ConnectionStatusContext,
@ -42,13 +42,6 @@ type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
export type SettingsContextQuery = {
readonly _id?: ISetting['_id'][] | RegExp;
readonly group?: ISetting['_id'];
readonly section?: string;
readonly tab?: ISetting['_id'];
};
// eslint-disable-next-line @typescript-eslint/naming-convention
interface MockedAppRootEvents {
'update-modal': void;
@ -417,7 +410,6 @@ export class MockedAppRootBuilder {
const filter =
cache.get(query) ??
createFilterFromQuery({
...query,
...(query._id ? { _id: { $in: query._id } } : {}),
} as any);
cache.set(query, filter);

@ -6,6 +6,8 @@ export type SettingsContextQuery = {
readonly group?: ISetting['_id'];
readonly section?: string;
readonly tab?: ISetting['_id'];
readonly skip?: number;
readonly limit?: number;
};
export type SettingsContextValue = {

Loading…
Cancel
Save