mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Enable sorting for workload IDs
Some checks are pending
Build and Test / Backend tests (rest-0) (push) Blocked by required conditions
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Detect changed areas (push) Waiting to run
Build and Test / Frontend (push) Blocked by required conditions
Build and Test / Backend tests (api) (push) Blocked by required conditions
Build and Test / Backend tests (rest-1) (push) Blocked by required conditions
Build and Test / Script smoke tests & backend build (push) Blocked by required conditions
Build and Test / Benchmarks (push) Blocked by required conditions
Canonical Governance / governance (push) Waiting to run
Public docs / check (push) Waiting to run
Core E2E Tests / Validate E2E tier selection (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 2/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 3/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 4/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 5/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 6/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 7/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 8/8) (push) Blocked by required conditions
Core E2E Tests / Agent registration lifecycle (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
Unified Agent Native Verification / macOS ARM64 (push) Waiting to run
Unified Agent Native Verification / macOS Intel (push) Waiting to run
Unified Agent Native Verification / FreeBSD cross-build contract (push) Waiting to run
Unified Agent Native Verification / Linux ARM64 (push) Waiting to run
Unified Agent Native Verification / Linux x64 (push) Waiting to run
Unified Agent Native Verification / Windows x64 (push) Waiting to run
Some checks are pending
Build and Test / Backend tests (rest-0) (push) Blocked by required conditions
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Detect changed areas (push) Waiting to run
Build and Test / Frontend (push) Blocked by required conditions
Build and Test / Backend tests (api) (push) Blocked by required conditions
Build and Test / Backend tests (rest-1) (push) Blocked by required conditions
Build and Test / Script smoke tests & backend build (push) Blocked by required conditions
Build and Test / Benchmarks (push) Blocked by required conditions
Canonical Governance / governance (push) Waiting to run
Public docs / check (push) Waiting to run
Core E2E Tests / Validate E2E tier selection (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 2/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 3/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 4/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 5/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 6/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 7/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 8/8) (push) Blocked by required conditions
Core E2E Tests / Agent registration lifecycle (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
Unified Agent Native Verification / macOS ARM64 (push) Waiting to run
Unified Agent Native Verification / macOS Intel (push) Waiting to run
Unified Agent Native Verification / FreeBSD cross-build contract (push) Waiting to run
Unified Agent Native Verification / Linux ARM64 (push) Waiting to run
Unified Agent Native Verification / Linux x64 (push) Waiting to run
Unified Agent Native Verification / Windows x64 (push) Waiting to run
This commit is contained in:
parent
3b866458f6
commit
ad4c82c34e
9 changed files with 135 additions and 36 deletions
|
|
@ -2486,6 +2486,12 @@ the canonical owners for desktop and mobile workload column sizing. Global CSS
|
|||
must not reintroduce competing `.workload-table [data-workload-col=…]` width
|
||||
rules or `min-width: max-content` fallbacks that can blow the table out
|
||||
horizontally on Firefox or other desktop browsers.
|
||||
The mixed Workloads `Info`/compact `ID` column must also remain sortable by the
|
||||
exact scalar rendered for each workload kind. `guestRowModel.tsx` owns that
|
||||
projection and `workloadSelectors.ts` owns its natural ordering, including
|
||||
numeric VM IDs, text fallbacks for images and namespaces, and empty values
|
||||
remaining last. Header code must consume the column model's `sortKey` instead
|
||||
of adding a second display-specific sorting path.
|
||||
The same `internal/api/router.go` payload boundary also keeps the
|
||||
will_fix_later remind-at deadline scoped to a single optional pointer
|
||||
(`*time.Time`) per finding on both API write paths, so adding the
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
{
|
||||
"version": 1,
|
||||
"base_sha": "af370a8dda01852586e6462b3208a7d822ee1261",
|
||||
"verified_at": "2026-08-26T09:08:45Z",
|
||||
"base_sha": "3b866458f665ff7be80e611cc60286d622a66e92",
|
||||
"verified_at": "2026-08-26T09:20:54Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/Workloads/GuestRow.tsx",
|
||||
"frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx"
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx",
|
||||
"frontend-modern/src/components/Workloads/useGuestRowState.ts",
|
||||
"frontend-modern/src/components/Workloads/workloadSelectors.ts",
|
||||
"frontend-modern/src/components/Workloads/workloadsFilterModel.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/Workloads/GuestRow.tsx": "c1886d219bb44a9560a93e3227e022e370d0df7e3e396fa6e69ac4d982f275e2",
|
||||
"frontend-modern/src/components/Workloads/WorkloadTableHeader.tsx": "9ed1b7fe10646efa7ec366a2ceca0c69fe30e77aff9334d2440ca6ede3f65fb8"
|
||||
"frontend-modern/src/components/Workloads/guestRowModel.tsx": "e5bdf0f4bcb070227c7c40d512863eaff1bcb80db53c19d59a9b9085b648d344",
|
||||
"frontend-modern/src/components/Workloads/useGuestRowState.ts": "591c98dbdb3213636ae5465f65fff4df03882ef428b57589c33eec36dc22fb85",
|
||||
"frontend-modern/src/components/Workloads/workloadSelectors.ts": "98fe0f042d5e114f5b3b99080cc894e7da99d490f208ce6cd94867aa5a8aaff3",
|
||||
"frontend-modern/src/components/Workloads/workloadsFilterModel.ts": "085dbf2b61d3cd87aec04b54aa3fcc15f0a12d3d70c4014830c45a807788cb79"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/overview"
|
||||
|
|
@ -26,16 +30,15 @@
|
|||
],
|
||||
"states": [
|
||||
"loaded grouped guest table with running and stopped VM and LXC rows",
|
||||
"wide table with shared Info/ID, Net I/O, and Disk I/O columns visible",
|
||||
"narrow table with the responsive Info/ID column visible and I/O columns omitted",
|
||||
"active descending Net I/O sort indicator"
|
||||
"wide table with the shared Info/ID column visible",
|
||||
"narrow table with the responsive Info/ID column visible",
|
||||
"active ascending and descending ID sort indicators"
|
||||
],
|
||||
"interactions": [
|
||||
"reloaded the Proxmox overview after the final source change",
|
||||
"confirmed Type and the shared Info/ID header and cells use balanced centered alignment at desktop and narrow widths",
|
||||
"confirmed Net I/O and Disk I/O headers stay centered over their paired directional readings",
|
||||
"sorted by ID and Net I/O and confirmed the sort indicator preserves the corrected alignment",
|
||||
"confirmed table and scroll-shell width parity at 1920px and 768px without unintended horizontal overflow",
|
||||
"confirmed no browser console warnings or errors and restored the viewport override"
|
||||
"loaded the Proxmox overview from the final source at desktop and narrow widths",
|
||||
"clicked the shared ID header to sort ascending and confirmed the first group ordered IDs from 663 upward",
|
||||
"clicked the shared ID header again to sort descending and confirmed the first group ordered IDs from 680 downward",
|
||||
"confirmed the ID sort indicator and centered header-cell alignment remain coherent at 1920px and 768px",
|
||||
"confirmed the responsive table remained readable without clipping the changed header and restored the viewport override"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1104,6 +1104,11 @@ describe('GUEST_COLUMNS', () => {
|
|||
expect(columnIds.slice(dockerTailStart)).toEqual(['netIo', 'diskIo', 'update']);
|
||||
});
|
||||
|
||||
it('sorts the mixed Info/ID column by its rendered value', () => {
|
||||
expect(GUEST_COLUMNS.find((column) => column.id === 'info')?.sortKey).toBe('info');
|
||||
expect(GUEST_COLUMNS.find((column) => column.id === 'vmid')?.sortKey).toBe('vmid');
|
||||
});
|
||||
|
||||
it('marks toggleable columns correctly', () => {
|
||||
const toggleable = GUEST_COLUMNS.filter((c) => c.toggleable);
|
||||
const toggleableIds = toggleable.map((c) => c.id);
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ import guestRowCellsSource from '../GuestRowCells.tsx?raw';
|
|||
import guestDrawerStateSource from '../useGuestDrawerState.ts?raw';
|
||||
import guestRowModelSource from '../guestRowModel.tsx?raw';
|
||||
import guestRowStateSource from '../useGuestRowState.ts?raw';
|
||||
import workloadSelectorsSource from '../workloadSelectors.ts?raw';
|
||||
import workloadTopologySource from '../workloadTopology.ts?raw';
|
||||
import {
|
||||
filterWorkloads,
|
||||
|
|
@ -1652,6 +1653,11 @@ describe('Workloads performance contract', () => {
|
|||
expect(workloadTableHeaderSource).toContain('TableHead');
|
||||
expect(workloadTableHeaderSource).toContain('col.sortKey as WorkloadSortKey');
|
||||
expect(workloadTableHeaderSource).toContain('getTableSortIndicator');
|
||||
expect(guestRowModelSource).toContain(
|
||||
"id: 'info', label: 'Info', width: '100px', sortKey: 'info'",
|
||||
);
|
||||
expect(workloadSelectorsSource).toContain("sortKey === 'info'");
|
||||
expect(workloadSelectorsSource).toContain('getWorkloadInfoValue');
|
||||
expect(workloadTableHeaderSource).toContain('style={getGuestColumnStyle(');
|
||||
expect(workloadTableHeaderSource).toContain('props.workloadTableLayoutMode()');
|
||||
expect(workloadTableHeaderSource).toContain('props.workloadTableVisibleColumnIds()');
|
||||
|
|
|
|||
|
|
@ -640,6 +640,64 @@ describe('workloadSelectors', () => {
|
|||
expect([...guests].sort(cpuAsc!).map((g) => g.id)).toEqual(['id-1', 'id-2', 'id-3']);
|
||||
expect(createWorkloadSortComparator('', 'asc')).toBeNull();
|
||||
});
|
||||
|
||||
it('sorts the mixed Info/ID projection naturally and keeps empty values last', () => {
|
||||
const guests = [
|
||||
makeGuest(1, {
|
||||
id: 'vm-10',
|
||||
name: 'vm-10',
|
||||
type: 'vm',
|
||||
workloadType: 'vm',
|
||||
displayId: '10',
|
||||
}),
|
||||
makeGuest(2, {
|
||||
id: 'container-nginx',
|
||||
name: 'container-nginx',
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
image: 'docker.io/library/nginx:2',
|
||||
}),
|
||||
makeGuest(3, {
|
||||
id: 'vm-2',
|
||||
name: 'vm-2',
|
||||
type: 'vm',
|
||||
workloadType: 'vm',
|
||||
displayId: '2',
|
||||
}),
|
||||
makeGuest(4, {
|
||||
id: 'pod-production',
|
||||
name: 'pod-production',
|
||||
type: 'pod',
|
||||
workloadType: 'pod',
|
||||
namespace: 'production',
|
||||
}),
|
||||
makeGuest(5, {
|
||||
id: 'pod-empty',
|
||||
name: 'pod-empty',
|
||||
type: 'pod',
|
||||
workloadType: 'pod',
|
||||
namespace: '',
|
||||
}),
|
||||
];
|
||||
|
||||
const infoAsc = createWorkloadSortComparator('info', 'asc')!;
|
||||
const infoDesc = createWorkloadSortComparator('info', 'desc')!;
|
||||
|
||||
expect([...guests].sort(infoAsc).map((guest) => guest.id)).toEqual([
|
||||
'vm-2',
|
||||
'vm-10',
|
||||
'container-nginx',
|
||||
'pod-production',
|
||||
'pod-empty',
|
||||
]);
|
||||
expect([...guests].sort(infoDesc).map((guest) => guest.id)).toEqual([
|
||||
'pod-production',
|
||||
'container-nginx',
|
||||
'vm-10',
|
||||
'vm-2',
|
||||
'pod-empty',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupWorkloads', () => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import type { WorkloadGuest, WorkloadType, ViewMode } from '@/types/workloads';
|
||||
import { getShortImageName } from '@/utils/format';
|
||||
import { resolveWorkloadType } from '@/utils/workloads';
|
||||
import { createVisibleCanonicalTypeColumn } from '@/utils/typeColumnDefinition';
|
||||
import type {
|
||||
WorkloadMetricHistoryReader,
|
||||
|
|
@ -184,6 +186,27 @@ export const getOutlierEmphasis = (value: number, stats: IODistributionStats): I
|
|||
return { className: 'text-muted', showOutlierHint: false };
|
||||
};
|
||||
|
||||
export const getWorkloadDisplayId = (guest: WorkloadGuest): string => {
|
||||
const provided = guest.displayId?.trim();
|
||||
if (provided) return provided;
|
||||
if (typeof guest.vmid === 'number' && guest.vmid > 0) return String(guest.vmid);
|
||||
return '';
|
||||
};
|
||||
|
||||
// The mixed Info column projects one scalar from each workload kind. Sorting
|
||||
// must use this same projection so the header never orders rows by a hidden or
|
||||
// provider-specific field that differs from the rendered value.
|
||||
export const getWorkloadInfoValue = (guest: WorkloadGuest): string => {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'vm' || type === 'system-container') return getWorkloadDisplayId(guest);
|
||||
if (type === 'app-container') {
|
||||
const image = guest.image?.trim() ?? '';
|
||||
return image ? getShortImageName(image) : '';
|
||||
}
|
||||
if (type === 'pod') return guest.namespace?.trim() ?? '';
|
||||
return '';
|
||||
};
|
||||
|
||||
export const GUEST_COLUMNS: ColumnDef[] = [
|
||||
{
|
||||
id: 'name',
|
||||
|
|
@ -213,7 +236,7 @@ export const GUEST_COLUMNS: ColumnDef[] = [
|
|||
kind: 'text',
|
||||
},
|
||||
createVisibleCanonicalTypeColumn(),
|
||||
{ id: 'info', label: 'Info', width: '100px', kind: 'numeric-value' },
|
||||
{ id: 'info', label: 'Info', width: '100px', sortKey: 'info', kind: 'numeric-value' },
|
||||
{ id: 'vmid', label: 'ID', width: '45px', sortKey: 'vmid', kind: 'numeric-value' },
|
||||
{ id: 'cpu', label: 'CPU', width: '140px', sortKey: 'cpu', kind: 'metric-bar' },
|
||||
{ id: 'memory', label: 'Mem', width: '140px', sortKey: 'memory', kind: 'metric-bar' },
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { Container } from '@/types/api';
|
|||
import type { DisplayMetricType } from '@/utils/metricThresholds';
|
||||
import { buildMetricKey } from '@/utils/metricsKeys';
|
||||
import { getGuestHealthIndicator, isGuestRunning } from '@/utils/status';
|
||||
import { getShortImageName, formatBytes } from '@/utils/format';
|
||||
import { formatBytes } from '@/utils/format';
|
||||
import { getContainerRuntimeBadgeForRuntime } from '@/utils/resourceBadgePresentation';
|
||||
import {
|
||||
getCanonicalWorkloadId,
|
||||
|
|
@ -31,6 +31,8 @@ import {
|
|||
EMPTY_IO_EMPHASIS,
|
||||
GROUPED_FIRST_CELL_INDENT,
|
||||
getOutlierEmphasis,
|
||||
getWorkloadDisplayId,
|
||||
getWorkloadInfoValue,
|
||||
type GuestRowProps,
|
||||
} from './guestRowModel';
|
||||
|
||||
|
|
@ -76,14 +78,7 @@ export function useGuestRowState(props: GuestRowProps) {
|
|||
|
||||
const customUrl = createMemo(() => props.customUrl?.trim() ?? '');
|
||||
|
||||
const displayId = createMemo(() => {
|
||||
const provided = props.guest.displayId?.trim();
|
||||
if (provided) return provided;
|
||||
if (typeof props.guest.vmid === 'number' && props.guest.vmid > 0) {
|
||||
return String(props.guest.vmid);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const displayId = createMemo(() => getWorkloadDisplayId(props.guest));
|
||||
|
||||
const ipAddresses = createMemo(() => props.guest.ipAddresses ?? []);
|
||||
const networkInterfaces = createMemo(() => props.guest.networkInterfaces ?? []);
|
||||
|
|
@ -107,13 +102,7 @@ export function useGuestRowState(props: GuestRowProps) {
|
|||
return type === 'vm' || type === 'system-container';
|
||||
});
|
||||
|
||||
const infoValue = createMemo(() => {
|
||||
const type = workloadType();
|
||||
if (type === 'vm' || type === 'system-container') return displayId();
|
||||
if (type === 'app-container') return dockerImage() ? getShortImageName(dockerImage()) : '';
|
||||
if (type === 'pod') return namespace();
|
||||
return '';
|
||||
});
|
||||
const infoValue = createMemo(() => getWorkloadInfoValue(props.guest));
|
||||
|
||||
const infoTooltip = createMemo(() => {
|
||||
if (workloadType() === 'app-container') return dockerImage();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Node } from '@/types/api';
|
||||
import type { WorkloadGuest, ViewMode, WorkloadType } from '@/types/workloads';
|
||||
import type { WorkloadIOEmphasis } from './guestRowModel';
|
||||
import { getWorkloadInfoValue, type WorkloadIOEmphasis } from './guestRowModel';
|
||||
import { computeIOScale } from '@/components/Infrastructure/infrastructureSelectors';
|
||||
import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction';
|
||||
import { parseFilterStack, evaluateFilterStack, splitSearchExclusions } from '@/utils/searchQuery';
|
||||
|
|
@ -307,12 +307,19 @@ export const createWorkloadSortComparator = (
|
|||
if (nameA !== nameB) return nameA < nameB ? -1 : 1;
|
||||
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
||||
};
|
||||
const naturalTextCollator = new Intl.Collator(undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base',
|
||||
});
|
||||
|
||||
return (a: WorkloadGuest, b: WorkloadGuest): number => {
|
||||
let aVal: SortValue = null;
|
||||
let bVal: SortValue = null;
|
||||
|
||||
if (sortKey === 'cpu') {
|
||||
if (sortKey === 'info') {
|
||||
aVal = getWorkloadInfoValue(a);
|
||||
bVal = getWorkloadInfoValue(b);
|
||||
} else if (sortKey === 'cpu') {
|
||||
aVal = getWorkloadCPUPercent(a.cpu) ?? 0;
|
||||
bVal = getWorkloadCPUPercent(b.cpu) ?? 0;
|
||||
} else if (sortKey === 'memory') {
|
||||
|
|
@ -349,7 +356,9 @@ export const createWorkloadSortComparator = (
|
|||
const bStr = String(bVal).toLowerCase();
|
||||
|
||||
if (aStr === bStr) return tiebreak(a, b);
|
||||
const comparison = aStr < bStr ? -1 : 1;
|
||||
const comparison =
|
||||
sortKey === 'info' ? naturalTextCollator.compare(aStr, bStr) : aStr < bStr ? -1 : 1;
|
||||
if (comparison === 0) return tiebreak(a, b);
|
||||
return sortDirection === 'asc' ? comparison : -comparison;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export type WorkloadsStatusMode = 'all' | 'running' | 'degraded' | 'stopped';
|
|||
export type WorkloadsGroupingMode = 'grouped' | 'flat';
|
||||
export type WorkloadsMetricDisplayMode = 'bars' | 'sparklines';
|
||||
export type WorkloadsMemoryDisplayBasis = 'guest' | 'host';
|
||||
export type WorkloadsSortKey = keyof WorkloadGuest | 'diskIo' | 'netIo';
|
||||
export type WorkloadsSortKey = keyof WorkloadGuest | 'diskIo' | 'info' | 'netIo';
|
||||
|
||||
export interface WorkloadsStatusOption {
|
||||
value: WorkloadsStatusMode;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue