Skip to content
Open
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 @@ -5882,7 +5882,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = `
]
}
>
a year ago
2 years ago
</Text>
</Text>
</Text>
Expand Down
8 changes: 8 additions & 0 deletions app/definitions/IServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ export type TSVMessage = {
subtitle?: string;
description?: string;
type: 'info' | 'alert' | 'error';
/**
* Roles allowed to see this message. When omitted (or empty), the message is
* shown to every user. When present, the message is only shown to users whose
* roles intersect this list (e.g. `['admin']` targets workspace admins only).
* Clients that predate this field ignore it and keep showing the message to
* everyone, so the field is backward compatible.
*/
roles?: string[];
params?: Record<string, unknown>;
link: string;
};
Expand Down
137 changes: 136 additions & 1 deletion app/lib/methods/checkSupportedVersions.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ISupportedVersionsData } from '../../definitions';
import { type ISupportedVersionsData, type TSVMessage } from '../../definitions';
import { checkSupportedVersions, getMessage } from './checkSupportedVersions';

const MOCK_I18N = {
Expand Down Expand Up @@ -518,4 +518,139 @@ describe('getMessage', () => {
link: 'Docs page'
});
});

describe('role targeting', () => {
const buildMessages = (roles?: string[]): TSVMessage[] => [
{
remainingDays: 15,
title: 'targeted',
subtitle: 'subtitle_token',
description: 'description_token',
type: 'info',
...(roles ? { roles } : {}),
link: 'Docs page'
}
];

test('shows a message with no roles to every user', () => {
expect(
getMessage({ messages: buildMessages(), expiration: '2023-04-10T00:00:00.000Z', userRoles: ['user'] })
).toMatchObject({ title: 'targeted' });
});

test('shows a role-targeted message when the user has the role', () => {
expect(
getMessage({ messages: buildMessages(['admin']), expiration: '2023-04-10T00:00:00.000Z', userRoles: ['admin', 'user'] })
).toMatchObject({ title: 'targeted' });
});

test('hides a role-targeted message from users without the role', () => {
expect(
getMessage({ messages: buildMessages(['admin']), expiration: '2023-04-10T00:00:00.000Z', userRoles: ['user'] })
).toBeUndefined();
});

test('hides a role-targeted message when user roles are unknown', () => {
expect(getMessage({ messages: buildMessages(['admin']), expiration: '2023-04-10T00:00:00.000Z' })).toBeUndefined();
});
});
});

describe('checkSupportedVersions role targeting', () => {
const buildSupportedVersions = (roles?: string[]): ISupportedVersionsData => ({
timestamp: TODAY,
enforcementStartDate: TODAY,
messages: [
{
remainingDays: 15,
title: 'targeted',
subtitle: 'subtitle_token',
description: 'description_token',
type: 'info',
...(roles ? { roles } : {}),
link: 'Docs page'
}
],
i18n: MOCK_I18N,
versions: [
{
version: '1.4.0',
expiration: '2023-04-10T00:00:00.000Z'
}
]
});

test('shows a role-targeted message when the user has the role', () => {
expect(
checkSupportedVersions({
supportedVersions: buildSupportedVersions(['admin']),
serverVersion: '1.4.0',
userRoles: ['admin', 'user']
})
).toMatchObject({ status: 'warn', message: { title: 'targeted' } });
});

test('hides a role-targeted message from users without the role', () => {
const result = checkSupportedVersions({
supportedVersions: buildSupportedVersions(['admin']),
serverVersion: '1.4.0',
userRoles: ['user']
});
expect(result.status).toBe('supported');
expect(result.message).toBeUndefined();
});

test('hides a role-targeted message when user roles are unknown', () => {
const result = checkSupportedVersions({
supportedVersions: buildSupportedVersions(['admin']),
serverVersion: '1.4.0'
});
expect(result.status).toBe('supported');
expect(result.message).toBeUndefined();
});

describe('enforcement window', () => {
const buildEnforcementSupportedVersions = (roles?: string[]): ISupportedVersionsData => ({
timestamp: TODAY,
enforcementStartDate: '2023-04-15T00:00:00.000Z',
messages: [
{
remainingDays: 15,
title: 'targeted',
subtitle: 'subtitle_token',
description: 'description_token',
type: 'info',
...(roles ? { roles } : {}),
link: 'Docs page'
}
],
i18n: MOCK_I18N,
versions: [
{
version: '1.4.0',
expiration: '2023-03-10T00:00:00.000Z'
}
]
});

test('does not warn a non-targeted user during the enforcement grace window', () => {
const result = checkSupportedVersions({
supportedVersions: buildEnforcementSupportedVersions(['admin']),
serverVersion: '1.4.0',
userRoles: ['user']
});
expect(result.status).toBe('supported');
expect(result.message).toBeUndefined();
});

test('warns a targeted user during the enforcement grace window', () => {
expect(
checkSupportedVersions({
supportedVersions: buildEnforcementSupportedVersions(['admin']),
serverVersion: '1.4.0',
userRoles: ['admin']
})
).toMatchObject({ status: 'warn', message: { title: 'targeted' } });
});
});
});
43 changes: 32 additions & 11 deletions app/lib/methods/checkSupportedVersions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,32 @@ import dayjs from '../dayjs';
import { type ISupportedVersionsData, type TSVDictionary, type TSVMessage, type TSVStatus } from '../../definitions';
import builtInSupportedVersions from '../../../app-supportedversions.json';

const messageMatchesUserRoles = (message: TSVMessage, userRoles?: string[]): boolean => {
// No targeting on the message → show to everyone (default behavior).
if (!message.roles?.length) {
return true;
}
// Targeting set but user roles unknown → don't show, honoring the restriction.
if (!userRoles?.length) {
return false;
}
return message.roles.some(role => userRoles.includes(role));
};

export const getMessage = ({
messages,
expiration
expiration,
userRoles
}: {
messages?: TSVMessage[];
expiration?: string;
userRoles?: string[];
}): TSVMessage | undefined => {
if (!messages?.length || !expiration || dayjs(expiration).diff(new Date(), 'days') < 0) {
return;
}
const sortedMessages = messages.sort((a, b) => a.remainingDays - b.remainingDays);
const eligibleMessages = messages.filter(message => messageMatchesUserRoles(message, userRoles));
const sortedMessages = eligibleMessages.sort((a, b) => a.remainingDays - b.remainingDays);
return sortedMessages.find(({ remainingDays }) => dayjs(expiration).diff(new Date(), 'hours') <= remainingDays * 24);
};

Expand All @@ -31,10 +46,12 @@ const getStatus = ({ expiration, message }: { expiration?: string; message?: TSV

export const checkSupportedVersions = function ({
supportedVersions,
serverVersion
serverVersion,
userRoles
}: {
supportedVersions?: ISupportedVersionsData;
serverVersion: string;
userRoles?: string[];
}): {
status: TSVStatus;
message?: TSVMessage;
Expand All @@ -56,21 +73,25 @@ export const checkSupportedVersions = function ({
const messages =
exception?.messages || (exception ? sv.exceptions?.messages : undefined) || versionInfo?.messages || sv.messages;
const expiration = exception?.expiration || versionInfo?.expiration;
const message = getMessage({ messages, expiration });
const message = getMessage({ messages, expiration, userRoles });
const status = getStatus({ message, expiration });

// TODO: enforcement start date is temp only. Remove after a few releases.
if (status === 'expired' && sv?.enforcementStartDate && new Date(sv.enforcementStartDate) > new Date()) {
const enforcementMessage = getMessage({
messages,
expiration: sv.enforcementStartDate
expiration: sv.enforcementStartDate,
userRoles
});
return {
status: 'warn',
message: enforcementMessage,
i18n: enforcementMessage ? sv?.i18n : undefined,
expiration: sv.enforcementStartDate
};
if (enforcementMessage) {
return {
status: 'warn',
message: enforcementMessage,
i18n: sv?.i18n,
expiration: sv.enforcementStartDate
};
}
return { status: 'supported', expiration: sv.enforcementStartDate };
}

return {
Expand Down
4 changes: 3 additions & 1 deletion app/sagas/selectServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,11 @@ const getServerInfoSaga = function* getServerInfoSaga({ server, raiseError = tru
if (!serverRecord) {
throw new Error('Server not found');
}
const userRoles = (yield* appSelector(state => state.login?.user?.roles)) ?? [];
const supportedVersionsResult = yield* call(checkSupportedVersions, {
supportedVersions: serverRecord.supportedVersions,
serverVersion: serverRecord.version
serverVersion: serverRecord.version,
userRoles
});
yield put(setSupportedVersions(supportedVersionsResult));

Expand Down
Loading