Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { useEditableSetting } from '../../EditableSettingsContext';
import MemoizedSetting from '../../settings/Setting/MemoizedSetting';
import SettingSkeleton from '../../settings/Setting/SettingSkeleton';

type SettingToggleProps = {
type ABACEnabledToggleProps = {
hasABAC: 'loading' | boolean;
};

const SettingToggle = ({ hasABAC }: SettingToggleProps) => {
const ABACEnabledToggle = ({ hasABAC }: ABACEnabledToggleProps) => {
const setting = useEditableSetting('ABAC_Enabled');
const setModal = useSetModal();
const dispatch = useSettingsDispatch();
Expand Down Expand Up @@ -86,10 +86,10 @@ const SettingToggle = ({ hasABAC }: SettingToggleProps) => {
packageValue={setting.packageValue === true}
hint={t(setting.i18nDescription || '')}
disabled={!hasABAC || setting.blocked}
hasResetButton={setting.packageValue !== setting.value}
hasResetButton={hasABAC && setting.packageValue !== setting.value}
onChangeValue={(value: SettingValue) => onChange(value === true)}
onResetButtonClick={() => onReset()}
/>
);
};
export default SettingToggle;
export default ABACEnabledToggle;
148 changes: 148 additions & 0 deletions apps/meteor/client/views/admin/ABAC/ABACSettingTab/SettingField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import type { ISettingColor, SettingEditor, SettingValue } from '@rocket.chat/core-typings';
import { isSettingColor, isSetting } from '@rocket.chat/core-typings';
import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import { useSettingStructure } from '@rocket.chat/ui-contexts';
import DOMPurify from 'dompurify';
import type { ReactElement } from 'react';
import { useEffect, useMemo, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

import MarkdownText from '../../../../components/MarkdownText';
import { useEditableSetting, useEditableSettingsDispatch, useEditableSettingVisibilityQuery } from '../../EditableSettingsContext';
import MemoizedSetting from '../../settings/Setting/MemoizedSetting';
import { useHasSettingModule } from '../../settings/hooks/useHasSettingModule';

type SettingFieldProps = {
className?: string;
settingId: string;
sectionChanged?: boolean;
};

function SettingField({ className = undefined, settingId, sectionChanged }: SettingFieldProps): ReactElement {
const setting = useEditableSetting(settingId);
const persistedSetting = useSettingStructure(settingId);
const hasSettingModule = useHasSettingModule(setting);

if (!setting || !persistedSetting) {
throw new Error(`Setting ${settingId} not found`);
}

// Checks if setting has at least required fields before doing anything
if (!isSetting(setting)) {
throw new Error(`Setting ${settingId} is not valid`);
}

const dispatch = useEditableSettingsDispatch();

const update = useDebouncedCallback(
({ value, editor }: { value?: SettingValue; editor?: SettingEditor }) => {
if (!persistedSetting) {
return;
}

dispatch([
{
_id: persistedSetting._id,
...(value !== undefined && { value }),
...(editor !== undefined && { editor }),
changed:
JSON.stringify(persistedSetting.value) !== JSON.stringify(value) ||
(isSettingColor(persistedSetting) && JSON.stringify(persistedSetting.editor) !== JSON.stringify(editor)),
},
]);
},
230,
[persistedSetting, dispatch],
);

const { t, i18n } = useTranslation();

const [value, setValue] = useState(setting.value);
const [editor, setEditor] = useState(isSettingColor(setting) ? setting.editor : undefined);

useEffect(() => {
setValue(setting.value);
}, [setting.value]);

useEffect(() => {
setEditor(isSettingColor(setting) ? setting.editor : undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [(setting as ISettingColor).editor]);

const onChangeValue = useCallback(
(value: SettingValue) => {
setValue(value);
update({ value });
},
[update],
);

const onChangeEditor = useCallback(
(editor: SettingEditor) => {
setEditor(editor);
update({ editor });
},
[update],
);

const onResetButtonClick = useCallback(() => {
setValue(setting.value);
setEditor(isSettingColor(setting) ? setting.editor : undefined);
update({
value: persistedSetting.packageValue,
...(isSettingColor(persistedSetting) && { editor: persistedSetting.packageEditor }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setting.value, (setting as ISettingColor).editor, update, persistedSetting]);

const { _id, readonly, type, packageValue, i18nLabel, i18nDescription, alert } = setting;

const disabled = !useEditableSettingVisibilityQuery(persistedSetting.enableQuery);
const invisible = !useEditableSettingVisibilityQuery(persistedSetting.displayQuery);

const labelText = (i18n.exists(i18nLabel) && t(i18nLabel)) || (i18n.exists(_id) && t(_id)) || i18nLabel || _id;

const hint = useMemo(
() => (i18nDescription && i18n.exists(i18nDescription) ? <MarkdownText variant='inline' content={t(i18nDescription)} /> : undefined),
[i18n, i18nDescription, t],
);

const callout = useMemo(
() =>
alert && <span dangerouslySetInnerHTML={{ __html: i18n.exists(alert) ? DOMPurify.sanitize(t(alert)) : DOMPurify.sanitize(alert) }} />,
[alert, i18n, t],
);

const shouldDisableEnterprise = setting.enterprise && !hasSettingModule;

const hasResetButton =
!shouldDisableEnterprise &&
!readonly &&
type !== 'asset' &&
((isSettingColor(setting) && JSON.stringify(setting.packageEditor) !== JSON.stringify(editor)) ||
JSON.stringify(value) !== JSON.stringify(packageValue)) &&
!disabled;

// @todo: type check props based on setting type

return (
<MemoizedSetting
className={className}
label={labelText}
hint={hint}
callout={callout}
sectionChanged={sectionChanged}
{...setting}
disabled={disabled || shouldDisableEnterprise}
value={value}
editor={editor}
hasResetButton={hasResetButton}
onChangeValue={onChangeValue}
onChangeEditor={onChangeEditor}
onResetButtonClick={onResetButtonClick}
invisible={invisible}
/>
);
}

export default SettingField;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';

import SettingToggle from './SettingToggle';
import AbacEnabledToggle from './AbacEnabledToggle';
import EditableSettingsProvider from '../../settings/EditableSettingsProvider';

const settingStructure = {
Expand All @@ -26,17 +26,17 @@ const baseAppRoot = mockAppRoot()
Cancel: 'Cancel',
});

describe('SettingToggle', () => {
describe('AbacEnabledToggle', () => {
it('should render the setting toggle when setting exists', () => {
const { baseElement } = render(<SettingToggle hasABAC={true} />, {
const { baseElement } = render(<AbacEnabledToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});
expect(baseElement).toMatchSnapshot();
});

it('should show warning modal when disabling ABAC', async () => {
const user = userEvent.setup();
render(<SettingToggle hasABAC={true} />, {
render(<AbacEnabledToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});

Expand All @@ -57,7 +57,7 @@ describe('SettingToggle', () => {

it('should not show warning modal when enabling ABAC', async () => {
const user = userEvent.setup();
render(<SettingToggle hasABAC={true} />, {
render(<AbacEnabledToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(),
});

Expand All @@ -70,7 +70,7 @@ describe('SettingToggle', () => {

it('should show warning modal when resetting setting', async () => {
const user = userEvent.setup();
render(<SettingToggle hasABAC={true} />, {
render(<AbacEnabledToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});

Expand All @@ -87,7 +87,7 @@ describe('SettingToggle', () => {
});

it('should have no accessibility violations', async () => {
const { container } = render(<SettingToggle hasABAC={true} />, {
const { container } = render(<AbacEnabledToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});
const results = await axe(container);
Expand All @@ -96,7 +96,7 @@ describe('SettingToggle', () => {

it('should handle setting change correctly', async () => {
const user = userEvent.setup();
render(<SettingToggle hasABAC={true} />, {
render(<AbacEnabledToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(),
});

Expand All @@ -108,7 +108,7 @@ describe('SettingToggle', () => {
});

it('should be disabled when abac license is not installed', () => {
const { baseElement } = render(<SettingToggle hasABAC={false} />, {
const { baseElement } = render(<AbacEnabledToggle hasABAC={false} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});

Expand All @@ -118,22 +118,22 @@ describe('SettingToggle', () => {
});

it('should show skeleton when loading', () => {
const { baseElement } = render(<SettingToggle hasABAC='loading' />, {
const { baseElement } = render(<AbacEnabledToggle hasABAC='loading' />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});
expect(baseElement).toMatchSnapshot();
});

it('should show reset button when value differs from package value', () => {
render(<SettingToggle hasABAC={true} />, {
render(<AbacEnabledToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
});

expect(screen.getByRole('button', { name: /reset/i })).toBeInTheDocument();
});

it('should not show reset button when value matches package value', () => {
render(<SettingToggle hasABAC={true} />, {
render(<AbacEnabledToggle hasABAC={true} />, {
wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(),
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import type { Meta, StoryObj } from '@storybook/react';

import SettingToggle from './SettingToggle';
import AbacEnabledToggle from './AbacEnabledToggle';
import EditableSettingsProvider from '../../settings/EditableSettingsProvider';

const meta: Meta<typeof SettingToggle> = {
title: 'Admin/ABAC/SettingToggle',
component: SettingToggle,
const meta: Meta<typeof AbacEnabledToggle> = {
component: AbacEnabledToggle,
parameters: {
layout: 'padded',
},
Expand Down Expand Up @@ -44,7 +43,7 @@ const meta: Meta<typeof SettingToggle> = {
};

export default meta;
type Story = StoryObj<typeof SettingToggle>;
type Story = StoryObj<typeof AbacEnabledToggle>;

export const Default: Story = {
args: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Box, Callout, Margins } from '@rocket.chat/fuselage';
import { Trans } from 'react-i18next';

import SettingToggle from './SettingToggle';
import AbacEnabledToggle from './AbacEnabledToggle';
import SettingField from './SettingField';
import { useHasLicenseModule } from '../../../../hooks/useHasLicenseModule';
import { links } from '../../../../lib/links';

Expand All @@ -11,7 +12,9 @@ const SettingsPage = () => {
<Box maxWidth='x600' w='full' alignSelf='center'>
<Box>
<Margins block={24}>
<SettingToggle hasABAC={hasABAC} />
<AbacEnabledToggle hasABAC={hasABAC} />
<SettingField settingId='ABAC_ShowAttributesInRooms' />
<SettingField settingId='Abac_Cache_Decision_Time_Seconds' />

<Callout>
<Trans i18nKey='ABAC_Enabled_callout'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`SettingToggle should be disabled when abac license is not installed 1`] = `
exports[`AbacEnabledToggle should be disabled when abac license is not installed 1`] = `
<body>
<div>
<div
Expand All @@ -25,19 +25,6 @@ exports[`SettingToggle should be disabled when abac license is not installed 1`]
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<button
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--icon-danger rcx-button--square rcx-button--icon rcx-button rcx-css-1rtu0k9"
data-qa-reset-setting-id="ABAC_Enabled"
title="Reset"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-undo rcx-icon rcx-css-4pvxx3"
>
</i>
</button>
<label
class="rcx-box rcx-box--full rcx-toggle-switch"
>
Expand Down Expand Up @@ -68,7 +55,7 @@ exports[`SettingToggle should be disabled when abac license is not installed 1`]
</body>
`;

exports[`SettingToggle should render the setting toggle when setting exists 1`] = `
exports[`AbacEnabledToggle should render the setting toggle when setting exists 1`] = `
<body>
<div>
<div
Expand Down Expand Up @@ -135,7 +122,7 @@ exports[`SettingToggle should render the setting toggle when setting exists 1`]
</body>
`;

exports[`SettingToggle should show skeleton when loading 1`] = `
exports[`AbacEnabledToggle should show skeleton when loading 1`] = `
<body>
<div>
<div
Expand Down
Loading