mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 14:56:27 +00:00
feat(labels): harden Stack Labels (gate parity, abort, dry-run, cap) (#1232)
* feat(labels): harden stack-labels surface (gate parity, abort, dry-run policy, cap) - Cap labelIds array at MAX_LABELS_PER_NODE on PUT /api/stacks/:name/labels. - Hide sidebar Labels submenu and Settings mutation buttons for roles without stack:edit, matching the backend requirePermission gate. - Break the per-label bulk-action loop on req.aborted so cancelled requests release the per-node lock once the in-flight op completes. - Reset saving state on success in LabelInlineCreateForm so the form stays interactive when reused outside the kebab/context menus. - Invoke enforcePolicyPreDeploy inside the dry-run deploy branch and report blocked stacks honestly; previously dry-run skipped the gate and would predict success for stacks the real deploy would block. * fix(labels): split sidebar gate so inline create matches unscoped backend guard Codex review of #1232 surfaced a parity miss: POST /api/labels is guarded by unscoped requirePermission('stack:edit'), but the sidebar inline "New label" entry was gated by canEditLabels (per-stack scoped). An Admiral user with only scoped grants on a stack could toggle existing labels but the inline create request would 403. Splits the sidebar gate: - canEditLabels (scoped) keeps gating the Labels submenu trigger and toggle items, matching PUT /api/stacks/:name/labels. - canCreateLabels (unscoped) now gates the inline "New label" entry, matching POST /api/labels. Also restores the swallow-catch in LabelInlineCreateForm. The earlier catch-to-finally change in #1232 let the rethrow from createAndAssignLabel surface as an unhandled event-handler rejection in the browser console. Parents already toast on failure; swallowing in the form is the intended behavior with the finally reset still in place.
This commit is contained in:
@@ -19,7 +19,7 @@ import type { MenuGroup, MenuItem, StackMenuCtx } from '@/components/sidebar/sid
|
||||
|
||||
export function useStackMenuItems(_file: string, ctx: StackMenuCtx): MenuGroup[] {
|
||||
const {
|
||||
stackStatus, hasPort, isBusy, isPaid, canDelete, isPinned, labels,
|
||||
stackStatus, hasPort, isBusy, isPaid, canDelete, canEditLabels, isPinned, labels,
|
||||
openAlertSheet, openAutoHeal, checkUpdates, openStackApp,
|
||||
deploy, stop, restart, update, remove, pin, unpin, toggleLabel,
|
||||
menuVisibility, autoUpdateEnabled, setAutoUpdateEnabled, openScheduleTask,
|
||||
@@ -48,19 +48,21 @@ export function useStackMenuItems(_file: string, ctx: StackMenuCtx): MenuGroup[]
|
||||
groups.push({ id: 'inspect', items: inspect });
|
||||
|
||||
const organize: MenuItem[] = [];
|
||||
organize.push({
|
||||
id: 'labels',
|
||||
label: 'Labels',
|
||||
icon: Tag,
|
||||
shortcut: 'L ›',
|
||||
onSelect: () => {},
|
||||
subItems: labels.map(l => ({
|
||||
id: `label:${l.id}`,
|
||||
label: l.name,
|
||||
if (canEditLabels) {
|
||||
organize.push({
|
||||
id: 'labels',
|
||||
label: 'Labels',
|
||||
icon: Tag,
|
||||
onSelect: () => toggleLabel(l.id),
|
||||
})),
|
||||
});
|
||||
shortcut: 'L ›',
|
||||
onSelect: () => {},
|
||||
subItems: labels.map(l => ({
|
||||
id: `label:${l.id}`,
|
||||
label: l.name,
|
||||
icon: Tag,
|
||||
onSelect: () => toggleLabel(l.id),
|
||||
})),
|
||||
});
|
||||
}
|
||||
organize.push(
|
||||
isPinned
|
||||
? { id: 'pin', label: 'Unpin', icon: PinOff, shortcut: 'P', onSelect: unpin }
|
||||
@@ -85,7 +87,7 @@ export function useStackMenuItems(_file: string, ctx: StackMenuCtx): MenuGroup[]
|
||||
|
||||
return groups;
|
||||
}, [
|
||||
stackStatus, hasPort, isBusy, isPaid, canDelete, isPinned, labels,
|
||||
stackStatus, hasPort, isBusy, isPaid, canDelete, canEditLabels, isPinned, labels,
|
||||
showDeploy, showStop, showRestart, showUpdate,
|
||||
autoUpdateEnabled, setAutoUpdateEnabled,
|
||||
openAlertSheet, openAutoHeal, checkUpdates, openStackApp,
|
||||
|
||||
Reference in New Issue
Block a user