diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx index 3017f38e7..d693f3c24 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx @@ -54,20 +54,20 @@ export const UnifiedResourcePBSTableSection: Component {table.headerLabels().resource} {table.headerLabels().datastores} @@ -80,14 +80,14 @@ export const UnifiedResourcePBSTableSection: Component {table.headerLabels().source} @@ -204,9 +204,7 @@ export const UnifiedResourcePBSTableSection: Component - +
- +
- +
diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx index 7408c0d5b..a1976c8be 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx @@ -54,34 +54,34 @@ export const UnifiedResourcePMGTableSection: Component {table.headerLabels().resource} {table.headerLabels().queue} {table.headerLabels().deferred} {table.headerLabels().hold} @@ -94,14 +94,14 @@ export const UnifiedResourcePMGTableSection: Component {table.headerLabels().source} @@ -218,9 +218,7 @@ export const UnifiedResourcePMGTableSection: Component - +
- +
@@ -261,9 +257,7 @@ export const UnifiedResourcePMGTableSection: Component - +
- +
diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourceSourceBadgeCell.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourceSourceBadgeCell.tsx index 389effa33..d3c7774ac 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourceSourceBadgeCell.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourceSourceBadgeCell.tsx @@ -11,7 +11,7 @@ interface UnifiedResourceSourceBadgeCellProps { } const getVisibleSourceBadgeLimit = (layoutMode: UnifiedResourceTableLayoutMode): number => - layoutMode === 'wide' ? 3 : 1; + layoutMode === 'wide' ? 3 : 2; export const UnifiedResourceSourceBadgeCell: Component = ( props, @@ -25,7 +25,11 @@ export const UnifiedResourceSourceBadgeCell: Component badges().slice(0, getVisibleSourceBadgeLimit(props.layoutMode)), ); + const hiddenBadges = createMemo(() => badges().slice(visibleBadges().length)); const hiddenBadgeCount = createMemo(() => Math.max(0, badges().length - visibleBadges().length)); + const hiddenBadgeLabel = createMemo(() => + hiddenBadgeCount() === 1 ? `+${hiddenBadges()[0]?.label ?? ''}` : `+${hiddenBadgeCount()}`, + ); const title = createMemo(() => badges() .map((badge) => badge.title ?? badge.label) @@ -47,9 +51,12 @@ export const UnifiedResourceSourceBadgeCell: Component 0}> badge.title ?? badge.label) + .join(', ')}`} title={title()} > - +{hiddenBadgeCount()} + {hiddenBadgeLabel()}
diff --git a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx index e267a9868..6005fd63d 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx @@ -237,6 +237,50 @@ describe('UnifiedResourceTable performance contract', () => { }); }); + it('keeps service columns denser than host columns before hiding them', async () => { + const resources = [ + makeResource(2, { + type: 'pbs', + name: 'pbs-docker', + displayName: 'pbs-docker', + platformType: 'proxmox-pbs', + sourceType: 'hybrid', + platformData: { sources: ['proxmox-pbs', 'agent'] }, + uptime: 86400, + }), + ]; + const { getByText, queryByText } = render(() => ( + + )); + + await waitFor(() => { + expect(getByText('Stores')).toBeInTheDocument(); + }); + + emitResizeObserverWidth(600); + + await waitFor(() => { + expect(getByText('Store').closest('th')).not.toHaveClass('hidden'); + expect(getByText('Jobs').closest('th')).not.toHaveClass('hidden'); + expect(getByText('Src').closest('th')).not.toHaveClass('hidden'); + expect(getByText('Up').closest('th')).toHaveClass('hidden'); + }); + expect(getByText('PBS')).toBeInTheDocument(); + expect(getByText('Agent')).toBeInTheDocument(); + expect(queryByText('+1')).toBeNull(); + + emitResizeObserverWidth(660); + + await waitFor(() => { + expect(getByText('Up').closest('th')).not.toHaveClass('hidden'); + }); + }); + it('renders the shared facet summary component in timeline-only mode for canonical resource counts', async () => { const { getByText, queryByText } = render(() => ( { ); expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableHeaderLabels'); expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableLayoutMode'); + expect(unifiedResourceTableStateSource).toContain('isUnifiedResourceServiceColumnVisible'); expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableShellClass'); expect(unifiedResourceTableStateSource).toContain('useTableWindowing'); expect(unifiedResourceTableStateSource).toContain('useUnifiedResourceTableViewportSync'); @@ -332,6 +377,9 @@ describe('UnifiedResourceTable performance contract', () => { expect(unifiedResourceTableStateModelSource).toContain( 'export const getUnifiedResourceTableLayoutMode', ); + expect(unifiedResourceTableStateModelSource).toContain( + 'export const isUnifiedResourceServiceColumnVisible', + ); expect(unifiedResourceTableStateModelSource).toContain( 'export const getUnifiedResourceTableShellClass', ); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts index 1a5c01b90..d341eb2e4 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/unifiedResourceTableStateModel.test.ts @@ -15,6 +15,7 @@ import { getUnifiedResourceTableSortIndicator, getUnifiedSources, getVisibleHostTableItems, + isUnifiedResourceServiceColumnVisible, isUnifiedResourceTableColumnVisible, normalizeUnifiedResourceTableLayoutWidth, shouldShowUnifiedResourceHostTable, @@ -128,21 +129,26 @@ describe('unifiedResourceTableStateModel', () => { // lower-priority columns are dropped. expect(mobileColumns.resourceColumn.width).toBe('40%'); expect(mobileColumns.metricColumn.width).toBe('20%'); - expect(mobileColumns.serviceHealthColumn.width).toBe('36%'); - expect(mobileColumns.serviceActionColumn.width).toBe('24%'); + expect(mobileColumns.serviceResourceColumn.width).toBe('28%'); + expect(mobileColumns.serviceCountColumn.width).toBe('11%'); + expect(mobileColumns.serviceHealthColumn.width).toBe('13%'); + expect(mobileColumns.serviceActionColumn.width).toBe('14%'); expect(tabletColumns.resourceColumn.width).toBe('26%'); + expect(tabletColumns.serviceResourceColumn.width).toBe('24%'); expect(tabletColumns.ioColumn.width).toBe('18%'); - expect(tabletColumns.sourceColumn.width).toBe('17%'); + expect(tabletColumns.sourceColumn.width).toBe('8%'); expect(compactColumns.resourceColumn.width).toBe('18%'); + expect(compactColumns.serviceResourceColumn.width).toBe('18%'); expect(compactColumns.metricColumn.width).toBe('10.5%'); expect(compactColumns.tempColumn.width).toBe('7%'); expect(wideColumns.resourceColumn.width).toBe('18%'); + expect(wideColumns.serviceResourceColumn.width).toBe('18%'); expect(wideColumns.ioColumn.width).toBe('12.5%'); expect(wideColumns.serviceActionColumn.width).toBe('16%'); expect(getUnifiedResourceTableHeaderLabels('wide').memory).toBe('Memory'); expect(getUnifiedResourceTableHeaderLabels('compact').memory).toBe('Mem'); expect(getUnifiedResourceTableHeaderLabels('tablet').network).toBe('Net'); - expect(getUnifiedResourceTableHeaderLabels('mobile').datastores).toBe('Stores'); + expect(getUnifiedResourceTableHeaderLabels('mobile').datastores).toBe('Store'); expect(shouldShowUnifiedResourceHostTable(0, 0)).toBe(true); expect(shouldShowUnifiedResourceHostTable(0, 2)).toBe(false); expect(shouldShowUnifiedResourceHostTable(3, 2)).toBe(true); @@ -166,6 +172,14 @@ describe('unifiedResourceTableStateModel', () => { expect(isUnifiedResourceTableColumnVisible('supplementary', 900)).toBe(true); expect(isUnifiedResourceTableColumnVisible('detailed', 1159)).toBe(false); expect(isUnifiedResourceTableColumnVisible('detailed', 1160)).toBe(true); + expect(isUnifiedResourceServiceColumnVisible('primary', 499)).toBe(false); + expect(isUnifiedResourceServiceColumnVisible('primary', 500)).toBe(true); + expect(isUnifiedResourceServiceColumnVisible('secondary', 579)).toBe(false); + expect(isUnifiedResourceServiceColumnVisible('secondary', 580)).toBe(true); + expect(isUnifiedResourceServiceColumnVisible('supplementary', 639)).toBe(false); + expect(isUnifiedResourceServiceColumnVisible('supplementary', 640)).toBe(true); + expect(isUnifiedResourceServiceColumnVisible('detailed', 899)).toBe(false); + expect(isUnifiedResourceServiceColumnVisible('detailed', 900)).toBe(true); }); it('reads unified source tags from platform data without hook state', () => { diff --git a/frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts b/frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts index f64109d95..d41edf022 100644 --- a/frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts +++ b/frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts @@ -46,6 +46,13 @@ export const UNIFIED_RESOURCE_TABLE_COLUMN_BREAKPOINTS: Record = { + essential: 0, + primary: 500, + secondary: 580, + supplementary: 640, + detailed: 900, +}; export type UnifiedResourceTableLayoutMode = 'mobile' | 'tablet' | 'compact' | 'wide'; @@ -83,6 +90,13 @@ export const isUnifiedResourceTableColumnVisible = ( normalizeUnifiedResourceTableLayoutWidth(layoutWidth) >= UNIFIED_RESOURCE_TABLE_COLUMN_BREAKPOINTS[priority]; +export const isUnifiedResourceServiceColumnVisible = ( + priority: ColumnPriority, + layoutWidth: number, +): boolean => + normalizeUnifiedResourceTableLayoutWidth(layoutWidth) >= + UNIFIED_RESOURCE_SERVICE_COLUMN_BREAKPOINTS[priority]; + export const buildResourceLabelById = (resources: Resource[]): Map => { const map = new Map(); for (const resource of resources) { @@ -212,6 +226,7 @@ export interface UnifiedResourceTableColumnPresentation { export type UnifiedResourceTableColumnPresentations = { resourceColumn: UnifiedResourceTableColumnPresentation; + serviceResourceColumn: UnifiedResourceTableColumnPresentation; metricColumn: UnifiedResourceTableColumnPresentation; ioColumn: UnifiedResourceTableColumnPresentation; sourceColumn: UnifiedResourceTableColumnPresentation; @@ -312,7 +327,7 @@ export const getUnifiedResourceTableHeaderLabels = ( source: 'Src', uptime: 'Up', temp: 'C', - datastores: 'Stores', + datastores: 'Store', activity: 'Jobs', queue: 'Queue', deferred: 'Def', @@ -332,36 +347,39 @@ export const getUnifiedResourceTableColumnPresentations = ( // never render. return { resourceColumn: buildUnifiedResourceTableColumnPresentation('', '40%'), + serviceResourceColumn: buildUnifiedResourceTableColumnPresentation('', '28%'), metricColumn: buildUnifiedResourceTableColumnPresentation('', '20%'), ioColumn: buildUnifiedResourceTableColumnPresentation('', '20%'), - sourceColumn: buildUnifiedResourceTableColumnPresentation('', '20%'), - uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '15%'), + sourceColumn: buildUnifiedResourceTableColumnPresentation('', '10%'), + uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '8%'), tempColumn: buildUnifiedResourceTableColumnPresentation('', '15%'), - serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '20%'), - serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '20%'), - serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '36%'), - serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '24%'), + serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '11%'), + serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '8%'), + serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '13%'), + serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '14%'), }; } if (layoutMode === 'tablet') { return { resourceColumn: buildUnifiedResourceTableColumnPresentation('', '26%'), + serviceResourceColumn: buildUnifiedResourceTableColumnPresentation('', '24%'), metricColumn: buildUnifiedResourceTableColumnPresentation('', '13%'), ioColumn: buildUnifiedResourceTableColumnPresentation('', '18%'), - sourceColumn: buildUnifiedResourceTableColumnPresentation('', '17%'), - uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '8%'), + sourceColumn: buildUnifiedResourceTableColumnPresentation('', '8%'), + uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '7%'), tempColumn: buildUnifiedResourceTableColumnPresentation('', '6%'), serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '8%'), - serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '8%'), - serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '16%'), - serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '17%'), + serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '7.5%'), + serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '14%'), + serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '13%'), }; } if (layoutMode === 'compact') { return { resourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'), + serviceResourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'), metricColumn: buildUnifiedResourceTableColumnPresentation('', '10.5%'), ioColumn: buildUnifiedResourceTableColumnPresentation('', '13%'), sourceColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'), @@ -376,6 +394,7 @@ export const getUnifiedResourceTableColumnPresentations = ( return { resourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'), + serviceResourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'), metricColumn: buildUnifiedResourceTableColumnPresentation('', '10.5%'), ioColumn: buildUnifiedResourceTableColumnPresentation('', '12.5%'), sourceColumn: buildUnifiedResourceTableColumnPresentation('', '10%'), diff --git a/frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts b/frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts index c1bfa55e1..53070648b 100644 --- a/frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts +++ b/frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts @@ -22,6 +22,7 @@ import { getUnifiedResourceTableColumnPresentations, getUnifiedResourceTableHeaderLabels, getUnifiedResourceTableLayoutMode, + isUnifiedResourceServiceColumnVisible, isUnifiedResourceTableColumnVisible, getUnifiedResourceTableShellClass, getUnifiedResourceTableSortIndicator, @@ -107,6 +108,8 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) { const isMobile = createMemo(() => shouldUseUnifiedResourceTableMobileLayout(tableLayoutWidth())); const isVisible = (priority: ColumnPriority) => isUnifiedResourceTableColumnVisible(priority, tableLayoutWidth()); + const isServiceVisible = (priority: ColumnPriority) => + isUnifiedResourceServiceColumnVisible(priority, tableLayoutWidth()); const split = createMemo(() => splitPrimaryAndServiceResources(props.resources)); const primaryResources = createMemo(() => split().primaryResources); @@ -210,6 +213,7 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) { layoutMode, isMobile, isVisible, + isServiceVisible, handleSort, renderSortIndicator, resolveResourceLabel, @@ -225,6 +229,7 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) { tableShellClass, headerLabels, resourceColumn: () => columnPresentations().resourceColumn, + serviceResourceColumn: () => columnPresentations().serviceResourceColumn, metricColumn: () => columnPresentations().metricColumn, ioColumn: () => columnPresentations().ioColumn, sourceColumn: () => columnPresentations().sourceColumn,