mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-26 02:06:49 +00:00
feat(scheduler): add helper text and risk badges to scheduled action picker (#1449)
* feat(scheduler): add helper text and risk badges to scheduled action picker Add a concise helper text and risk level badge to every scheduled action in the create/edit modal. The six risk levels (Safe, Read-only, Interruptive, Runtime change, Removes containers, Destructive) map to the four existing design-system tones and render as a small dot+label chip next to the helper text, following the same pattern as SeverityBadge. Fix an ambiguous mobile label: update + target_type: fleet now resolves through resolveTaskAction and renders 'update node stacks' instead of the misleading 'update fleet'. Add exact helper-text and risk-level assertions for all 10 actions, plus component tests for default modal state, action-switch scenarios, and mobile update+fleet rendering. * docs: update stale scheduled-operations alt text for changed helper text
This commit is contained in:
@@ -10,8 +10,13 @@ import {
|
||||
DEFAULT_SCHEDULED_ACTION_ID,
|
||||
getActionById,
|
||||
resolveTaskAction,
|
||||
RISK_LABEL,
|
||||
RISK_TONE,
|
||||
RISK_BADGE_CLASSES,
|
||||
RISK_DOT_CLASSES,
|
||||
type BackendAction,
|
||||
type ScheduledActionCategory,
|
||||
type ScheduledActionRiskLevel,
|
||||
} from '../scheduledActions';
|
||||
|
||||
const BACKEND_ACTIONS: BackendAction[] = [
|
||||
@@ -93,4 +98,79 @@ describe('scheduledActions registry', () => {
|
||||
expect(fleetUpdate!.backendAction).toBe('update');
|
||||
expect(fleetUpdate!.targetType).toBe('fleet');
|
||||
});
|
||||
|
||||
describe('helperText', () => {
|
||||
const expected: Record<string, string> = {
|
||||
'auto_backup': 'Backs up compose and env files only. This does not back up application volumes.',
|
||||
'auto_start': 'Creates containers if they do not exist, or starts existing stopped containers.',
|
||||
'restart': 'Restarts containers in place. Running services are stopped and started again on the same configuration.',
|
||||
'auto_stop': 'Stops containers but keeps them in place for a faster start later.',
|
||||
'auto_down': 'Runs compose down. Containers are removed, but compose files remain on disk.',
|
||||
'update': "Checks this stack's images and recreates the stack only when newer images are available.",
|
||||
'update-fleet': 'Checks every stack on the selected node and updates stacks with newer images.',
|
||||
'scan': 'Runs Trivy against images on the selected local node and records the findings.',
|
||||
'prune': 'Removes unused Docker resources on the selected node. Be careful when pruning volumes.',
|
||||
'snapshot': 'Creates a versioned snapshot of compose and env files across the fleet.',
|
||||
};
|
||||
|
||||
for (const [id, text] of Object.entries(expected)) {
|
||||
it(`${id} helper text matches the specified wording`, () => {
|
||||
expect(getActionById(id)?.helperText).toBe(text);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('riskLevel', () => {
|
||||
const expected: Record<string, ScheduledActionRiskLevel> = {
|
||||
'auto_backup': 'safe',
|
||||
'auto_start': 'runtime-change',
|
||||
'restart': 'interruptive',
|
||||
'auto_stop': 'interruptive',
|
||||
'auto_down': 'removes-containers',
|
||||
'update': 'runtime-change',
|
||||
'update-fleet': 'runtime-change',
|
||||
'scan': 'read-only',
|
||||
'prune': 'destructive',
|
||||
'snapshot': 'safe',
|
||||
};
|
||||
|
||||
for (const [id, level] of Object.entries(expected)) {
|
||||
it(`${id} risk level is ${level}`, () => {
|
||||
expect(getActionById(id)?.riskLevel).toBe(level);
|
||||
});
|
||||
}
|
||||
|
||||
it('every action has a RISK_LABEL entry', () => {
|
||||
for (const def of SCHEDULED_ACTIONS) {
|
||||
expect(RISK_LABEL[def.riskLevel]).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('risk metadata maps', () => {
|
||||
it('RISK_TONE maps every action risk level', () => {
|
||||
for (const def of SCHEDULED_ACTIONS) {
|
||||
expect(RISK_TONE[def.riskLevel]).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('RISK_BADGE_CLASSES maps every action risk level', () => {
|
||||
for (const def of SCHEDULED_ACTIONS) {
|
||||
expect(RISK_BADGE_CLASSES[def.riskLevel]).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('RISK_DOT_CLASSES maps every action risk level', () => {
|
||||
for (const def of SCHEDULED_ACTIONS) {
|
||||
expect(RISK_DOT_CLASSES[def.riskLevel]).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('resolveTaskAction for update+fleet returns correct metadata', () => {
|
||||
const def = resolveTaskAction({ action: 'update', target_type: 'fleet' });
|
||||
expect(def?.id).toBe('update-fleet');
|
||||
expect(def?.riskLevel).toBe('runtime-change');
|
||||
expect(def?.helperText).toBe('Checks every stack on the selected node and updates stacks with newer images.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,49 @@ export type ScheduledActionId = BackendAction | 'update-fleet';
|
||||
export type ScheduledActionCategory = 'lifecycle' | 'updates' | 'security' | 'maintenance' | 'backups';
|
||||
export type ScheduledActionTone = 'success' | 'warning' | 'destructive' | 'brand';
|
||||
|
||||
/** Risk level assigned to each scheduled action, shown as a badge in the create/edit form. */
|
||||
export type ScheduledActionRiskLevel = 'safe' | 'read-only' | 'interruptive' | 'runtime-change' | 'removes-containers' | 'destructive';
|
||||
|
||||
/** Human-readable label for each risk level. */
|
||||
export const RISK_LABEL: Record<ScheduledActionRiskLevel, string> = {
|
||||
'safe': 'Safe',
|
||||
'read-only': 'Read-only',
|
||||
'interruptive': 'Interruptive',
|
||||
'runtime-change': 'Runtime change',
|
||||
'removes-containers': 'Removes containers',
|
||||
'destructive': 'Destructive',
|
||||
};
|
||||
|
||||
/** Design-system tone for each risk level. */
|
||||
export const RISK_TONE: Record<ScheduledActionRiskLevel, ScheduledActionTone> = {
|
||||
'safe': 'success',
|
||||
'read-only': 'brand',
|
||||
'interruptive': 'warning',
|
||||
'runtime-change': 'warning',
|
||||
'removes-containers': 'destructive',
|
||||
'destructive': 'destructive',
|
||||
};
|
||||
|
||||
/** Chip border/background/text classes for each risk level. */
|
||||
export const RISK_BADGE_CLASSES: Record<ScheduledActionRiskLevel, string> = {
|
||||
'safe': 'border-success/25 bg-success/8 text-success',
|
||||
'read-only': 'border-brand/25 bg-brand/8 text-brand',
|
||||
'interruptive': 'border-warning/25 bg-warning/8 text-warning',
|
||||
'runtime-change': 'border-warning/25 bg-warning/8 text-warning',
|
||||
'removes-containers': 'border-destructive/25 bg-destructive/8 text-destructive',
|
||||
'destructive': 'border-destructive/25 bg-destructive/8 text-destructive',
|
||||
};
|
||||
|
||||
/** Leading dot fill class for each risk level. */
|
||||
export const RISK_DOT_CLASSES: Record<ScheduledActionRiskLevel, string> = {
|
||||
'safe': 'bg-success',
|
||||
'read-only': 'bg-brand',
|
||||
'interruptive': 'bg-warning',
|
||||
'runtime-change': 'bg-warning',
|
||||
'removes-containers': 'bg-destructive',
|
||||
'destructive': 'bg-destructive',
|
||||
};
|
||||
|
||||
export interface ScheduledActionDefinition {
|
||||
id: ScheduledActionId;
|
||||
/** The action value sent to the backend (`update-fleet` maps to `update`). */
|
||||
@@ -38,7 +81,10 @@ export interface ScheduledActionDefinition {
|
||||
requiresStack: boolean;
|
||||
supportsServiceSelection: boolean;
|
||||
nodeScope?: 'local';
|
||||
helperText?: string;
|
||||
/** One-line explanation shown below the action picker in the create/edit form. */
|
||||
helperText: string;
|
||||
/** Risk level shown as a coloured chip next to the helper text. */
|
||||
riskLevel: ScheduledActionRiskLevel;
|
||||
}
|
||||
|
||||
/** Action pre-selected when the create modal opens. Decoupled from picker order. */
|
||||
@@ -47,20 +93,20 @@ export const DEFAULT_SCHEDULED_ACTION_ID: ScheduledActionId = 'restart';
|
||||
/** Ordered for the create-flow action picker, grouped by category. */
|
||||
export const SCHEDULED_ACTIONS: ScheduledActionDefinition[] = [
|
||||
// Lifecycle
|
||||
{ id: 'auto_backup', backendAction: 'auto_backup', label: 'Backup Stack Compose Files', shortLabel: 'backup', category: 'lifecycle', targetType: 'stack', tone: 'brand', requiresNode: true, requiresStack: true, supportsServiceSelection: false },
|
||||
{ id: 'auto_start', backendAction: 'auto_start', label: 'Start / Bring Up Stack', shortLabel: 'start', category: 'lifecycle', targetType: 'stack', tone: 'success', requiresNode: true, requiresStack: true, supportsServiceSelection: false },
|
||||
{ id: 'restart', backendAction: 'restart', label: 'Restart Stack', shortLabel: 'restart', category: 'lifecycle', targetType: 'stack', tone: 'brand', requiresNode: true, requiresStack: true, supportsServiceSelection: true },
|
||||
{ id: 'auto_stop', backendAction: 'auto_stop', label: 'Stop Stack', shortLabel: 'stop', category: 'lifecycle', targetType: 'stack', tone: 'warning', requiresNode: true, requiresStack: true, supportsServiceSelection: false },
|
||||
{ id: 'auto_down', backendAction: 'auto_down', label: 'Take Stack Down', shortLabel: 'down', category: 'lifecycle', targetType: 'stack', tone: 'destructive', requiresNode: true, requiresStack: true, supportsServiceSelection: false },
|
||||
{ id: 'auto_backup', backendAction: 'auto_backup', label: 'Backup Stack Compose Files', shortLabel: 'backup', category: 'lifecycle', targetType: 'stack', tone: 'brand', requiresNode: true, requiresStack: true, supportsServiceSelection: false, helperText: 'Backs up compose and env files only. This does not back up application volumes.', riskLevel: 'safe' },
|
||||
{ id: 'auto_start', backendAction: 'auto_start', label: 'Start / Bring Up Stack', shortLabel: 'start', category: 'lifecycle', targetType: 'stack', tone: 'success', requiresNode: true, requiresStack: true, supportsServiceSelection: false, helperText: 'Creates containers if they do not exist, or starts existing stopped containers.', riskLevel: 'runtime-change' },
|
||||
{ id: 'restart', backendAction: 'restart', label: 'Restart Stack', shortLabel: 'restart', category: 'lifecycle', targetType: 'stack', tone: 'brand', requiresNode: true, requiresStack: true, supportsServiceSelection: true, helperText: 'Restarts containers in place. Running services are stopped and started again on the same configuration.', riskLevel: 'interruptive' },
|
||||
{ id: 'auto_stop', backendAction: 'auto_stop', label: 'Stop Stack', shortLabel: 'stop', category: 'lifecycle', targetType: 'stack', tone: 'warning', requiresNode: true, requiresStack: true, supportsServiceSelection: false, helperText: 'Stops containers but keeps them in place for a faster start later.', riskLevel: 'interruptive' },
|
||||
{ id: 'auto_down', backendAction: 'auto_down', label: 'Take Stack Down', shortLabel: 'down', category: 'lifecycle', targetType: 'stack', tone: 'destructive', requiresNode: true, requiresStack: true, supportsServiceSelection: false, helperText: 'Runs compose down. Containers are removed, but compose files remain on disk.', riskLevel: 'removes-containers' },
|
||||
// Updates
|
||||
{ id: 'update', backendAction: 'update', label: 'Auto-update Stack', shortLabel: 'update', category: 'updates', targetType: 'stack', tone: 'success', requiresNode: true, requiresStack: true, supportsServiceSelection: false },
|
||||
{ id: 'update-fleet', backendAction: 'update', label: 'Auto-update All Stacks on Node', shortLabel: 'update', category: 'updates', targetType: 'fleet', tone: 'success', requiresNode: true, requiresStack: false, supportsServiceSelection: false, helperText: 'Every stack on the selected node will be checked and updated when new images are available.' },
|
||||
{ id: 'update', backendAction: 'update', label: 'Auto-update Stack', shortLabel: 'update', category: 'updates', targetType: 'stack', tone: 'success', requiresNode: true, requiresStack: true, supportsServiceSelection: false, helperText: 'Checks this stack\'s images and recreates the stack only when newer images are available.', riskLevel: 'runtime-change' },
|
||||
{ id: 'update-fleet', backendAction: 'update', label: 'Auto-update All Stacks on Node', shortLabel: 'update node', category: 'updates', targetType: 'fleet', tone: 'success', requiresNode: true, requiresStack: false, supportsServiceSelection: false, helperText: 'Checks every stack on the selected node and updates stacks with newer images.', riskLevel: 'runtime-change' },
|
||||
// Security
|
||||
{ id: 'scan', backendAction: 'scan', label: 'Scan Node Images', shortLabel: 'scan', category: 'security', targetType: 'system', tone: 'success', requiresNode: true, requiresStack: false, supportsServiceSelection: false, nodeScope: 'local', helperText: 'Every image on the selected node will be scanned. Scans run on local nodes only.' },
|
||||
{ id: 'scan', backendAction: 'scan', label: 'Scan Node Images', shortLabel: 'scan', category: 'security', targetType: 'system', tone: 'success', requiresNode: true, requiresStack: false, supportsServiceSelection: false, nodeScope: 'local', helperText: 'Runs Trivy against images on the selected local node and records the findings.', riskLevel: 'read-only' },
|
||||
// Maintenance
|
||||
{ id: 'prune', backendAction: 'prune', label: 'Prune Node Resources', shortLabel: 'prune', category: 'maintenance', targetType: 'system', tone: 'warning', requiresNode: true, requiresStack: false, supportsServiceSelection: false, nodeScope: 'local', helperText: 'Resources are pruned on the selected node. Prunes run on local nodes only.' },
|
||||
{ id: 'prune', backendAction: 'prune', label: 'Prune Node Resources', shortLabel: 'prune', category: 'maintenance', targetType: 'system', tone: 'warning', requiresNode: true, requiresStack: false, supportsServiceSelection: false, nodeScope: 'local', helperText: 'Removes unused Docker resources on the selected node. Be careful when pruning volumes.', riskLevel: 'destructive' },
|
||||
// Backups
|
||||
{ id: 'snapshot', backendAction: 'snapshot', label: 'Create Fleet Snapshot', shortLabel: 'snapshot', category: 'backups', targetType: 'fleet', tone: 'warning', requiresNode: false, requiresStack: false, supportsServiceSelection: false },
|
||||
{ id: 'snapshot', backendAction: 'snapshot', label: 'Create Fleet Snapshot', shortLabel: 'snapshot', category: 'backups', targetType: 'fleet', tone: 'warning', requiresNode: false, requiresStack: false, supportsServiceSelection: false, helperText: 'Creates a versioned snapshot of compose and env files across the fleet.', riskLevel: 'safe' },
|
||||
];
|
||||
|
||||
const ACTION_BY_ID = new Map<string, ScheduledActionDefinition>(SCHEDULED_ACTIONS.map(a => [a.id, a]));
|
||||
|
||||
Reference in New Issue
Block a user