mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 07:25:44 +02:00
fix(permissions): self-heal Administrators to ALL_PERMISSIONS on upgrade + Pipelines runs dashboard polish
Administrators system group sync - Fresh installs already bootstrap with ALL_PERMISSIONS, so they always have every permission. Upgrades previously only got what one-off backfill blocks in seed_default_groups() explicitly listed (library:purge, archives:purge, the OWN/ALL read-flag block, orca_cloud:auth, pipelines:*). Any Permission enum member added without a matching block silently stayed missing on existing admin rows. The most recent gap was printer_sensor_history:read (Sensor History charts returned 403 for upgraded admins). - seed_default_groups() now syncs Administrators to ALL_PERMISSIONS on every startup: append every Permission value that isn't already on the row. Additive only -- hand-added custom permissions are preserved. - The pure-admin one-off backfills (library:purge / archives:purge block, the OWN/ALL + orca_cloud:auth + legacy-read-flag block, the Administrators branch of the pipeline backfill) are retired since the sync subsumes them. Non-admin backfills (Operators / Viewers OWN-tier reads, Operators orca_cloud:auth, pipelines for non-admin groups, makerworld:*, clear_plate cross-group adders) are untouched. - Tests: test_administrators_printer_sensor_history_read_backfilled (regression for the reported gap), test_administrators_sync_covers_every_current_permission (generic invariant -- any future new permission lands on admin without needing a one-off test), test_administrators_sync_is_additive_only (custom permissions preserved). 12/12 backfill-migration + 102/102 broader permission tests green; ruff clean. Pipelines runs dashboard - PipelineRunsPage.tsx: the Pipeline / Status / Target filter row's three native <select> elements are replaced with a bambu-themed FilterDropdown (button trigger, floating menu, optgroup-style headers for the Target picker, hover + selected states with a check mark, closes on outside click and Escape). Same value/onChange contract -- visual only. - SlicerPipelinesPanel.tsx: wrap list?.pipelines ?? [] in useMemo so the reference is stable when the data is stable. Fixes the react-hooks/exhaustive-deps warning where the inline fallback returned a fresh empty array every render, invalidating both downstream useMemo caches (target-options + filtered-pipelines list).
This commit is contained in:
@@ -9,7 +9,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from '../utils';
|
||||
import { PipelineRunsPage } from '../../pages/PipelineRunsPage';
|
||||
import { PipelineRunsView } from '../../pages/PipelineRunsPage';
|
||||
import { api, type PipelineRun } from '../../api/client';
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
@@ -88,7 +88,7 @@ function makeRun(overrides: Partial<PipelineRun> = {}): PipelineRun {
|
||||
};
|
||||
}
|
||||
|
||||
describe('PipelineRunsPage', () => {
|
||||
describe('PipelineRunsView', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockApi.listSlicerPipelines.mockResolvedValue({ pipelines: [] });
|
||||
@@ -96,7 +96,7 @@ describe('PipelineRunsPage', () => {
|
||||
});
|
||||
|
||||
it('renders empty state when no runs exist', async () => {
|
||||
render(<PipelineRunsPage />);
|
||||
render(<PipelineRunsView />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/No pipeline runs yet/i)).toBeInTheDocument();
|
||||
});
|
||||
@@ -104,7 +104,7 @@ describe('PipelineRunsPage', () => {
|
||||
|
||||
it('lists runs and surfaces the pipeline name + status chip', async () => {
|
||||
mockApi.listAllPipelineRuns.mockResolvedValue({ runs: [makeRun()], total: 1 });
|
||||
render(<PipelineRunsPage />);
|
||||
render(<PipelineRunsView />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Production Batch/i)).toBeInTheDocument();
|
||||
// Status chip uses the i18n key.
|
||||
@@ -115,7 +115,7 @@ describe('PipelineRunsPage', () => {
|
||||
it('shows a Cancel button on in-flight runs and fires the mutation', async () => {
|
||||
mockApi.listAllPipelineRuns.mockResolvedValue({ runs: [makeRun()], total: 1 });
|
||||
mockApi.cancelPipelineRun.mockResolvedValue(makeRun({ status: 'cancelled' }));
|
||||
render(<PipelineRunsPage />);
|
||||
render(<PipelineRunsView />);
|
||||
const user = userEvent.setup();
|
||||
await waitFor(() => expect(screen.getByText(/Production Batch/i)).toBeInTheDocument());
|
||||
await user.click(screen.getByRole('button', { name: /Cancel/i }));
|
||||
@@ -135,7 +135,7 @@ describe('PipelineRunsPage', () => {
|
||||
total: 1,
|
||||
});
|
||||
mockApi.retryFailedPipelineRun.mockResolvedValue(makeRun({ id: 2, parent_run_id: 1, copies: 2 }));
|
||||
render(<PipelineRunsPage />);
|
||||
render(<PipelineRunsView />);
|
||||
const user = userEvent.setup();
|
||||
await waitFor(() => expect(screen.getByText(/Production Batch/i)).toBeInTheDocument());
|
||||
await user.click(screen.getByRole('button', { name: /Retry failed/i }));
|
||||
@@ -144,7 +144,7 @@ describe('PipelineRunsPage', () => {
|
||||
|
||||
it('expands the row to show per-copy jobs', async () => {
|
||||
mockApi.listAllPipelineRuns.mockResolvedValue({ runs: [makeRun()], total: 1 });
|
||||
render(<PipelineRunsPage />);
|
||||
render(<PipelineRunsView />);
|
||||
const user = userEvent.setup();
|
||||
await waitFor(() => expect(screen.getByText(/Production Batch/i)).toBeInTheDocument());
|
||||
await user.click(screen.getByRole('button', { name: /Expand/i }));
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('SettingsPage', () => {
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
SIDEBAR_ORDER_KEY,
|
||||
JSON.stringify(['ext-7', 'printers', 'inventory', 'archives', 'queue', 'pipelineRuns', 'projects', 'files', 'makerworld', 'profiles', 'maintenance', 'stats', 'settings']),
|
||||
JSON.stringify(['ext-7', 'printers', 'inventory', 'archives', 'queue', 'projects', 'files', 'makerworld', 'profiles', 'maintenance', 'stats', 'settings']),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -345,7 +345,7 @@ describe('SettingsPage', () => {
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(SIDEBAR_HIDDEN_SYSTEM_ITEMS_KEY, JSON.stringify([]));
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(
|
||||
SIDEBAR_ORDER_KEY,
|
||||
JSON.stringify(['printers', 'inventory', 'archives', 'queue', 'pipelineRuns', 'projects', 'files', 'makerworld', 'profiles', 'maintenance', 'stats', 'settings', 'ext-7']),
|
||||
JSON.stringify(['printers', 'inventory', 'archives', 'queue', 'projects', 'files', 'makerworld', 'profiles', 'maintenance', 'stats', 'settings', 'ext-7']),
|
||||
);
|
||||
|
||||
const settingsRow = screen.getAllByText('Settings')
|
||||
@@ -410,7 +410,6 @@ describe('SettingsPage', () => {
|
||||
'inventory',
|
||||
'archives',
|
||||
'queue',
|
||||
'pipelineRuns',
|
||||
'projects',
|
||||
'files',
|
||||
'makerworld',
|
||||
|
||||
Reference in New Issue
Block a user