fix: harden stack label permissions (#1036)

* fix: harden stack label permissions

* fix: avoid test db init from debug logging
This commit is contained in:
Anso
2026-05-13 11:56:22 -04:00
committed by GitHub
parent 328a98439d
commit b52323036b
9 changed files with 295 additions and 27 deletions
@@ -2,7 +2,6 @@ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
import { useNodes } from '@/context/NodeContext';
import { useLicense } from '@/context/LicenseContext';
import { useImageUpdates } from '@/hooks/useImageUpdates';
import { usePinnedStacks } from '@/hooks/usePinnedStacks';
import { useSidebarGroupCollapse } from '@/hooks/useSidebarGroupCollapse';
@@ -32,7 +31,6 @@ export interface RemoteResult {
export function useStackListState() {
const { nodes, activeNode } = useNodes();
const { isPaid } = useLicense();
const [files, setFiles] = useState<string[]>([]);
const [selectedFile, setSelectedFile] = useState<string | null>(null);
@@ -84,7 +82,6 @@ export function useStackListState() {
};
const refreshLabels = useCallback(async () => {
if (!isPaid) return;
try {
const [labelsRes, assignmentsRes] = await Promise.all([
apiFetch('/labels'),
@@ -95,7 +92,7 @@ export function useStackListState() {
} catch {
// Labels are non-critical; fail silently
}
}, [isPaid]);
}, []);
useEffect(() => {
const handler = () => refreshLabels();
+3 -3
View File
@@ -44,13 +44,13 @@ const MAX_VISIBLE_LABELS = 3;
export function StackRow(props: StackRowProps) {
const {
file, displayName, status, isBusy, isActive, isPaid, labels,
file, displayName, status, isBusy, isActive, labels,
hasUpdate, hasGitPending, onSelect, kebabSlot,
bulkMode = false, isSelected = false, onToggleSelect,
} = props;
const visibleLabels = isPaid ? labels.slice(0, MAX_VISIBLE_LABELS) : [];
const overflowCount = isPaid ? Math.max(0, labels.length - MAX_VISIBLE_LABELS) : 0;
const visibleLabels = labels.slice(0, MAX_VISIBLE_LABELS);
const overflowCount = Math.max(0, labels.length - MAX_VISIBLE_LABELS);
const handleClick = () => {
if (bulkMode) onToggleSelect?.(file);
@@ -73,4 +73,13 @@ describe('StackRow', () => {
expect(container.querySelector('.animate-spin')).not.toBeNull();
expect(screen.queryByText('UP')).not.toBeInTheDocument();
});
it('renders label indicators on community tier', () => {
const labels: Label[] = [
{ id: 1, node_id: 0, name: 'prod', color: 'teal' },
{ id: 2, node_id: 0, name: 'media', color: 'blue' },
];
const { container } = render(<StackRow {...base({ isPaid: false, labels })} />);
expect(container.querySelectorAll('[style*="--label-"]')).toHaveLength(2);
});
});
@@ -95,6 +95,17 @@ describe('useStackMenuItems', () => {
expect(inspect.items.find(i => i.id === 'auto-update')).toBeUndefined();
});
it('keeps label assignment available when !isPaid', () => {
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({
isPaid: false,
labels: [{ id: 1, node_id: 0, name: 'prod', color: 'teal' }],
})));
const organize = result.current.find(g => g.id === 'organize')!;
const labelsItem = organize.items.find(i => i.id === 'labels');
expect(labelsItem).toBeDefined();
expect(labelsItem?.subItems?.[0]).toMatchObject({ id: 'label:1', label: 'prod' });
});
it('auto-update toggle calls setAutoUpdateEnabled with toggled value', () => {
const setAutoUpdateEnabled = vi.fn();
const { result } = renderHook(() =>
+12 -14
View File
@@ -48,21 +48,19 @@ export function useStackMenuItems(_file: string, ctx: StackMenuCtx): MenuGroup[]
groups.push({ id: 'inspect', items: inspect });
const organize: MenuItem[] = [];
if (isPaid) {
organize.push({
id: 'labels',
label: 'Labels',
organize.push({
id: 'labels',
label: 'Labels',
icon: Tag,
shortcut: 'L ',
onSelect: () => {},
subItems: labels.map(l => ({
id: `label:${l.id}`,
label: l.name,
icon: Tag,
shortcut: 'L ',
onSelect: () => {},
subItems: labels.map(l => ({
id: `label:${l.id}`,
label: l.name,
icon: Tag,
onSelect: () => toggleLabel(l.id),
})),
});
}
onSelect: () => toggleLabel(l.id),
})),
});
organize.push(
isPinned
? { id: 'pin', label: 'Unpin', icon: PinOff, shortcut: 'P', onSelect: unpin }