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:
Devin Binnie
2026-08-17 12:35:59 -04:00
committed by GitHub
parent fd62fe4faa
commit d989f802d5
7 changed files with 111 additions and 60 deletions
@@ -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 {
@@ -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');
@@ -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);
@@ -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}`}
@@ -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
@@ -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.',
},
});
+1
View File
@@ -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",