mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Fix Proxmox LXC workload columns
This commit is contained in:
parent
e2e8b72baa
commit
202f3b2f4c
9 changed files with 122 additions and 31 deletions
|
|
@ -252,7 +252,11 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant.
|
|||
`useWorkloadsState` must apply them before dedupe, filter-option
|
||||
derivation, summary statistics, and row rendering so a platform overview
|
||||
that demotes nested workloads does not carry hidden rows through downstream
|
||||
table state.
|
||||
table state. The shared workload column profile must consume those same
|
||||
exclusions. When the public combined `container` view has only system or
|
||||
application containers remaining, it must resolve to that subtype's
|
||||
canonical column set instead of exposing empty runtime-specific columns;
|
||||
page-local column suppression and duplicate platform schemas are forbidden.
|
||||
Responsive workload columns are part of the same table hot-path and
|
||||
usability contract. The active width stage must filter both the rendered
|
||||
column set and the column picker's available choices; a remembered user
|
||||
|
|
|
|||
|
|
@ -1,25 +1,23 @@
|
|||
{
|
||||
"version": 1,
|
||||
"base_sha": "e7de6602f7d43052950fb72d9591ecf154090f10",
|
||||
"verified_at": "2026-08-19T19:07:24Z",
|
||||
"base_sha": "e2e8b72baa56b60107fb8bc694a27394cd76c8c6",
|
||||
"verified_at": "2026-08-19T19:21:10Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/shared/FilterButtonGroup.tsx",
|
||||
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx",
|
||||
"frontend-modern/src/index.css"
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsState.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/shared/FilterButtonGroup.tsx": "4f7c115ad13ad614f5ebb3f73d2c0f7561342e2a6b58b2bde36cf11d828d54ca",
|
||||
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "c946582423cf0f95e3e8f92d676c175a51c2f474be70ed383f882b7ef30e8973",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "01ea9d0ec46b0cb97870aa145b17fdd1fcc4fd8b3576ffdd21dfd2f4ad50e5c7",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "66fd5ef686cd052d37fa59ceacdae524a9274fd12e1f22f4c2be0e1ca80f5ce4",
|
||||
"frontend-modern/src/index.css": "d8fe7b2c49b50003ab493cd37e37bbdd5c637764c1efd23fa20b966550c7ba5f"
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx": "ace9b2dfb177850527b1719792098c33fd7fcf9fd57bb2ac7aacde03b17aae16",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts": "f5f5b87a6ebf6d4ad71bfb60bfeeb796b29a7e9a989d111128997efc16c0f8a7",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts": "00d3ce0ed568326f1a6b96c2d9e1efeba84f8abd3df795bc19ad498e6ec55702",
|
||||
"frontend-modern/src/components/Workloads/useWorkloadsState.ts": "9921577a647d6d617ab2b66df7c6b8f35dc93a72ae4273a869e01743a92c414f"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/overview",
|
||||
"/vmware/overview"
|
||||
"/proxmox/overview?type=container"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
|
|
@ -32,18 +30,17 @@
|
|||
}
|
||||
],
|
||||
"states": [
|
||||
"Proxmox desktop default bounded node preview with eight rows, visible workload controls, and the workload table in the initial viewport",
|
||||
"Proxmox desktop expanded 32-node estate and restored eight-row collapsed preview",
|
||||
"Proxmox phone default bounded node preview with four rows and the reveal control visible",
|
||||
"Proxmox phone expanded 32-node estate and restored four-row collapsed preview without horizontal overflow",
|
||||
"Proxmox inventory totals shown and hidden across filter counts, Nodes topology, and the node reveal label",
|
||||
"Proxmox and vSphere 12px labels with 11px counts sharing identical 16px line boxes and baselines"
|
||||
"Proxmox desktop All workload view with the shared mixed-workload column profile",
|
||||
"Proxmox desktop LXCs workload view narrowed to the system-container Name, ID, CPU, Mem, Disk, Uptime, and Backup columns",
|
||||
"Proxmox desktop LXC Columns disclosure without Runtime, Image, Context, or Update choices",
|
||||
"Reloaded Proxmox LXC route preserving the selected type and narrowed column profile",
|
||||
"Proxmox narrow LXC view with the responsive Name, CPU, Mem, Disk, and Age columns",
|
||||
"Desktop and narrow document and table scroll widths equal to their client widths"
|
||||
],
|
||||
"interactions": [
|
||||
"Expanded and collapsed Show all 32 nodes on desktop and phone",
|
||||
"Switched Proxmox workload status from All to Attention and observed the adjacent workload table change from 150 rendered rows to 21",
|
||||
"Hid and restored Inventory totals from the View menu and confirmed the node reveal label stopped exposing the total while hidden",
|
||||
"Compared the final shared inline count geometry on Proxmox and vSphere",
|
||||
"Cleared the temporary Proxmox status filter and left /proxmox/overview at 1440x900"
|
||||
"Switched from All to LXCs and confirmed the column profile narrowed without changing the public type=container URL",
|
||||
"Opened View and its Columns disclosure and confirmed application-container fields were absent",
|
||||
"Dismissed the open View menu with Escape and confirmed focus returned to the View trigger",
|
||||
"Reloaded the selected LXC route and resized between 1440x900 and 390x844 while retaining the corrected profile"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ import {
|
|||
getWorkloadTableLayoutMode,
|
||||
getWorkloadTableLayoutModeForContainer,
|
||||
getWorkloadVisibleColumnsForLayout,
|
||||
resolveWorkloadColumnViewMode,
|
||||
type WorkloadIOEmphasis,
|
||||
} from '../guestRowModel';
|
||||
|
||||
|
|
@ -1355,6 +1356,16 @@ describe('VIEW_MODE_COLUMNS', () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('narrows the combined container column profile when a subtype is excluded', () => {
|
||||
expect(resolveWorkloadColumnViewMode('container', ['app-container'])).toBe('system-container');
|
||||
expect(resolveWorkloadColumnViewMode('container', ['system-container'])).toBe('app-container');
|
||||
expect(resolveWorkloadColumnViewMode('container')).toBe('container');
|
||||
expect(resolveWorkloadColumnViewMode('container', ['system-container', 'app-container'])).toBe(
|
||||
'container',
|
||||
);
|
||||
expect(resolveWorkloadColumnViewMode('vm', ['app-container'])).toBe('vm');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOutlierEmphasis (via I/O column rendering)', () => {
|
||||
|
|
|
|||
|
|
@ -1078,7 +1078,7 @@ describe('Workloads performance contract', () => {
|
|||
nodes={[]}
|
||||
useWorkloads
|
||||
forcedPlatform="proxmox-pve"
|
||||
forcedViewMode="all"
|
||||
forcedViewMode="container"
|
||||
excludedWorkloadTypes={['app-container']}
|
||||
showNestedExcludedWorkloads
|
||||
/>
|
||||
|
|
@ -1094,6 +1094,12 @@ describe('Workloads performance contract', () => {
|
|||
).toBeNull();
|
||||
expect(screen.queryByTestId('nested-workload-context-row')).toBeNull();
|
||||
expect(screen.getByTestId('guest-row-nested-cue-pve-lxc')).toHaveTextContent('Docker 1');
|
||||
expect(screen.getByRole('columnheader', { name: 'ID' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('columnheader', { name: 'Backup' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('columnheader', { name: 'Runtime' })).toBeNull();
|
||||
expect(screen.queryByRole('columnheader', { name: 'Image' })).toBeNull();
|
||||
expect(screen.queryByRole('columnheader', { name: 'Context' })).toBeNull();
|
||||
expect(screen.queryByRole('columnheader', { name: 'Update' })).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByTestId('guest-row-toggle-pve-lxc'));
|
||||
expect(await screen.findByTestId('guest-drawer')).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -238,6 +238,52 @@ describe('useWorkloadsControlsState', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('uses the remaining container subtype for columns when a platform excludes the other subtype', () => {
|
||||
createRoot((dispose) => {
|
||||
try {
|
||||
const [showFilters, setShowFilters] = createSignal(false);
|
||||
const proxmoxState = useWorkloadsControlsState({
|
||||
viewMode: () => 'container' as ViewMode,
|
||||
excludedWorkloadTypes: ['app-container'],
|
||||
showFilters,
|
||||
setShowFilters,
|
||||
});
|
||||
|
||||
const visibleColumnIds = proxmoxState.visibleColumns().map((column) => column.id);
|
||||
expect(visibleColumnIds).toContain('vmid');
|
||||
expect(visibleColumnIds).toContain('backup');
|
||||
expect(visibleColumnIds).not.toContain('runtime');
|
||||
expect(visibleColumnIds).not.toContain('image');
|
||||
expect(visibleColumnIds).not.toContain('context');
|
||||
expect(visibleColumnIds).not.toContain('update');
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
createRoot((dispose) => {
|
||||
try {
|
||||
const [showFilters, setShowFilters] = createSignal(false);
|
||||
const appContainerState = useWorkloadsControlsState({
|
||||
viewMode: () => 'container' as ViewMode,
|
||||
excludedWorkloadTypes: ['system-container'],
|
||||
showFilters,
|
||||
setShowFilters,
|
||||
});
|
||||
|
||||
const visibleColumnIds = appContainerState.visibleColumns().map((column) => column.id);
|
||||
expect(visibleColumnIds).toContain('runtime');
|
||||
expect(visibleColumnIds).toContain('image');
|
||||
expect(visibleColumnIds).toContain('context');
|
||||
expect(visibleColumnIds).toContain('update');
|
||||
expect(visibleColumnIds).not.toContain('vmid');
|
||||
expect(visibleColumnIds).not.toContain('backup');
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// The Backup column reads only `resource.proxmox.lastBackup`, so scopes that
|
||||
// opt into hiding it (vSphere) rendered "None" on every row for anyone whose
|
||||
// saved preference predates that default. The one-time migration retires that
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
getPlatformTableWeightedColumnWidthStyle,
|
||||
PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import type { WorkloadGuest, ViewMode } from '@/types/workloads';
|
||||
import type { WorkloadGuest, WorkloadType, ViewMode } from '@/types/workloads';
|
||||
import { createVisibleCanonicalTypeColumn } from '@/utils/typeColumnDefinition';
|
||||
import type {
|
||||
WorkloadMetricHistoryReader,
|
||||
|
|
@ -705,3 +705,22 @@ export const VIEW_MODE_COLUMNS: Record<ViewMode, Set<string> | null> = {
|
|||
]),
|
||||
pod: new Set(['name', 'cpu', 'memory', 'image', 'namespace', 'context', 'aiContext']),
|
||||
};
|
||||
|
||||
/**
|
||||
* The public `container` view intentionally covers both system containers and
|
||||
* application containers. Platform-owned surfaces can exclude either subtype;
|
||||
* in that case the column profile must narrow with the same canonical boundary
|
||||
* instead of exposing fields that every remaining row renders empty.
|
||||
*/
|
||||
export const resolveWorkloadColumnViewMode = (
|
||||
viewMode: ViewMode,
|
||||
excludedWorkloadTypes: readonly WorkloadType[] = [],
|
||||
): ViewMode => {
|
||||
if (viewMode !== 'container') return viewMode;
|
||||
|
||||
const includesSystemContainers = !excludedWorkloadTypes.includes('system-container');
|
||||
const includesAppContainers = !excludedWorkloadTypes.includes('app-container');
|
||||
|
||||
if (includesSystemContainers === includesAppContainers) return viewMode;
|
||||
return includesSystemContainers ? 'system-container' : 'app-container';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
|||
import { blurFocusedTypeToSearch } from '@/hooks/useTypeToSearch';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import { aiChatStore } from '@/stores/aiChat';
|
||||
import type { ViewMode } from '@/types/workloads';
|
||||
import type { ViewMode, WorkloadType } from '@/types/workloads';
|
||||
|
||||
import {
|
||||
GUEST_COLUMNS,
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
getWorkloadTableLayoutModeForContainer,
|
||||
getWorkloadTableLayoutMode,
|
||||
getWorkloadVisibleColumnsForLayout,
|
||||
resolveWorkloadColumnViewMode,
|
||||
} from './guestRowModel';
|
||||
import {
|
||||
DEFAULT_WORKLOADS_SORT_DIRECTION,
|
||||
|
|
@ -52,6 +53,7 @@ interface WorkloadsControlsStateOptions {
|
|||
// empty cell on every row. Omitted means "assume data exists".
|
||||
hasAvailabilityData?: Accessor<boolean>;
|
||||
columnLabelOverrides?: Partial<Record<string, string>>;
|
||||
excludedWorkloadTypes?: readonly WorkloadType[];
|
||||
setShowFilters: (value: boolean | ((current: boolean) => boolean)) => void;
|
||||
showFilters: Accessor<boolean>;
|
||||
viewMode: Accessor<ViewMode>;
|
||||
|
|
@ -209,7 +211,11 @@ export function useWorkloadsControlsState(options: WorkloadsControlsStateOptions
|
|||
);
|
||||
|
||||
const relevantColumns = createMemo(() => {
|
||||
const base = VIEW_MODE_COLUMNS[options.viewMode()];
|
||||
const columnViewMode = resolveWorkloadColumnViewMode(
|
||||
options.viewMode(),
|
||||
options.excludedWorkloadTypes,
|
||||
);
|
||||
const base = VIEW_MODE_COLUMNS[columnViewMode];
|
||||
const dropNode = effectiveGroupingMode() === 'grouped';
|
||||
const dropAvailability = !hasAvailabilityData();
|
||||
if (!base) {
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export function useWorkloadsDerivedState(options: WorkloadsWorkloadDerivedStateO
|
|||
});
|
||||
|
||||
const totalStats = createMemo(() => computeWorkloadStats(filteredGuests()));
|
||||
const inventoryStats = createMemo(() => computeWorkloadStats(options.allGuests()));
|
||||
const workloadIOEmphasis = createMemo(() => computeWorkloadIOEmphasis(filteredGuests()));
|
||||
|
||||
return {
|
||||
|
|
@ -171,6 +172,7 @@ export function useWorkloadsDerivedState(options: WorkloadsWorkloadDerivedStateO
|
|||
groupedGuests,
|
||||
groupedWindowing,
|
||||
guestParentNodeMap,
|
||||
inventoryStats,
|
||||
nodeByInstance,
|
||||
topSpacerHeight,
|
||||
totalStats,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import { getCanonicalWorkloadId, resolveWorkloadType } from '@/utils/workloads';
|
|||
import { nodeFromResource } from '@/utils/resourceStateAdapters';
|
||||
import {
|
||||
buildWorkloadSummaryGroupScopeMap,
|
||||
computeWorkloadStats,
|
||||
createWorkloadSortComparator,
|
||||
filterWorkloads,
|
||||
type FilterWorkloadsParams,
|
||||
|
|
@ -314,6 +313,7 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) {
|
|||
onMetricHistoryRangeChange: props.onMetricHistoryRangeChange,
|
||||
columnVisibilityStorageScope: props.columnVisibilityStorageScope,
|
||||
additionalDefaultHiddenColumnIds: props.additionalDefaultHiddenColumnIds,
|
||||
excludedWorkloadTypes: props.excludedWorkloadTypes,
|
||||
hasAvailabilityData,
|
||||
columnLabelOverrides: props.columnLabelOverrides,
|
||||
layoutWidth: props.layoutWidth,
|
||||
|
|
@ -494,6 +494,7 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) {
|
|||
groupedGuests,
|
||||
groupedWindowing,
|
||||
guestParentNodeMap,
|
||||
inventoryStats,
|
||||
nodeByInstance,
|
||||
topSpacerHeight,
|
||||
totalStats,
|
||||
|
|
@ -513,7 +514,6 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) {
|
|||
tableBodyRef,
|
||||
groupLabelBadges,
|
||||
});
|
||||
const inventoryStats = createMemo(() => computeWorkloadStats(allGuests()));
|
||||
|
||||
return {
|
||||
activeAlerts,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue