mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-22 19:56:01 +00:00
workloads(columns): let explicit toggles override responsive column gating
Below the 1440px wide breakpoint the workloads table layout-gated IP, Node, Tags, OS, Net I/O, and Disk I/O with no recourse: the columns menu still offered the toggles but flipping them changed nothing, and v5's horizontal-scroll fallback was gone, so a 1280px laptop simply could not see guest tags or I/O rates. Flagged by the Proxmox overview parity audit; applies to every workloads surface. - Track explicitly pinned columns per storage scope (workloadsHiddenColumns[:scope]:forced); pinned columns render even when the breakpoint gates them off and the table wrapper's existing overflow-x-auto picks up the slack. - Menu checkboxes now mirror what the table actually renders: a layout-gated column reads as hidden, the first toggle pins it, the second unpins it back to the layout default (without poisoning the user-hidden set for wide viewports). - Column reset clears pinned columns along with hidden ones.
This commit is contained in:
@@ -30,6 +30,13 @@ const setWideViewport = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const setCompactViewport = () => {
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
configurable: true,
|
||||
value: 1280,
|
||||
});
|
||||
};
|
||||
|
||||
describe('useWorkloadsControlsState', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
@@ -38,6 +45,53 @@ describe('useWorkloadsControlsState', () => {
|
||||
setWideViewport();
|
||||
});
|
||||
|
||||
it('lets an explicit toggle pin a layout-hidden column into the compact table', () => {
|
||||
setCompactViewport();
|
||||
createRoot((dispose) => {
|
||||
try {
|
||||
const [showFilters, setShowFilters] = createSignal(false);
|
||||
const state = useWorkloadsControlsState({
|
||||
viewMode: () => 'all' as ViewMode,
|
||||
showFilters,
|
||||
setShowFilters,
|
||||
});
|
||||
const menu = state.workloadsFilterColumnVisibility();
|
||||
|
||||
// netIo is layout-gated to the wide breakpoint: invisible at 1280px
|
||||
// and reported hidden by the menu even though the user never hid it.
|
||||
expect(state.workloadTableVisibleColumnIds()).not.toContain('netIo');
|
||||
expect(menu.isColumnHidden('netIo')).toBe(true);
|
||||
|
||||
// First toggle pins it into view instead of flipping a flag the user
|
||||
// cannot see the effect of.
|
||||
menu.onColumnToggle('netIo');
|
||||
expect(state.workloadTableVisibleColumnIds()).toContain('netIo');
|
||||
expect(menu.isColumnHidden('netIo')).toBe(false);
|
||||
expect(state.columnVisibility.hiddenColumns()).not.toContain('netIo');
|
||||
|
||||
// Second toggle unpins it; the column returns to its layout default
|
||||
// rather than becoming user-hidden on wide viewports too.
|
||||
menu.onColumnToggle('netIo');
|
||||
expect(state.workloadTableVisibleColumnIds()).not.toContain('netIo');
|
||||
expect(state.columnVisibility.hiddenColumns()).not.toContain('netIo');
|
||||
|
||||
// Layout-visible columns keep the plain hide/show semantics.
|
||||
menu.onColumnToggle('backup');
|
||||
expect(state.columnVisibility.hiddenColumns()).toContain('backup');
|
||||
menu.onColumnToggle('backup');
|
||||
expect(state.columnVisibility.hiddenColumns()).not.toContain('backup');
|
||||
|
||||
// Reset clears pinned columns along with user-hidden ones.
|
||||
menu.onColumnToggle('netIo');
|
||||
expect(state.workloadTableVisibleColumnIds()).toContain('netIo');
|
||||
menu.onColumnReset();
|
||||
expect(state.workloadTableVisibleColumnIds()).not.toContain('netIo');
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('lets Docker scope use a container-native column profile without hiding disk globally', () => {
|
||||
createRoot((dispose) => {
|
||||
try {
|
||||
|
||||
@@ -194,10 +194,52 @@ export function useWorkloadsControlsState(options: WorkloadsControlsStateOptions
|
||||
['aiContext'],
|
||||
);
|
||||
|
||||
const visibleColumns = columnVisibility.visibleColumns;
|
||||
const workloadTableVisibleColumns = createMemo(() =>
|
||||
getWorkloadVisibleColumnsForLayout(visibleColumns(), workloadTableLayoutMode()),
|
||||
// Columns the user explicitly pinned into view while the responsive layout
|
||||
// had them gated off. Without this, the columns menu offered toggles (IP,
|
||||
// Tags, Net I/O, ...) that did nothing below the wide breakpoint, and
|
||||
// sub-1440px viewports had no path to data v5 reached via horizontal
|
||||
// scroll. Explicit intent beats the breakpoint default; the table wrapper
|
||||
// scrolls horizontally when the pinned columns no longer fit.
|
||||
const [forcedColumnIds, setForcedColumnIds] = usePersistentSignal<string[]>(
|
||||
`${columnStorageKey}:forced`,
|
||||
[],
|
||||
{
|
||||
serialize: (arr) => JSON.stringify(arr),
|
||||
deserialize: (str) => {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(str);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed.filter((value): value is string => typeof value === 'string');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
const forcedColumnIdSet = createMemo(() => new Set(forcedColumnIds()));
|
||||
const layoutHiddenColumnIds = createMemo(() => {
|
||||
const layoutVisible = new Set(
|
||||
getWorkloadVisibleColumnsForLayout(workloadColumns, workloadTableLayoutMode()).map(
|
||||
(column) => column.id,
|
||||
),
|
||||
);
|
||||
return new Set(
|
||||
workloadColumns.filter((column) => !layoutVisible.has(column.id)).map((c) => c.id),
|
||||
);
|
||||
});
|
||||
|
||||
const visibleColumns = columnVisibility.visibleColumns;
|
||||
const workloadTableVisibleColumns = createMemo(() => {
|
||||
const layoutVisible = new Set(
|
||||
getWorkloadVisibleColumnsForLayout(visibleColumns(), workloadTableLayoutMode()).map(
|
||||
(column) => column.id,
|
||||
),
|
||||
);
|
||||
const forced = forcedColumnIdSet();
|
||||
return visibleColumns().filter(
|
||||
(column) => layoutVisible.has(column.id) || forced.has(column.id),
|
||||
);
|
||||
});
|
||||
const workloadTableVisibleColumnIds = createMemo(() =>
|
||||
workloadTableVisibleColumns().map((column) => column.id),
|
||||
);
|
||||
@@ -277,11 +319,40 @@ export function useWorkloadsControlsState(options: WorkloadsControlsStateOptions
|
||||
}
|
||||
};
|
||||
|
||||
// Menu checkbox state mirrors what the table actually renders: a column the
|
||||
// layout gates off reads as hidden even when the user never hid it. Toggling
|
||||
// such a column pins it into view (or unpins it) instead of flipping a
|
||||
// user-hidden flag the user cannot see the effect of.
|
||||
const isColumnHiddenForMenu = (id: string): boolean =>
|
||||
columnVisibility.isHiddenByUser(id) ||
|
||||
(layoutHiddenColumnIds().has(id) && !forcedColumnIdSet().has(id));
|
||||
|
||||
const handleColumnToggle = (id: string): void => {
|
||||
if (!layoutHiddenColumnIds().has(id)) {
|
||||
setForcedColumnIds(forcedColumnIds().filter((existing) => existing !== id));
|
||||
columnVisibility.toggle(id);
|
||||
return;
|
||||
}
|
||||
if (forcedColumnIdSet().has(id)) {
|
||||
setForcedColumnIds(forcedColumnIds().filter((existing) => existing !== id));
|
||||
return;
|
||||
}
|
||||
setForcedColumnIds([...forcedColumnIds(), id]);
|
||||
if (columnVisibility.isHiddenByUser(id)) {
|
||||
columnVisibility.toggle(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleColumnReset = (): void => {
|
||||
setForcedColumnIds([]);
|
||||
columnVisibility.resetToDefaults();
|
||||
};
|
||||
|
||||
const workloadsFilterColumnVisibility = createMemo(() => ({
|
||||
availableColumns: columnVisibility.availableToggles(),
|
||||
isColumnHidden: columnVisibility.isHiddenByUser,
|
||||
onColumnToggle: columnVisibility.toggle,
|
||||
onColumnReset: columnVisibility.resetToDefaults,
|
||||
isColumnHidden: isColumnHiddenForMenu,
|
||||
onColumnToggle: handleColumnToggle,
|
||||
onColumnReset: handleColumnReset,
|
||||
}));
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user