mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-29 02:28:16 +08:00
Disable TTL/grace period editing for server-derived attributes (#38001)
* Disable TTL/grace period editing for server-derived attributes * FIx lint * Fix bug with dot menu
This commit is contained in:
+2
@@ -121,9 +121,11 @@
|
||||
border-radius: 4px;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.75);
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.SessionAttributes__type-cell {
|
||||
|
||||
+8
-8
@@ -59,7 +59,7 @@ function makeField(name: string, type: 'text' | 'select', sortOrder: number, ext
|
||||
}
|
||||
|
||||
const representativeFields: UserPropertyField[] = [
|
||||
makeField('ip_address', 'text', 0, {
|
||||
makeField('client_ip_address', 'text', 0, {
|
||||
display_name: 'Client IP',
|
||||
platforms: ['desktop', 'browser'],
|
||||
ttl_seconds: 300,
|
||||
@@ -212,9 +212,9 @@ describe('SessionAttributesPage', () => {
|
||||
|
||||
await screen.findByText('Client IP');
|
||||
|
||||
await userEvent.click(screen.getByTestId('session-attribute-dotmenu-session-ip_address'));
|
||||
await userEvent.click(screen.getByTestId('session-attribute-dotmenu-session-client_ip_address'));
|
||||
await userEvent.hover(screen.getByRole('menuitem', {name: /Time-to-live/}));
|
||||
await userEvent.click(await screen.findByTestId('session-attribute-ttl-option-session-ip_address-3600'));
|
||||
await userEvent.click(await screen.findByTestId('session-attribute-ttl-option-session-client_ip_address-3600'));
|
||||
|
||||
const saveButton = screen.getByRole('button', {name: /Save/});
|
||||
expect(saveButton).toBeEnabled();
|
||||
@@ -228,7 +228,7 @@ describe('SessionAttributesPage', () => {
|
||||
expect(patchPropertyField).toHaveBeenCalledWith(
|
||||
SESSION_ATTRIBUTES_GROUP_ID,
|
||||
SESSION_ATTRIBUTES_OBJECT_TYPE,
|
||||
'session-ip_address',
|
||||
'session-client_ip_address',
|
||||
{attrs: {ttl_seconds: 3600}},
|
||||
);
|
||||
});
|
||||
@@ -255,9 +255,9 @@ describe('SessionAttributesPage', () => {
|
||||
await screen.findByText('Client IP');
|
||||
expect(store.getState().views.admin.navigationBlock.blocked).toBe(false);
|
||||
|
||||
await userEvent.click(screen.getByTestId('session-attribute-dotmenu-session-ip_address'));
|
||||
await userEvent.click(screen.getByTestId('session-attribute-dotmenu-session-client_ip_address'));
|
||||
await userEvent.hover(screen.getByRole('menuitem', {name: /Time-to-live/}));
|
||||
await userEvent.click(await screen.findByTestId('session-attribute-ttl-option-session-ip_address-3600'));
|
||||
await userEvent.click(await screen.findByTestId('session-attribute-ttl-option-session-client_ip_address-3600'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.getState().views.admin.navigationBlock.blocked).toBe(true);
|
||||
@@ -277,9 +277,9 @@ describe('SessionAttributesPage', () => {
|
||||
|
||||
await screen.findByText('Client IP');
|
||||
|
||||
await userEvent.click(screen.getByTestId('session-attribute-dotmenu-session-ip_address'));
|
||||
await userEvent.click(screen.getByTestId('session-attribute-dotmenu-session-client_ip_address'));
|
||||
await userEvent.hover(screen.getByRole('menuitem', {name: /Time-to-live/}));
|
||||
await userEvent.click(await screen.findByTestId('session-attribute-ttl-option-session-ip_address-3600'));
|
||||
await userEvent.click(await screen.findByTestId('session-attribute-ttl-option-session-client_ip_address-3600'));
|
||||
|
||||
const stagedRow = screen.getAllByText('Client IP')[0].closest('tr') as HTMLElement;
|
||||
expect(within(stagedRow).getByTestId('session-attribute-ttl')).toHaveTextContent('1h');
|
||||
|
||||
+17
-1
@@ -42,7 +42,7 @@ function makeField(name: string, extra: ExtraAttrs = {}): SessionAttributeField
|
||||
} as UserPropertyField;
|
||||
}
|
||||
|
||||
const enabledField = makeField('ip_address', {
|
||||
const enabledField = makeField('client_ip_address', {
|
||||
display_name: 'Client IP',
|
||||
enabled: true,
|
||||
ttl_seconds: 300,
|
||||
@@ -56,6 +56,13 @@ const disabledField = makeField('vpn_active', {
|
||||
grace_period_seconds: 30,
|
||||
});
|
||||
|
||||
const serverSourcedField = makeField('ip_address', {
|
||||
display_name: 'IP Address',
|
||||
enabled: true,
|
||||
ttl_seconds: 300,
|
||||
grace_period_seconds: 60,
|
||||
});
|
||||
|
||||
function renderMenu(field: SessionAttributeField, onStageChange = jest.fn()) {
|
||||
renderWithContext(
|
||||
<div>
|
||||
@@ -125,6 +132,15 @@ describe('SessionAttributesDotMenu', () => {
|
||||
expect(unselected).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
|
||||
it('hides the TTL and Grace submenus for server-sourced attributes', async () => {
|
||||
renderMenu(serverSourcedField);
|
||||
|
||||
await userEvent.click(screen.getByTestId(`session-attribute-dotmenu-${serverSourcedField.id}`));
|
||||
|
||||
expect(screen.queryByRole('menuitem', {name: /Time-to-live/})).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('menuitem', {name: /Grace Period/})).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the confirmation modal for Disable without staging immediately', async () => {
|
||||
const onStageChange = renderMenu(enabledField);
|
||||
|
||||
|
||||
+45
-40
@@ -15,7 +15,7 @@ import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import DisableAttributeModal from './disable_attribute_modal';
|
||||
import type {StagedAttrs} from './use_session_attribute_edits';
|
||||
import {DURATION_PRESETS_SECONDS, formatDuration, getSessionAttrs, getSessionDisplayName} from './utils';
|
||||
import {DURATION_PRESETS_SECONDS, formatDuration, getSessionAttrs, getSessionDisplayName, isServerSourced} from './utils';
|
||||
import type {SessionAttributeField} from './utils';
|
||||
|
||||
type Props = {
|
||||
@@ -30,6 +30,7 @@ export default function SessionAttributesDotMenu({field, onStageChange, disabled
|
||||
|
||||
const attrs = getSessionAttrs(field);
|
||||
const menuId = `session-attribute-dotmenu-${field.id}`;
|
||||
const isServerAttribute = isServerSourced(field.name);
|
||||
|
||||
const handleTtlChange = useCallback((seconds: number) => {
|
||||
onStageChange(field.id, {ttl_seconds: seconds});
|
||||
@@ -80,45 +81,49 @@ export default function SessionAttributesDotMenu({field, onStageChange, disabled
|
||||
'aria-label': formatMessage({id: 'admin.session_attributes.dotmenu.menu.aria_label', defaultMessage: 'Select an action'}),
|
||||
}}
|
||||
>
|
||||
<Menu.SubMenu
|
||||
id={`${menuId}-ttl`}
|
||||
menuId={`${menuId}-ttl-menu`}
|
||||
leadingElement={<UpdateIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='admin.session_attributes.dotmenu.ttl.label'
|
||||
defaultMessage='Time-to-live (TTL)'
|
||||
/>
|
||||
)}
|
||||
trailingElements={(
|
||||
<>
|
||||
{formatDuration(attrs.ttl_seconds)}
|
||||
<ChevronRightIcon size={16}/>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{renderPresets(attrs.ttl_seconds, `session-attribute-ttl-option-${field.id}`, handleTtlChange)}
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu
|
||||
id={`${menuId}-grace`}
|
||||
menuId={`${menuId}-grace-menu`}
|
||||
leadingElement={<AlertCircleOutlineIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='admin.session_attributes.dotmenu.grace.label'
|
||||
defaultMessage='Grace Period'
|
||||
/>
|
||||
)}
|
||||
trailingElements={(
|
||||
<>
|
||||
{formatDuration(attrs.grace_period_seconds)}
|
||||
<ChevronRightIcon size={16}/>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{renderPresets(attrs.grace_period_seconds, `session-attribute-grace-option-${field.id}`, (seconds) => onStageChange(field.id, {grace_period_seconds: seconds}))}
|
||||
</Menu.SubMenu>
|
||||
<Menu.Separator/>
|
||||
{!isServerAttribute && ([
|
||||
<Menu.SubMenu
|
||||
key={`${menuId}-ttl`}
|
||||
id={`${menuId}-ttl`}
|
||||
menuId={`${menuId}-ttl-menu`}
|
||||
leadingElement={<UpdateIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='admin.session_attributes.dotmenu.ttl.label'
|
||||
defaultMessage='Time-to-live (TTL)'
|
||||
/>
|
||||
)}
|
||||
trailingElements={(
|
||||
<>
|
||||
{formatDuration(attrs.ttl_seconds)}
|
||||
<ChevronRightIcon size={16}/>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{renderPresets(attrs.ttl_seconds, `session-attribute-ttl-option-${field.id}`, handleTtlChange)}
|
||||
</Menu.SubMenu>,
|
||||
<Menu.SubMenu
|
||||
key={`${menuId}-grace`}
|
||||
id={`${menuId}-grace`}
|
||||
menuId={`${menuId}-grace-menu`}
|
||||
leadingElement={<AlertCircleOutlineIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='admin.session_attributes.dotmenu.grace.label'
|
||||
defaultMessage='Grace Period'
|
||||
/>
|
||||
)}
|
||||
trailingElements={(
|
||||
<>
|
||||
{formatDuration(attrs.grace_period_seconds)}
|
||||
<ChevronRightIcon size={16}/>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{renderPresets(attrs.grace_period_seconds, `session-attribute-grace-option-${field.id}`, (seconds) => onStageChange(field.id, {grace_period_seconds: seconds}))}
|
||||
</Menu.SubMenu>,
|
||||
<Menu.Separator key={`${menuId}-separator`}/>,
|
||||
])}
|
||||
{attrs.enabled ? (
|
||||
<Menu.Item
|
||||
id={`session-attribute-disable-${field.id}`}
|
||||
|
||||
+16
-1
@@ -61,6 +61,8 @@ const fields: SessionAttributeField[] = [
|
||||
makeField('client_ip_address', 'text', {
|
||||
sort_order: 1,
|
||||
platforms: ['desktop', 'mobile', 'browser'],
|
||||
ttl_seconds: 300,
|
||||
grace_period_seconds: 60,
|
||||
enabled: false,
|
||||
}),
|
||||
makeField('os_version', 'text', {sort_order: 2, enabled: true}),
|
||||
@@ -189,11 +191,24 @@ describe('SessionAttributesTable', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const row = rowFor('Client IP');
|
||||
const row = rowFor('client_ip_address');
|
||||
expect(within(row).getByTestId('session-attribute-ttl')).toHaveTextContent('5m');
|
||||
expect(within(row).getByTestId('session-attribute-grace')).toHaveTextContent('1m');
|
||||
});
|
||||
|
||||
it('shows a dash for TTL and Grace on server-sourced attributes', () => {
|
||||
renderWithContext(
|
||||
<SessionAttributesTable
|
||||
data={fields}
|
||||
onStageChange={onStageChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
const row = rowFor('Client IP');
|
||||
expect(within(row).getByTestId('session-attribute-ttl')).toHaveTextContent('—');
|
||||
expect(within(row).getByTestId('session-attribute-grace')).toHaveTextContent('—');
|
||||
});
|
||||
|
||||
it('falls back to the field name when display_name is absent', () => {
|
||||
renderWithContext(
|
||||
<SessionAttributesTable
|
||||
|
||||
+22
-10
@@ -8,6 +8,7 @@ import {FormattedMessage, defineMessages} from 'react-intl';
|
||||
|
||||
import {CheckboxMarkedCircleOutlineIcon, ChevronDownCircleOutlineIcon, MenuVariantIcon} from '@mattermost/compass-icons/components';
|
||||
import type IconProps from '@mattermost/compass-icons/components/props';
|
||||
import {WithTooltip} from '@mattermost/shared/components/tooltip';
|
||||
|
||||
import PlatformIcons from './platform_icons';
|
||||
import SessionAttributesDotMenu from './session_attributes_dot_menu';
|
||||
@@ -78,15 +79,19 @@ export default function SessionAttributesTable({data, onStageChange, disabled =
|
||||
<span className='SessionAttributes__name-cell'>
|
||||
<span className='SessionAttributes__name-text'>{getValue()}</span>
|
||||
{isServerSourced(row.original.name) && (
|
||||
<span
|
||||
className='SessionAttributes__server-badge'
|
||||
data-testid='session-attribute-server-label'
|
||||
<WithTooltip
|
||||
title={messages.serverLabelTooltip}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.session_attributes.table.server_label'
|
||||
defaultMessage='Server'
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className='SessionAttributes__server-badge'
|
||||
data-testid='session-attribute-server-label'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.session_attributes.table.server_label'
|
||||
defaultMessage='Server'
|
||||
/>
|
||||
</span>
|
||||
</WithTooltip>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
@@ -154,7 +159,7 @@ export default function SessionAttributesTable({data, onStageChange, disabled =
|
||||
className='SessionAttributes__duration'
|
||||
data-testid='session-attribute-ttl'
|
||||
>
|
||||
{formatDuration(getSessionAttrs(row.original).ttl_seconds)}
|
||||
{isServerSourced(row.original.name) ? '—' : formatDuration(getSessionAttrs(row.original).ttl_seconds)}
|
||||
</span>
|
||||
),
|
||||
enableHiding: false,
|
||||
@@ -176,7 +181,7 @@ export default function SessionAttributesTable({data, onStageChange, disabled =
|
||||
className='SessionAttributes__duration'
|
||||
data-testid='session-attribute-grace'
|
||||
>
|
||||
{formatDuration(getSessionAttrs(row.original).grace_period_seconds)}
|
||||
{isServerSourced(row.original.name) ? '—' : formatDuration(getSessionAttrs(row.original).grace_period_seconds)}
|
||||
</span>
|
||||
),
|
||||
enableHiding: false,
|
||||
@@ -257,3 +262,10 @@ const typeLabels = defineMessages({
|
||||
Boolean: {id: 'admin.session_attributes.type.boolean', defaultMessage: 'Boolean'},
|
||||
Enum: {id: 'admin.session_attributes.type.enum', defaultMessage: 'Enum'},
|
||||
});
|
||||
|
||||
const messages = defineMessages({
|
||||
serverLabelTooltip: {
|
||||
id: 'admin.session_attributes.table.server_label.tooltip',
|
||||
defaultMessage: 'This attribute is captured by the server for every request a user makes. It is not delivered by the client.',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3337,6 +3337,7 @@
|
||||
"admin.session_attributes.table.name": "Name",
|
||||
"admin.session_attributes.table.platform": "Platform",
|
||||
"admin.session_attributes.table.server_label": "Server",
|
||||
"admin.session_attributes.table.server_label.tooltip": "This attribute is captured by the server for every request a user makes. It is not delivered by the client.",
|
||||
"admin.session_attributes.table.status": "Status",
|
||||
"admin.session_attributes.table.ttl": "TTL",
|
||||
"admin.session_attributes.table.type": "Type",
|
||||
|
||||
Reference in New Issue
Block a user