snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import type { SettingValue } from '@rocket.chat/core-typings';
|
||||
import { useSetModal, useSettingsDispatch } from '@rocket.chat/ui-contexts';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import WarningModal from './WarningModal';
|
||||
import type { EditableSetting } from '../../EditableSettingsContext';
|
||||
import { useEditableSetting } from '../../EditableSettingsContext';
|
||||
import MemoizedSetting from '../../settings/Setting/MemoizedSetting';
|
||||
import SettingSkeleton from '../../settings/Setting/SettingSkeleton';
|
||||
|
||||
export type ABACEnabledToggleProps = {
|
||||
hasABAC: 'loading' | boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const ABACEnabledToggle = ({ className, hasABAC }: ABACEnabledToggleProps) => {
|
||||
const setting = useEditableSetting('ABAC_Enabled');
|
||||
const setModal = useSetModal();
|
||||
const dispatch = useSettingsDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [value, setValue] = useState<boolean>(setting?.value === true);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(setting?.value === true);
|
||||
}, [setting]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: boolean) => {
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleChange = (value: boolean, setting: EditableSetting) => {
|
||||
setValue(value);
|
||||
dispatch([{ _id: setting._id, value }]);
|
||||
};
|
||||
|
||||
if (value === false) {
|
||||
return setModal(
|
||||
<WarningModal
|
||||
onConfirm={() => {
|
||||
handleChange(value, setting);
|
||||
setModal();
|
||||
}}
|
||||
onCancel={() => setModal()}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
handleChange(value, setting);
|
||||
},
|
||||
[dispatch, setModal, setting],
|
||||
);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
const value = setting.packageValue as boolean;
|
||||
setModal(
|
||||
<WarningModal
|
||||
onConfirm={() => {
|
||||
setValue(value);
|
||||
dispatch([{ _id: setting._id, value }]);
|
||||
setModal();
|
||||
}}
|
||||
onCancel={() => setModal()}
|
||||
/>,
|
||||
);
|
||||
}, [dispatch, setModal, setting]);
|
||||
|
||||
if (!setting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasABAC === 'loading') {
|
||||
return <SettingSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MemoizedSetting
|
||||
type='boolean'
|
||||
_id={setting._id}
|
||||
label={t(setting.i18nLabel)}
|
||||
value={value}
|
||||
packageValue={setting.packageValue === true}
|
||||
hint={t(setting.i18nDescription || '')}
|
||||
disabled={!hasABAC || setting.blocked}
|
||||
hasResetButton={hasABAC && setting.packageValue !== setting.value}
|
||||
onChangeValue={(value: SettingValue) => onChange(value === true)}
|
||||
onResetButtonClick={() => onReset()}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ABACEnabledToggle;
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { ISetting } from '@rocket.chat/core-typings';
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import SettingField from './SettingField';
|
||||
import EditableSettingsProvider from '../../settings/EditableSettingsProvider';
|
||||
|
||||
const settingStructure = {
|
||||
packageValue: false,
|
||||
blocked: false,
|
||||
public: true,
|
||||
type: 'boolean',
|
||||
i18nLabel: 'Test_Setting',
|
||||
i18nDescription: 'Test_Setting_Description',
|
||||
enableQuery: undefined,
|
||||
displayQuery: undefined,
|
||||
} as Partial<ISetting>;
|
||||
|
||||
const dispatchMock = jest.fn();
|
||||
|
||||
jest.mock('@rocket.chat/ui-contexts', () => ({
|
||||
...jest.requireActual('@rocket.chat/ui-contexts'),
|
||||
useSettingsDispatch: () => dispatchMock,
|
||||
}));
|
||||
jest.mock('@rocket.chat/core-typings', () => ({
|
||||
...jest.requireActual('@rocket.chat/core-typings'),
|
||||
isSetting: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
describe('SettingField', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
dispatchMock.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should call dispatch when setting value is changed', async () => {
|
||||
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
|
||||
render(<SettingField settingId='Test_Setting' />, {
|
||||
wrapper: mockAppRoot()
|
||||
.wrap((children) => <EditableSettingsProvider>{children}</EditableSettingsProvider>)
|
||||
.withSetting('Test_Setting', false, settingStructure)
|
||||
.build(),
|
||||
});
|
||||
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
await user.click(checkbox);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(dispatchMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch when the setting has an alert (confirmation is handled by the settings dispatch)', async () => {
|
||||
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
|
||||
render(<SettingField settingId='Test_Setting' />, {
|
||||
wrapper: mockAppRoot()
|
||||
.wrap((children) => <EditableSettingsProvider>{children}</EditableSettingsProvider>)
|
||||
.withSetting('Test_Setting', false, { ...settingStructure, alert: 'Test_Setting_Alert' })
|
||||
.build(),
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('checkbox'));
|
||||
|
||||
await act(async () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledWith([{ _id: 'Test_Setting', value: true }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
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 { useSettingsDispatch, useSettingStructure } from '@rocket.chat/ui-contexts';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MarkdownText from '../../../../components/MarkdownText';
|
||||
import { useEditableSetting, useEditableSettingVisibilityQuery } from '../../EditableSettingsContext';
|
||||
import MemoizedSetting from '../../settings/Setting/MemoizedSetting';
|
||||
import { useHasSettingModule } from '../../settings/hooks/useHasSettingModule';
|
||||
|
||||
export type SettingFieldProps = {
|
||||
className?: string;
|
||||
settingId: string;
|
||||
sectionChanged?: boolean;
|
||||
};
|
||||
|
||||
function SettingField({ className = undefined, settingId, sectionChanged }: SettingFieldProps) {
|
||||
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 = useSettingsDispatch();
|
||||
|
||||
const update = useDebouncedCallback(
|
||||
({ value, editor }: { value?: SettingValue; editor?: SettingEditor }) => {
|
||||
if (!persistedSetting) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch([
|
||||
{
|
||||
_id: persistedSetting._id,
|
||||
...(value !== undefined && { value }),
|
||||
...(editor !== undefined && { 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;
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { ISetting } from '@rocket.chat/core-typings';
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { axe } from 'jest-axe';
|
||||
|
||||
import AbacEnabledToggle from './AbacEnabledToggle';
|
||||
import EditableSettingsProvider from '../../settings/EditableSettingsProvider';
|
||||
|
||||
const settingStructure = {
|
||||
packageValue: false,
|
||||
blocked: false,
|
||||
public: true,
|
||||
type: 'boolean',
|
||||
i18nLabel: 'ABAC_Enabled',
|
||||
i18nDescription: 'ABAC_Enabled_Description',
|
||||
} as Partial<ISetting>;
|
||||
|
||||
const baseAppRoot = mockAppRoot()
|
||||
.wrap((children) => <EditableSettingsProvider>{children}</EditableSettingsProvider>)
|
||||
.withTranslations('en', 'core', {
|
||||
ABAC_Enabled: 'Enable ABAC',
|
||||
ABAC_Enabled_Description: 'Enable Attribute-Based Access Control',
|
||||
ABAC_Warning_Modal_Title: 'Disable ABAC',
|
||||
ABAC_Warning_Modal_Confirm_Text: 'Disable',
|
||||
Cancel: 'Cancel',
|
||||
});
|
||||
|
||||
describe('AbacEnabledToggle', () => {
|
||||
it('should render the setting toggle when setting exists', () => {
|
||||
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(<AbacEnabledToggle hasABAC={true} />, {
|
||||
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
|
||||
});
|
||||
|
||||
const toggle = screen.getByRole('checkbox');
|
||||
await waitFor(() => {
|
||||
expect(toggle).not.toBeDisabled();
|
||||
});
|
||||
await user.click(toggle);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Disable ABAC')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: discover how to automatically unmount all modals after each test
|
||||
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
});
|
||||
|
||||
it('should not show warning modal when enabling ABAC', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AbacEnabledToggle hasABAC={true} />, {
|
||||
wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(),
|
||||
});
|
||||
|
||||
const toggle = screen.getByRole('checkbox');
|
||||
await user.click(toggle);
|
||||
|
||||
// The modal should not appear when enabling ABAC
|
||||
expect(screen.queryByText('Disable ABAC')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show warning modal when resetting setting', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AbacEnabledToggle hasABAC={true} />, {
|
||||
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
|
||||
});
|
||||
|
||||
const resetButton = screen.getByRole('button', { name: /reset/i });
|
||||
await user.click(resetButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Disable ABAC')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: discover how to automatically unmount all modals after each test
|
||||
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
});
|
||||
|
||||
it('should have no accessibility violations', async () => {
|
||||
const { container } = render(<AbacEnabledToggle hasABAC={true} />, {
|
||||
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
|
||||
});
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should handle setting change correctly', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AbacEnabledToggle hasABAC={true} />, {
|
||||
wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(),
|
||||
});
|
||||
|
||||
const toggle = await screen.findByRole('checkbox', { busy: false });
|
||||
expect(toggle).not.toBeChecked();
|
||||
|
||||
await user.click(toggle);
|
||||
expect(toggle).toBeChecked();
|
||||
});
|
||||
|
||||
it('should be disabled when abac license is not installed', () => {
|
||||
const { baseElement } = render(<AbacEnabledToggle hasABAC={false} />, {
|
||||
wrapper: baseAppRoot.withSetting('ABAC_Enabled', true, settingStructure).build(),
|
||||
});
|
||||
|
||||
const toggle = screen.getByRole('checkbox');
|
||||
expect(toggle).toBeDisabled();
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show skeleton when 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(<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(<AbacEnabledToggle hasABAC={true} />, {
|
||||
wrapper: baseAppRoot.withSetting('ABAC_Enabled', false, settingStructure).build(),
|
||||
});
|
||||
|
||||
expect(screen.queryByRole('button', { name: /reset/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { mockAppRoot } from '@rocket.chat/mock-providers';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import AbacEnabledToggle from './AbacEnabledToggle';
|
||||
import EditableSettingsProvider from '../../settings/EditableSettingsProvider';
|
||||
|
||||
const meta: Meta<typeof AbacEnabledToggle> = {
|
||||
component: AbacEnabledToggle,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
decorators: [
|
||||
(Story) => {
|
||||
const AppRoot = mockAppRoot()
|
||||
.wrap((children) => <EditableSettingsProvider>{children}</EditableSettingsProvider>)
|
||||
.withTranslations('en', 'core', {
|
||||
ABAC_Enabled: 'Enable ABAC',
|
||||
ABAC_Enabled_Description: 'Enable Attribute-Based Access Control',
|
||||
ABAC_Warning_Modal_Title: 'Disable ABAC',
|
||||
ABAC_Warning_Modal_Confirm_Text: 'Disable',
|
||||
Cancel: 'Cancel',
|
||||
})
|
||||
.withSetting('ABAC_Enabled', true, {
|
||||
packageValue: false,
|
||||
blocked: false,
|
||||
public: true,
|
||||
type: 'boolean',
|
||||
i18nLabel: 'ABAC_Enabled',
|
||||
i18nDescription: 'ABAC_Enabled_Description',
|
||||
})
|
||||
.build();
|
||||
|
||||
return (
|
||||
<AppRoot>
|
||||
<Story />
|
||||
</AppRoot>
|
||||
);
|
||||
},
|
||||
],
|
||||
args: {
|
||||
hasABAC: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AbacEnabledToggle>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
hasABAC: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
hasABAC: 'loading',
|
||||
},
|
||||
};
|
||||
|
||||
export const False: Story = {
|
||||
args: {
|
||||
hasABAC: false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Accordion, AccordionItem, Box, Callout, FieldGroup } from '@rocket.chat/fuselage';
|
||||
import { ExternalLink } from '@rocket.chat/ui-client';
|
||||
import { useSetting } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
|
||||
import AbacEnabledToggle from './AbacEnabledToggle';
|
||||
import SettingField from './SettingField';
|
||||
import { useHasLicenseModule } from '../../../../hooks/useHasLicenseModule';
|
||||
import { links } from '../../../../lib/links';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: hasABAC = false } = useHasLicenseModule('abac');
|
||||
const pdpType = useSetting('ABAC_PDP_Type', 'local');
|
||||
|
||||
return (
|
||||
<Box maxWidth='x600' width='full' alignSelf='center' overflow='auto' marginBlock={24}>
|
||||
<FieldGroup>
|
||||
<AbacEnabledToggle hasABAC={hasABAC} />
|
||||
<SettingField settingId='ABAC_PDP_Type' />
|
||||
{pdpType !== 'local' && <SettingField settingId='ABAC_Attribute_Store' />}
|
||||
<SettingField settingId='ABAC_ShowAttributesInRooms' />
|
||||
<SettingField settingId='Abac_Cache_Decision_Time_Seconds' />
|
||||
|
||||
{pdpType === 'local' && (
|
||||
<Callout>
|
||||
<Trans i18nKey='ABAC_Enabled_callout'>
|
||||
User attributes are synchronized via LDAP
|
||||
<ExternalLink to={links.go.abacLDAPDocs}>Learn more</ExternalLink>
|
||||
</Trans>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
<Accordion>
|
||||
<AccordionItem title={t('ABAC_Classification_Banners')}>
|
||||
<FieldGroup>
|
||||
<SettingField settingId='ABAC_Classification_Banners_Enabled' />
|
||||
<SettingField settingId='ABAC_Classification_Banners_Config' />
|
||||
</FieldGroup>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem title={t('LDAP_DataSync_ABAC')}>
|
||||
<FieldGroup>
|
||||
<SettingField settingId='LDAP_Background_Sync_ABAC_Attributes' />
|
||||
<SettingField settingId='LDAP_Background_Sync_ABAC_Attributes_Interval' />
|
||||
<SettingField settingId='LDAP_ABAC_AttributeMap' />
|
||||
</FieldGroup>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem title={t('ABAC_Virtru_PDP_Configuration')}>
|
||||
<SettingField settingId='ABAC_Virtru_Base_URL' />
|
||||
<SettingField settingId='ABAC_Virtru_Client_ID' />
|
||||
<SettingField settingId='ABAC_Virtru_Client_Secret' />
|
||||
<SettingField settingId='ABAC_Virtru_OIDC_Endpoint' />
|
||||
<SettingField settingId='ABAC_Virtru_Default_Entity_Key' />
|
||||
<SettingField settingId='ABAC_Virtru_Attribute_Namespace' />
|
||||
<SettingField settingId='ABAC_Virtru_Sync_Interval' />
|
||||
<SettingField settingId='ABAC_Virtru_Test_Connection' />
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</FieldGroup>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Box } from '@rocket.chat/fuselage';
|
||||
import { GenericModal } from '@rocket.chat/ui-client';
|
||||
import { useRouter } from '@rocket.chat/ui-contexts';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
export type WarningModalProps = {
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const WarningModal = ({ onConfirm, onCancel }: WarningModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const handleNavigate = () => {
|
||||
onCancel();
|
||||
router.navigate({
|
||||
name: 'admin-ABAC',
|
||||
params: {
|
||||
tab: 'rooms',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
title={t('ABAC_Warning_Modal_Title')}
|
||||
variant='secondary-danger'
|
||||
confirmText={t('ABAC_Warning_Modal_Confirm_Text')}
|
||||
cancelText={t('Cancel')}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
onClose={onCancel}
|
||||
onDismiss={onCancel}
|
||||
>
|
||||
<Trans i18nKey='ABAC_Warning_Modal_Content'>
|
||||
You will not be able to automatically or manually manage users in existing ABAC-managed rooms. To restore a room's default access
|
||||
control, it must be removed from ABAC management in
|
||||
<Box is='a' onClick={handleNavigate}>
|
||||
{' '}
|
||||
ABAC {'>'} Rooms
|
||||
</Box>
|
||||
.
|
||||
</Trans>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarningModal;
|
||||
@@ -0,0 +1,149 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`AbacEnabledToggle should be disabled when abac license is not installed 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field rcx-css-1gfu76s"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1u2ihfm"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row rcx-css-ctk2ij"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-field__label rcx-label"
|
||||
for="ABAC_Enabled"
|
||||
title="ABAC_Enabled"
|
||||
>
|
||||
Enable ABAC
|
||||
</label>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-127j9mz"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-toggle-switch"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="rcx-box rcx-box--full rcx-toggle-switch__input"
|
||||
disabled=""
|
||||
id="ABAC_Enabled"
|
||||
type="checkbox"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-toggle-switch__fake"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__hint"
|
||||
>
|
||||
Enable Attribute-Based Access Control
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`AbacEnabledToggle should render the setting toggle when setting exists 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field rcx-css-1gfu76s"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-css-1u2ihfm"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row rcx-css-ctk2ij"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-field__label rcx-label"
|
||||
for="ABAC_Enabled"
|
||||
title="ABAC_Enabled"
|
||||
>
|
||||
Enable ABAC
|
||||
</label>
|
||||
<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"
|
||||
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"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="rcx-box rcx-box--full rcx-toggle-switch__input"
|
||||
id="ABAC_Enabled"
|
||||
type="checkbox"
|
||||
/>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="rcx-box rcx-box--full rcx-toggle-switch__fake"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__hint"
|
||||
>
|
||||
Enable Attribute-Based Access Control
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`AbacEnabledToggle should show skeleton when loading 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-field"
|
||||
>
|
||||
<label
|
||||
class="rcx-box rcx-box--full rcx-field__label rcx-label rcx-css-1exa5vl"
|
||||
>
|
||||
<span
|
||||
class="rcx-skeleton rcx-skeleton--text rcx-css-1v1eapn"
|
||||
/>
|
||||
</label>
|
||||
<span
|
||||
class="rcx-box rcx-box--full rcx-field__row"
|
||||
>
|
||||
<div
|
||||
class="rcx-box rcx-box--full rcx-skeleton__input"
|
||||
>
|
||||
<span
|
||||
class="rcx-skeleton rcx-skeleton--text rcx-css-1qcz93u"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
Reference in New Issue
Block a user