mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
Fix infrastructure table density at tablet widths
This commit is contained in:
parent
3f21acd0da
commit
f4d0006a5e
8 changed files with 465 additions and 263 deletions
|
|
@ -33,6 +33,7 @@ import { ResourceDetailDrawer } from './ResourceDetailDrawer';
|
|||
import { buildWorkloadsHref } from './workloadsLink';
|
||||
import { ClusterDeployBanner } from './ClusterDeployBanner';
|
||||
import { ResourceFacetSummary } from './ResourceFacetSummary';
|
||||
import { UnifiedResourceSourceBadgeCell } from './UnifiedResourceSourceBadgeCell';
|
||||
import type { SummarySeriesGroupScope } from '@/components/shared/summaryCardInteraction';
|
||||
import { resolveSummaryGroupMemberInteractionState } from '@/components/shared/summaryCardInteraction';
|
||||
import {
|
||||
|
|
@ -74,28 +75,28 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
width={table.resourceColumn().width}
|
||||
onClick={() => table.handleSort('name')}
|
||||
>
|
||||
Resource {table.renderSortIndicator('name')}
|
||||
{table.headerLabels().resource} {table.renderSortIndicator('name')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={table.metricColumn().className}
|
||||
width={table.metricColumn().width}
|
||||
onClick={() => table.handleSort('cpu')}
|
||||
>
|
||||
CPU {table.renderSortIndicator('cpu')}
|
||||
{table.headerLabels().cpu} {table.renderSortIndicator('cpu')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={table.metricColumn().className}
|
||||
width={table.metricColumn().width}
|
||||
onClick={() => table.handleSort('memory')}
|
||||
>
|
||||
{table.isMobile() ? 'Mem' : 'Memory'} {table.renderSortIndicator('memory')}
|
||||
{table.headerLabels().memory} {table.renderSortIndicator('memory')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={table.metricColumn().className}
|
||||
width={table.metricColumn().width}
|
||||
onClick={() => table.handleSort('disk')}
|
||||
>
|
||||
Disk {table.renderSortIndicator('disk')}
|
||||
{table.headerLabels().disk} {table.renderSortIndicator('disk')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
|
|
@ -103,7 +104,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
width={table.ioColumn().width}
|
||||
onClick={() => table.handleSort('network')}
|
||||
>
|
||||
Net I/O {table.renderSortIndicator('network')}
|
||||
{table.headerLabels().network} {table.renderSortIndicator('network')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
|
||||
|
|
@ -111,7 +112,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
width={table.ioColumn().width}
|
||||
onClick={() => table.handleSort('diskio')}
|
||||
>
|
||||
Disk I/O {table.renderSortIndicator('diskio')}
|
||||
{table.headerLabels().diskIo} {table.renderSortIndicator('diskio')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
|
|
@ -119,7 +120,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
width={table.sourceColumn().width}
|
||||
onClick={() => table.handleSort('source')}
|
||||
>
|
||||
Source {table.renderSortIndicator('source')}
|
||||
{table.headerLabels().source} {table.renderSortIndicator('source')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
|
||||
|
|
@ -127,7 +128,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
width={table.uptimeColumn().width}
|
||||
onClick={() => table.handleSort('uptime')}
|
||||
>
|
||||
Uptime {table.renderSortIndicator('uptime')}
|
||||
{table.headerLabels().uptime} {table.renderSortIndicator('uptime')}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
|
||||
|
|
@ -135,7 +136,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
width={table.tempColumn().width}
|
||||
onClick={() => table.handleSort('temp')}
|
||||
>
|
||||
Temp {table.renderSortIndicator('temp')}
|
||||
{table.headerLabels().temp} {table.renderSortIndicator('temp')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -166,7 +167,9 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
const handleGroupFocusToggle = () => {
|
||||
const nextScope = groupSummaryScope();
|
||||
tableProps.onGroupFocusChange?.(
|
||||
nextScope && tableProps.focusedSummaryGroupId === nextScope.id ? null : nextScope?.id ?? null,
|
||||
nextScope && tableProps.focusedSummaryGroupId === nextScope.id
|
||||
? null
|
||||
: (nextScope?.id ?? null),
|
||||
);
|
||||
};
|
||||
const groupRowInteraction = createSummaryInteractiveRowPreviewHandlers({
|
||||
|
|
@ -301,7 +304,6 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
const unifiedSourceBadges = createMemo(() =>
|
||||
getUnifiedSourceBadges(table.getUnifiedSources(resource)),
|
||||
);
|
||||
const hasUnifiedSources = createMemo(() => unifiedSourceBadges().length > 0);
|
||||
const policyBadges = createMemo(() =>
|
||||
getResourcePolicyTableBadges(resource.policy),
|
||||
);
|
||||
|
|
@ -497,10 +499,16 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div class="grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums">
|
||||
<div
|
||||
class={
|
||||
table.layoutMode() === 'wide'
|
||||
? 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums'
|
||||
: 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[10px] leading-tight tabular-nums'
|
||||
}
|
||||
>
|
||||
<span class="inline-flex w-3 justify-center text-emerald-500">↓</span>
|
||||
<span
|
||||
class={`min-w-0 whitespace-nowrap ${networkEmphasis().className}`}
|
||||
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${networkEmphasis().className}`}
|
||||
title={
|
||||
networkEmphasis().showOutlierHint
|
||||
? `${formatSpeed(resource.network!.rxBytes)} (Top outlier)`
|
||||
|
|
@ -511,7 +519,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
</span>
|
||||
<span class="inline-flex w-3 justify-center text-orange-400">↑</span>
|
||||
<span
|
||||
class={`min-w-0 whitespace-nowrap ${networkEmphasis().className}`}
|
||||
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${networkEmphasis().className}`}
|
||||
title={
|
||||
networkEmphasis().showOutlierHint
|
||||
? `${formatSpeed(resource.network!.txBytes)} (Top outlier)`
|
||||
|
|
@ -537,12 +545,18 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div class="grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums">
|
||||
<div
|
||||
class={
|
||||
table.layoutMode() === 'wide'
|
||||
? 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)_0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[11px] tabular-nums'
|
||||
: 'grid w-full grid-cols-[0.75rem_minmax(0,1fr)] items-center gap-x-1 text-[10px] leading-tight tabular-nums'
|
||||
}
|
||||
>
|
||||
<span class="inline-flex w-3 justify-center font-mono text-blue-500">
|
||||
R
|
||||
</span>
|
||||
<span
|
||||
class={`min-w-0 whitespace-nowrap ${diskIOEmphasis().className}`}
|
||||
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${diskIOEmphasis().className}`}
|
||||
title={
|
||||
diskIOEmphasis().showOutlierHint
|
||||
? `${formatSpeed(resource.diskIO!.readRate)} (Top outlier)`
|
||||
|
|
@ -555,7 +569,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
W
|
||||
</span>
|
||||
<span
|
||||
class={`min-w-0 whitespace-nowrap ${diskIOEmphasis().className}`}
|
||||
class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap ${diskIOEmphasis().className}`}
|
||||
title={
|
||||
diskIOEmphasis().showOutlierHint
|
||||
? `${formatSpeed(resource.diskIO!.writeRate)} (Top outlier)`
|
||||
|
|
@ -571,37 +585,12 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
|
|||
<TableCell
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Show
|
||||
when={hasUnifiedSources()}
|
||||
fallback={
|
||||
<>
|
||||
<Show when={platformBadge()}>
|
||||
{(badge) => (
|
||||
<span class={badge().classes} title={badge().title}>
|
||||
{badge().label}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={sourceBadge()}>
|
||||
{(badge) => (
|
||||
<span class={badge().classes} title={badge().title}>
|
||||
{badge().label}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<For each={unifiedSourceBadges()}>
|
||||
{(badge) => (
|
||||
<span class={badge.classes} title={badge.title}>
|
||||
{badge.label}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
<UnifiedResourceSourceBadgeCell
|
||||
unifiedBadges={unifiedSourceBadges()}
|
||||
platformBadge={platformBadge()}
|
||||
sourceBadge={sourceBadge()}
|
||||
layoutMode={table.layoutMode()}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity'
|
|||
import { shouldShowResourceAlternateName } from '@/utils/resourcePolicyPresentation';
|
||||
import { ResourceDetailDrawer } from './ResourceDetailDrawer';
|
||||
import { ResourceFacetSummary } from './ResourceFacetSummary';
|
||||
import { UnifiedResourceSourceBadgeCell } from './UnifiedResourceSourceBadgeCell';
|
||||
import {
|
||||
type UnifiedResourceTableProps,
|
||||
type UnifiedResourceTableState,
|
||||
|
|
@ -56,47 +57,47 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
|
|||
class={`text-left pl-2 sm:pl-3 ${table.resourceColumn().className}`}
|
||||
width={table.resourceColumn().width}
|
||||
>
|
||||
Resource
|
||||
{table.headerLabels().resource}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}
|
||||
class={table.serviceCountColumn().className}
|
||||
width={table.serviceCountColumn().width}
|
||||
>
|
||||
Datastores
|
||||
{table.headerLabels().datastores}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
class={table.serviceCountColumn().className}
|
||||
width={table.serviceCountColumn().width}
|
||||
>
|
||||
Activity
|
||||
{table.headerLabels().activity}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={table.serviceHealthColumn().className}
|
||||
width={table.serviceHealthColumn().width}
|
||||
>
|
||||
Health
|
||||
{table.headerLabels().health}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
class={table.sourceColumn().className}
|
||||
width={table.sourceColumn().width}
|
||||
>
|
||||
Source
|
||||
{table.headerLabels().source}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
|
||||
class={table.uptimeColumn().className}
|
||||
width={table.uptimeColumn().width}
|
||||
>
|
||||
Uptime
|
||||
{table.headerLabels().uptime}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={table.serviceActionColumn().className}
|
||||
width={table.serviceActionColumn().width}
|
||||
>
|
||||
Action
|
||||
{table.headerLabels().action}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -110,9 +111,7 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
|
|||
const displayName = createMemo(() =>
|
||||
getPreferredInfrastructureDisplayName(resource),
|
||||
);
|
||||
const serviceLink = createMemo(
|
||||
() => buildServiceDetailLinks(resource)[0] ?? null,
|
||||
);
|
||||
const serviceLink = createMemo(() => buildServiceDetailLinks(resource)[0] ?? null);
|
||||
const statusIndicator = createMemo(() =>
|
||||
getAgentStatusIndicator({ status: resource.status }),
|
||||
);
|
||||
|
|
@ -122,7 +121,6 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
|
|||
const unifiedSourceBadges = createMemo(() =>
|
||||
getUnifiedSourceBadges(table.getUnifiedSources(resource)),
|
||||
);
|
||||
const hasUnifiedSources = createMemo(() => unifiedSourceBadges().length > 0);
|
||||
const healthClass = createMemo(
|
||||
() =>
|
||||
getServiceHealthSummaryPresentation(resource.status, pbsRow()?.health)
|
||||
|
|
@ -206,7 +204,9 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}>
|
||||
<TableCell
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Show
|
||||
when={pbsRow()?.datastores != null}
|
||||
|
|
@ -217,7 +217,9 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
|
||||
<TableCell
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Show
|
||||
when={pbsRow()?.activity}
|
||||
|
|
@ -253,41 +255,22 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Show
|
||||
when={hasUnifiedSources()}
|
||||
fallback={
|
||||
<>
|
||||
<Show when={platformBadge()}>
|
||||
{(badge) => (
|
||||
<span class={badge().classes} title={badge().title}>
|
||||
{badge().label}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={sourceBadge()}>
|
||||
{(badge) => (
|
||||
<span class={badge().classes} title={badge().title}>
|
||||
{badge().label}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<For each={unifiedSourceBadges()}>
|
||||
{(badge) => (
|
||||
<span class={badge.classes} title={badge.title}>
|
||||
{badge.label}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
<TableCell
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
>
|
||||
<UnifiedResourceSourceBadgeCell
|
||||
unifiedBadges={unifiedSourceBadges()}
|
||||
platformBadge={platformBadge()}
|
||||
sourceBadge={sourceBadge()}
|
||||
layoutMode={table.layoutMode()}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}>
|
||||
<TableCell
|
||||
classList={{
|
||||
hidden: table.isMobile() || !table.isVisible('supplementary'),
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Show
|
||||
when={resource.uptime}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity'
|
|||
import { shouldShowResourceAlternateName } from '@/utils/resourcePolicyPresentation';
|
||||
import { ResourceDetailDrawer } from './ResourceDetailDrawer';
|
||||
import { ResourceFacetSummary } from './ResourceFacetSummary';
|
||||
import { UnifiedResourceSourceBadgeCell } from './UnifiedResourceSourceBadgeCell';
|
||||
import {
|
||||
type UnifiedResourceTableProps,
|
||||
type UnifiedResourceTableState,
|
||||
|
|
@ -56,61 +57,61 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
|
|||
class={`text-left pl-2 sm:pl-3 ${table.resourceColumn().className}`}
|
||||
width={table.resourceColumn().width}
|
||||
>
|
||||
Resource
|
||||
{table.headerLabels().resource}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}
|
||||
class={table.serviceQueueColumn().className}
|
||||
width={table.serviceQueueColumn().width}
|
||||
>
|
||||
Queue
|
||||
{table.headerLabels().queue}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
class={table.serviceQueueColumn().className}
|
||||
width={table.serviceQueueColumn().width}
|
||||
>
|
||||
Deferred
|
||||
{table.headerLabels().deferred}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
|
||||
class={table.serviceQueueColumn().className}
|
||||
width={table.serviceQueueColumn().width}
|
||||
>
|
||||
Hold
|
||||
{table.headerLabels().hold}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
class={table.serviceCountColumn().className}
|
||||
width={table.serviceCountColumn().width}
|
||||
>
|
||||
Nodes
|
||||
{table.headerLabels().nodes}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={table.serviceHealthColumn().className}
|
||||
width={table.serviceHealthColumn().width}
|
||||
>
|
||||
Health
|
||||
{table.headerLabels().health}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
class={table.sourceColumn().className}
|
||||
width={table.sourceColumn().width}
|
||||
>
|
||||
Source
|
||||
{table.headerLabels().source}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}
|
||||
class={table.uptimeColumn().className}
|
||||
width={table.uptimeColumn().width}
|
||||
>
|
||||
Uptime
|
||||
{table.headerLabels().uptime}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={table.serviceActionColumn().className}
|
||||
width={table.serviceActionColumn().width}
|
||||
>
|
||||
Action
|
||||
{table.headerLabels().action}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -124,9 +125,7 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
|
|||
const displayName = createMemo(() =>
|
||||
getPreferredInfrastructureDisplayName(resource),
|
||||
);
|
||||
const serviceLink = createMemo(
|
||||
() => buildServiceDetailLinks(resource)[0] ?? null,
|
||||
);
|
||||
const serviceLink = createMemo(() => buildServiceDetailLinks(resource)[0] ?? null);
|
||||
const statusIndicator = createMemo(() =>
|
||||
getAgentStatusIndicator({ status: resource.status }),
|
||||
);
|
||||
|
|
@ -136,7 +135,6 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
|
|||
const unifiedSourceBadges = createMemo(() =>
|
||||
getUnifiedSourceBadges(table.getUnifiedSources(resource)),
|
||||
);
|
||||
const hasUnifiedSources = createMemo(() => unifiedSourceBadges().length > 0);
|
||||
const healthClass = createMemo(
|
||||
() =>
|
||||
getServiceHealthSummaryPresentation(resource.status, pmgRow()?.health)
|
||||
|
|
@ -220,18 +218,24 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}>
|
||||
<TableCell
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('primary') }}
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Show
|
||||
when={pmgRow()?.queue != null}
|
||||
fallback={<span class="text-xs text-slate-400">—</span>}
|
||||
>
|
||||
<span class={`text-xs font-medium ${queueClass()}`}>{pmgRow()!.queue}</span>
|
||||
<span class={`text-xs font-medium ${queueClass()}`}>
|
||||
{pmgRow()!.queue}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
|
||||
<TableCell
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Show
|
||||
when={pmgRow()?.deferred != null}
|
||||
|
|
@ -242,7 +246,11 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}>
|
||||
<TableCell
|
||||
classList={{
|
||||
hidden: table.isMobile() || !table.isVisible('supplementary'),
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Show
|
||||
when={pmgRow()?.hold != null}
|
||||
|
|
@ -253,7 +261,9 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
|
||||
<TableCell
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Show
|
||||
when={pmgRow()?.nodes != null}
|
||||
|
|
@ -277,41 +287,22 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
|
|||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Show
|
||||
when={hasUnifiedSources()}
|
||||
fallback={
|
||||
<>
|
||||
<Show when={platformBadge()}>
|
||||
{(badge) => (
|
||||
<span class={badge().classes} title={badge().title}>
|
||||
{badge().label}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={sourceBadge()}>
|
||||
{(badge) => (
|
||||
<span class={badge().classes} title={badge().title}>
|
||||
{badge().label}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<For each={unifiedSourceBadges()}>
|
||||
{(badge) => (
|
||||
<span class={badge.classes} title={badge.title}>
|
||||
{badge.label}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
<TableCell
|
||||
classList={{ hidden: table.isMobile() || !table.isVisible('secondary') }}
|
||||
>
|
||||
<UnifiedResourceSourceBadgeCell
|
||||
unifiedBadges={unifiedSourceBadges()}
|
||||
platformBadge={platformBadge()}
|
||||
sourceBadge={sourceBadge()}
|
||||
layoutMode={table.layoutMode()}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell classList={{ hidden: table.isMobile() || !table.isVisible('supplementary') }}>
|
||||
<TableCell
|
||||
classList={{
|
||||
hidden: table.isMobile() || !table.isVisible('supplementary'),
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<Show
|
||||
when={resource.uptime}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { For, Show, createMemo } from 'solid-js';
|
||||
import type { Component } from 'solid-js';
|
||||
import type { ResourceBadge } from '@/utils/resourceBadgePresentation';
|
||||
import type { UnifiedResourceTableLayoutMode } from './unifiedResourceTableStateModel';
|
||||
|
||||
interface UnifiedResourceSourceBadgeCellProps {
|
||||
unifiedBadges: ResourceBadge[];
|
||||
platformBadge: ResourceBadge | null;
|
||||
sourceBadge: ResourceBadge | null;
|
||||
layoutMode: UnifiedResourceTableLayoutMode;
|
||||
}
|
||||
|
||||
const getVisibleSourceBadgeLimit = (layoutMode: UnifiedResourceTableLayoutMode): number =>
|
||||
layoutMode === 'wide' ? 3 : 1;
|
||||
|
||||
export const UnifiedResourceSourceBadgeCell: Component<UnifiedResourceSourceBadgeCellProps> = (
|
||||
props,
|
||||
) => {
|
||||
const badges = createMemo(() => {
|
||||
if (props.unifiedBadges.length > 0) return props.unifiedBadges;
|
||||
return [props.platformBadge, props.sourceBadge].filter((badge): badge is ResourceBadge =>
|
||||
Boolean(badge),
|
||||
);
|
||||
});
|
||||
const visibleBadges = createMemo(() =>
|
||||
badges().slice(0, getVisibleSourceBadgeLimit(props.layoutMode)),
|
||||
);
|
||||
const hiddenBadgeCount = createMemo(() => Math.max(0, badges().length - visibleBadges().length));
|
||||
const title = createMemo(() =>
|
||||
badges()
|
||||
.map((badge) => badge.title ?? badge.label)
|
||||
.join(', '),
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="flex min-w-0 max-w-full items-center justify-center gap-1 overflow-hidden">
|
||||
<For each={visibleBadges()}>
|
||||
{(badge) => (
|
||||
<span
|
||||
class={`${badge.classes} min-w-0 max-w-full overflow-hidden px-1`}
|
||||
title={badge.title}
|
||||
>
|
||||
<span class="min-w-0 truncate">{badge.label}</span>
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
<Show when={hiddenBadgeCount() > 0}>
|
||||
<span
|
||||
class="inline-flex min-w-0 max-w-full items-center overflow-hidden rounded bg-surface-alt px-1 py-0.5 text-[10px] font-medium text-muted"
|
||||
title={title()}
|
||||
>
|
||||
+{hiddenBadgeCount()}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -45,10 +45,7 @@ globalThis.ResizeObserver = class ResizeObserver {
|
|||
} as unknown as typeof ResizeObserver;
|
||||
const emitResizeObserverWidth = (width: number) => {
|
||||
for (const callback of resizeObserverCallbacks) {
|
||||
callback(
|
||||
[{ contentRect: { width } } as ResizeObserverEntry],
|
||||
{} as ResizeObserver,
|
||||
);
|
||||
callback([{ contentRect: { width } } as ResizeObserverEntry], {} as ResizeObserver);
|
||||
}
|
||||
};
|
||||
if (typeof Element.prototype.scrollIntoView !== 'function') {
|
||||
|
|
@ -209,39 +206,44 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
expect(getByText('Net I/O')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
emitResizeObserverWidth(900);
|
||||
emitResizeObserverWidth(820);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Net I/O').closest('th')).toHaveClass('hidden');
|
||||
expect(getByText('Source').closest('th')).toHaveClass('hidden');
|
||||
expect(getByText('Net').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('Src').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('I/O').closest('th')).toHaveClass('hidden');
|
||||
expect(getByText('Up').closest('th')).toHaveClass('hidden');
|
||||
});
|
||||
expect(getByText('CPU').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('Memory')).toBeInTheDocument();
|
||||
expect(getByText('Mem')).toBeInTheDocument();
|
||||
expect(getByText('Disk').closest('th')).not.toHaveClass('hidden');
|
||||
|
||||
emitResizeObserverWidth(980);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Net I/O').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('Disk I/O').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('Src').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('Up').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('Temp').closest('th')).not.toHaveClass('hidden');
|
||||
});
|
||||
|
||||
emitResizeObserverWidth(1200);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Net I/O').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('Memory')).toBeInTheDocument();
|
||||
expect(getByText('Source').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('Uptime').closest('th')).toHaveClass('hidden');
|
||||
});
|
||||
|
||||
emitResizeObserverWidth(1400);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Uptime').closest('th')).not.toHaveClass('hidden');
|
||||
expect(getByText('Temp').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(() => (
|
||||
<ResourceFacetSummary
|
||||
counts={{
|
||||
recentChanges: 3,
|
||||
recentChangeKinds: {
|
||||
restart: 2,
|
||||
<ResourceFacetSummary
|
||||
counts={{
|
||||
recentChanges: 3,
|
||||
recentChangeKinds: {
|
||||
restart: 2,
|
||||
config_update: 1,
|
||||
metric_anomaly: 1,
|
||||
},
|
||||
|
|
@ -283,23 +285,33 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
expect(unifiedResourceTableSource).toContain('UnifiedResourceHostTableCard');
|
||||
expect(unifiedResourceTableSource).toContain('UnifiedResourceServiceInfrastructureCard');
|
||||
expect(unifiedResourceTableSource).toContain('data-summary-clear-surface');
|
||||
expect(unifiedResourceTableSource).not.toContain('const sortedPBSResources = createMemo(() =>');
|
||||
expect(unifiedResourceTableSource).not.toContain(
|
||||
'const sortedPBSResources = createMemo(() =>',
|
||||
);
|
||||
expect(unifiedResourceTableSource).not.toContain('const getOutlierEmphasis =');
|
||||
expect(unifiedResourceTableSource).not.toContain('const getPBSTableRow =');
|
||||
expect(unifiedResourceTableStateSource).toContain("from './unifiedResourceTableStateModel'");
|
||||
expect(unifiedResourceTableStateSource).toContain('buildHostTableItems');
|
||||
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableColumnPresentations');
|
||||
expect(unifiedResourceTableStateSource).toContain(
|
||||
'getUnifiedResourceTableColumnPresentations',
|
||||
);
|
||||
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableHeaderLabels');
|
||||
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableLayoutMode');
|
||||
expect(unifiedResourceTableStateSource).toContain('getUnifiedResourceTableShellClass');
|
||||
expect(unifiedResourceTableStateSource).toContain('useTableWindowing');
|
||||
expect(unifiedResourceTableStateSource).toContain('useUnifiedResourceTableViewportSync');
|
||||
expect(unifiedResourceTableStateSource).toContain('clearPinnedSummaryScope?: () => void;');
|
||||
expect(unifiedResourceTableStateSource).toContain('showHostClearAction');
|
||||
expect(unifiedResourceTableStateSource).toContain('showServiceClearAction');
|
||||
expect(unifiedResourceTableStateSource).not.toContain('const resourceColumnStyle = createMemo(() =>');
|
||||
expect(unifiedResourceTableStateSource).not.toContain(
|
||||
'const resourceColumnStyle = createMemo(() =>',
|
||||
);
|
||||
expect(unifiedResourceHostTableCardSource).not.toContain('style={');
|
||||
expect(unifiedResourcePBSTableSectionSource).not.toContain('style={');
|
||||
expect(unifiedResourcePMGTableSectionSource).not.toContain('style={');
|
||||
expect(unifiedResourceTableStateSource).not.toContain("const showGroupHeaders = props.groupingMode === 'grouped'");
|
||||
expect(unifiedResourceTableStateSource).not.toContain(
|
||||
"const showGroupHeaders = props.groupingMode === 'grouped'",
|
||||
);
|
||||
expect(unifiedResourceTableStateSource).not.toContain('const items: HostTableItem[] = [];');
|
||||
expect(unifiedResourceTableStateSource).not.toContain('window.addEventListener');
|
||||
expect(unifiedResourceTableStateSource).not.toContain('getBoundingClientRect');
|
||||
|
|
@ -314,6 +326,12 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
expect(unifiedResourceTableStateModelSource).toContain(
|
||||
'export const getUnifiedResourceTableColumnPresentations',
|
||||
);
|
||||
expect(unifiedResourceTableStateModelSource).toContain(
|
||||
'export const getUnifiedResourceTableHeaderLabels',
|
||||
);
|
||||
expect(unifiedResourceTableStateModelSource).toContain(
|
||||
'export const getUnifiedResourceTableLayoutMode',
|
||||
);
|
||||
expect(unifiedResourceTableStateModelSource).toContain(
|
||||
'export const getUnifiedResourceTableShellClass',
|
||||
);
|
||||
|
|
@ -324,7 +342,7 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
|
||||
it('keeps PBS activity projection in the shared table-model owner', () => {
|
||||
expect(unifiedResourceTableModelSource).toContain('getPbsActivitySummary');
|
||||
expect(unifiedResourcePBSTableSectionSource).toContain('Activity');
|
||||
expect(unifiedResourcePBSTableSectionSource).toContain('headerLabels().activity');
|
||||
expect(unifiedResourcePBSTableSectionSource).not.toContain('backupJobs');
|
||||
expect(unifiedResourcePBSTableSectionSource).not.toContain('syncJobs');
|
||||
expect(unifiedResourcePBSTableSectionSource).not.toContain('getPbsActivitySummary');
|
||||
|
|
@ -351,13 +369,13 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
expect(infrastructureSummaryStateSource).not.toContain(
|
||||
'const match = allSeries.find((series) => series.id === focused);',
|
||||
);
|
||||
expect(infrastructureSummaryStateSource).not.toContain(
|
||||
"displaySeries().map((series) => ({",
|
||||
);
|
||||
expect(infrastructureSummaryStateSource).not.toContain('displaySeries().map((series) => ({');
|
||||
expect(infrastructureSummaryStateSource).not.toContain(
|
||||
"isAwaitingFirstSample() ? 'Gathering first sample…' : 'Building trend history…'",
|
||||
);
|
||||
expect(infrastructureSummaryStateSource).not.toContain("fetchFailed() ? 'Trend data unavailable' : emptyHistoryLabel()");
|
||||
expect(infrastructureSummaryStateSource).not.toContain(
|
||||
"fetchFailed() ? 'Trend data unavailable' : emptyHistoryLabel()",
|
||||
);
|
||||
expect(infrastructureSummaryModelSource).toContain(
|
||||
'export function buildInfrastructureSummarySeries',
|
||||
);
|
||||
|
|
@ -380,7 +398,9 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
expect(frontendIndexCssSource).toContain('--color-summary-row-bg');
|
||||
expect(frontendIndexCssSource).toContain('--color-summary-row-accent');
|
||||
expect(summaryInteractionA11ySource).toContain('createSummaryInteractiveRowPreviewHandlers');
|
||||
expect(summaryInteractionA11ySource).toContain('createSummaryInteractiveActionKeydownHandler');
|
||||
expect(summaryInteractionA11ySource).toContain(
|
||||
'createSummaryInteractiveActionKeydownHandler',
|
||||
);
|
||||
expect(summaryRowActionButtonSource).toContain('SummaryRowActionButton');
|
||||
expect(summaryRowActionButtonSource).toContain('aria-controls');
|
||||
expect(summaryRowActionButtonSource).toContain('aria-expanded');
|
||||
|
|
@ -400,7 +420,9 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
|
||||
expect(unifiedResourceHostTableCardSource).not.toContain('kind="scope"');
|
||||
expect(unifiedResourceHostTableCardSource).toContain('SummaryTableCardHeader');
|
||||
expect(unifiedResourceHostTableCardSource).toContain('onClear={tableProps.clearPinnedSummaryScope}');
|
||||
expect(unifiedResourceHostTableCardSource).toContain(
|
||||
'onClear={tableProps.clearPinnedSummaryScope}',
|
||||
);
|
||||
expect(unifiedResourceServiceInfrastructureCardSource).toContain('SummaryTableCardHeader');
|
||||
expect(unifiedResourceServiceInfrastructureCardSource).toContain('showClearAction');
|
||||
expect(unifiedResourceServiceInfrastructureCardSource).toContain(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {
|
|||
getHostSpacerHeights,
|
||||
getNextUnifiedResourceTableSortState,
|
||||
getUnifiedResourceTableColumnPresentations,
|
||||
getUnifiedResourceTableHeaderLabels,
|
||||
getUnifiedResourceTableLayoutMode,
|
||||
getUnifiedResourceTableShellClass,
|
||||
getUnifiedResourceTableSortIndicator,
|
||||
getUnifiedSources,
|
||||
|
|
@ -80,9 +82,11 @@ describe('unifiedResourceTableStateModel', () => {
|
|||
);
|
||||
|
||||
expect(getVisibleHostTableItems(items, false, 1, 2)).toEqual(items);
|
||||
expect(getVisibleHostTableItems(items, true, 1, 3).map((item) =>
|
||||
item.type === 'row' ? item.resource.id : item.type,
|
||||
)).toEqual(['b', 'c']);
|
||||
expect(
|
||||
getVisibleHostTableItems(items, true, 1, 3).map((item) =>
|
||||
item.type === 'row' ? item.resource.id : item.type,
|
||||
),
|
||||
).toEqual(['b', 'c']);
|
||||
expect(getHostSpacerHeights(items.length, 1, 3, true, 40)).toEqual({
|
||||
top: 40,
|
||||
bottom: 0,
|
||||
|
|
@ -108,27 +112,37 @@ describe('unifiedResourceTableStateModel', () => {
|
|||
});
|
||||
|
||||
it('derives responsive column presentations and host-table visibility as pure layout policy', () => {
|
||||
const mobileColumns = getUnifiedResourceTableColumnPresentations(true);
|
||||
const desktopColumns = getUnifiedResourceTableColumnPresentations(false);
|
||||
const mobileColumns = getUnifiedResourceTableColumnPresentations('mobile');
|
||||
const tabletColumns = getUnifiedResourceTableColumnPresentations('tablet');
|
||||
const compactColumns = getUnifiedResourceTableColumnPresentations('compact');
|
||||
const wideColumns = getUnifiedResourceTableColumnPresentations('wide');
|
||||
|
||||
expect(getUnifiedResourceTableShellClass(true)).toContain('table-fixed');
|
||||
expect(getUnifiedResourceTableShellClass(true)).toContain('min-w-full');
|
||||
expect(getUnifiedResourceTableShellClass(false)).toContain('min-w-[max-content]');
|
||||
// Mobile uses percentage widths so the visible-column set fills the
|
||||
// viewport without horizontal overflow. Host rows render
|
||||
// Resource + CPU + Memory + Disk = 40 + 3×20 = 100%; service (PBS/PMG)
|
||||
// rows render Resource + Health + Action = 40 + 36 + 24 = 100%. Columns
|
||||
// hidden at mobile keep placeholder percentages that never paint.
|
||||
expect(getUnifiedResourceTableShellClass('mobile')).toContain('table-fixed');
|
||||
expect(getUnifiedResourceTableShellClass('mobile')).toContain('min-w-full');
|
||||
expect(getUnifiedResourceTableShellClass('compact')).toContain('min-w-full');
|
||||
expect(getUnifiedResourceTableShellClass('wide')).toContain('min-w-full');
|
||||
expect(getUnifiedResourceTableShellClass('wide')).not.toContain('min-w-[max-content]');
|
||||
// Mobile and tablet use percentage widths so the visible-column set fills
|
||||
// the table surface without horizontal overflow. Wider modes keep all
|
||||
// host columns visible while compressing their tracks before any
|
||||
// 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(desktopColumns.resourceColumn.className).toContain('min-w-[220px]');
|
||||
expect(desktopColumns.resourceColumn.className).toContain('max-w-[220px]');
|
||||
expect(desktopColumns.resourceColumn.width).toBe(220);
|
||||
expect(desktopColumns.metricColumn.width).toBe(144);
|
||||
expect(desktopColumns.ioColumn.width).toBe(192);
|
||||
expect(desktopColumns.sourceColumn.width).toBe(144);
|
||||
expect(tabletColumns.resourceColumn.width).toBe('26%');
|
||||
expect(tabletColumns.ioColumn.width).toBe('18%');
|
||||
expect(tabletColumns.sourceColumn.width).toBe('17%');
|
||||
expect(compactColumns.resourceColumn.width).toBe('18%');
|
||||
expect(compactColumns.metricColumn.width).toBe('10.5%');
|
||||
expect(compactColumns.tempColumn.width).toBe('7%');
|
||||
expect(wideColumns.resourceColumn.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(shouldShowUnifiedResourceHostTable(0, 0)).toBe(true);
|
||||
expect(shouldShowUnifiedResourceHostTable(0, 2)).toBe(false);
|
||||
expect(shouldShowUnifiedResourceHostTable(3, 2)).toBe(true);
|
||||
|
|
@ -137,20 +151,27 @@ describe('unifiedResourceTableStateModel', () => {
|
|||
it('derives infrastructure table breakpoints from the measured table surface width', () => {
|
||||
expect(normalizeUnifiedResourceTableLayoutWidth(820.4)).toBe(820);
|
||||
expect(normalizeUnifiedResourceTableLayoutWidth(null, 700)).toBe(700);
|
||||
expect(shouldUseUnifiedResourceTableMobileLayout(767)).toBe(true);
|
||||
expect(shouldUseUnifiedResourceTableMobileLayout(768)).toBe(false);
|
||||
expect(isUnifiedResourceTableColumnVisible('primary', 700)).toBe(true);
|
||||
expect(isUnifiedResourceTableColumnVisible('secondary', 1119)).toBe(false);
|
||||
expect(isUnifiedResourceTableColumnVisible('secondary', 1120)).toBe(true);
|
||||
expect(isUnifiedResourceTableColumnVisible('supplementary', 1359)).toBe(false);
|
||||
expect(isUnifiedResourceTableColumnVisible('supplementary', 1360)).toBe(true);
|
||||
expect(getUnifiedResourceTableLayoutMode(699)).toBe('mobile');
|
||||
expect(getUnifiedResourceTableLayoutMode(700)).toBe('tablet');
|
||||
expect(getUnifiedResourceTableLayoutMode(899)).toBe('tablet');
|
||||
expect(getUnifiedResourceTableLayoutMode(900)).toBe('compact');
|
||||
expect(getUnifiedResourceTableLayoutMode(1159)).toBe('compact');
|
||||
expect(getUnifiedResourceTableLayoutMode(1160)).toBe('wide');
|
||||
expect(shouldUseUnifiedResourceTableMobileLayout(699)).toBe(true);
|
||||
expect(shouldUseUnifiedResourceTableMobileLayout(700)).toBe(false);
|
||||
expect(isUnifiedResourceTableColumnVisible('primary', 640)).toBe(true);
|
||||
expect(isUnifiedResourceTableColumnVisible('secondary', 699)).toBe(false);
|
||||
expect(isUnifiedResourceTableColumnVisible('secondary', 700)).toBe(true);
|
||||
expect(isUnifiedResourceTableColumnVisible('supplementary', 899)).toBe(false);
|
||||
expect(isUnifiedResourceTableColumnVisible('supplementary', 900)).toBe(true);
|
||||
expect(isUnifiedResourceTableColumnVisible('detailed', 1159)).toBe(false);
|
||||
expect(isUnifiedResourceTableColumnVisible('detailed', 1160)).toBe(true);
|
||||
});
|
||||
|
||||
it('reads unified source tags from platform data without hook state', () => {
|
||||
expect(getUnifiedSources(makeResource('a', { platformData: { sources: ['proxmox', 'agent'] } }))).toEqual([
|
||||
'proxmox',
|
||||
'agent',
|
||||
]);
|
||||
expect(
|
||||
getUnifiedSources(makeResource('a', { platformData: { sources: ['proxmox', 'agent'] } })),
|
||||
).toEqual(['proxmox', 'agent']);
|
||||
expect(getUnifiedSources(makeResource('b', { platformData: {} }))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,15 +36,19 @@ export type HostTableItem = HostTableHeaderItem | HostTableResourceItem;
|
|||
export const HOST_TABLE_ESTIMATED_ROW_HEIGHT = 40;
|
||||
export const HOST_TABLE_WINDOW_SIZE = 137;
|
||||
export const UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH = 1024;
|
||||
export const UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH = 768;
|
||||
export const UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH = 700;
|
||||
export const UNIFIED_RESOURCE_TABLE_COMPACT_LAYOUT_WIDTH = 900;
|
||||
export const UNIFIED_RESOURCE_TABLE_WIDE_LAYOUT_WIDTH = 1160;
|
||||
export const UNIFIED_RESOURCE_TABLE_COLUMN_BREAKPOINTS: Record<ColumnPriority, number> = {
|
||||
essential: 0,
|
||||
primary: 640,
|
||||
secondary: 1120,
|
||||
supplementary: 1360,
|
||||
detailed: 1536,
|
||||
secondary: UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH,
|
||||
supplementary: UNIFIED_RESOURCE_TABLE_COMPACT_LAYOUT_WIDTH,
|
||||
detailed: UNIFIED_RESOURCE_TABLE_WIDE_LAYOUT_WIDTH,
|
||||
};
|
||||
|
||||
export type UnifiedResourceTableLayoutMode = 'mobile' | 'tablet' | 'compact' | 'wide';
|
||||
|
||||
export const normalizeUnifiedResourceTableLayoutWidth = (
|
||||
width: number | null | undefined,
|
||||
fallback: number = UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH,
|
||||
|
|
@ -58,8 +62,19 @@ export const normalizeUnifiedResourceTableLayoutWidth = (
|
|||
return UNIFIED_RESOURCE_TABLE_DEFAULT_LAYOUT_WIDTH;
|
||||
};
|
||||
|
||||
export const getUnifiedResourceTableLayoutMode = (
|
||||
layoutWidth: number,
|
||||
): UnifiedResourceTableLayoutMode => {
|
||||
const width = normalizeUnifiedResourceTableLayoutWidth(layoutWidth);
|
||||
|
||||
if (width < UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH) return 'mobile';
|
||||
if (width < UNIFIED_RESOURCE_TABLE_COMPACT_LAYOUT_WIDTH) return 'tablet';
|
||||
if (width < UNIFIED_RESOURCE_TABLE_WIDE_LAYOUT_WIDTH) return 'compact';
|
||||
return 'wide';
|
||||
};
|
||||
|
||||
export const shouldUseUnifiedResourceTableMobileLayout = (layoutWidth: number): boolean =>
|
||||
normalizeUnifiedResourceTableLayoutWidth(layoutWidth) < UNIFIED_RESOURCE_TABLE_MOBILE_LAYOUT_WIDTH;
|
||||
getUnifiedResourceTableLayoutMode(layoutWidth) === 'mobile';
|
||||
|
||||
export const isUnifiedResourceTableColumnVisible = (
|
||||
priority: ColumnPriority,
|
||||
|
|
@ -173,10 +188,7 @@ export const getUnifiedResourceTableSortIndicator = (
|
|||
return activeDirection === 'asc' ? '▲' : '▼';
|
||||
};
|
||||
|
||||
export const sortServiceResources = (
|
||||
services: Resource[],
|
||||
type: 'pbs' | 'pmg',
|
||||
): Resource[] =>
|
||||
export const sortServiceResources = (services: Resource[], type: 'pbs' | 'pmg'): Resource[] =>
|
||||
sortResources(
|
||||
services.filter((resource) => resource.type === type),
|
||||
'default',
|
||||
|
|
@ -211,6 +223,26 @@ export type UnifiedResourceTableColumnPresentations = {
|
|||
serviceActionColumn: UnifiedResourceTableColumnPresentation;
|
||||
};
|
||||
|
||||
export type UnifiedResourceTableHeaderLabels = {
|
||||
resource: string;
|
||||
cpu: string;
|
||||
memory: string;
|
||||
disk: string;
|
||||
network: string;
|
||||
diskIo: string;
|
||||
source: string;
|
||||
uptime: string;
|
||||
temp: string;
|
||||
datastores: string;
|
||||
activity: string;
|
||||
queue: string;
|
||||
deferred: string;
|
||||
hold: string;
|
||||
nodes: string;
|
||||
health: string;
|
||||
action: string;
|
||||
};
|
||||
|
||||
const buildUnifiedResourceTableColumnPresentation = (
|
||||
className: string,
|
||||
width?: string | number,
|
||||
|
|
@ -219,46 +251,139 @@ const buildUnifiedResourceTableColumnPresentation = (
|
|||
width,
|
||||
});
|
||||
|
||||
export const getUnifiedResourceTableShellClass = (isMobile: boolean): string =>
|
||||
`table-fixed ${isMobile ? 'min-w-full' : 'min-w-[max-content]'}`;
|
||||
export const getUnifiedResourceTableShellClass = (
|
||||
layoutMode: UnifiedResourceTableLayoutMode,
|
||||
): string => `table-fixed min-w-full${layoutMode === 'wide' ? '' : ' text-[11px] sm:text-xs'}`;
|
||||
|
||||
export const getUnifiedResourceTableHeaderLabels = (
|
||||
layoutMode: UnifiedResourceTableLayoutMode,
|
||||
): UnifiedResourceTableHeaderLabels => {
|
||||
if (layoutMode === 'wide') {
|
||||
return {
|
||||
resource: 'Resource',
|
||||
cpu: 'CPU',
|
||||
memory: 'Memory',
|
||||
disk: 'Disk',
|
||||
network: 'Net I/O',
|
||||
diskIo: 'Disk I/O',
|
||||
source: 'Source',
|
||||
uptime: 'Uptime',
|
||||
temp: 'Temp',
|
||||
datastores: 'Datastores',
|
||||
activity: 'Activity',
|
||||
queue: 'Queue',
|
||||
deferred: 'Deferred',
|
||||
hold: 'Hold',
|
||||
nodes: 'Nodes',
|
||||
health: 'Health',
|
||||
action: 'Action',
|
||||
};
|
||||
}
|
||||
|
||||
if (layoutMode === 'compact') {
|
||||
return {
|
||||
resource: 'Resource',
|
||||
cpu: 'CPU',
|
||||
memory: 'Mem',
|
||||
disk: 'Disk',
|
||||
network: 'Net I/O',
|
||||
diskIo: 'Disk I/O',
|
||||
source: 'Src',
|
||||
uptime: 'Up',
|
||||
temp: 'Temp',
|
||||
datastores: 'Stores',
|
||||
activity: 'Activity',
|
||||
queue: 'Queue',
|
||||
deferred: 'Def',
|
||||
hold: 'Hold',
|
||||
nodes: 'Nodes',
|
||||
health: 'Health',
|
||||
action: 'Open',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
resource: 'Resource',
|
||||
cpu: 'CPU',
|
||||
memory: 'Mem',
|
||||
disk: 'Disk',
|
||||
network: 'Net',
|
||||
diskIo: 'I/O',
|
||||
source: 'Src',
|
||||
uptime: 'Up',
|
||||
temp: 'C',
|
||||
datastores: 'Stores',
|
||||
activity: 'Jobs',
|
||||
queue: 'Queue',
|
||||
deferred: 'Def',
|
||||
hold: 'Hold',
|
||||
nodes: 'Nodes',
|
||||
health: 'Health',
|
||||
action: 'Open',
|
||||
};
|
||||
};
|
||||
|
||||
export const getUnifiedResourceTableColumnPresentations = (
|
||||
isMobile: boolean,
|
||||
): UnifiedResourceTableColumnPresentations => ({
|
||||
// Mobile widths are percentages so visible columns fill the viewport without
|
||||
// triggering horizontal scroll. Hidden columns retain placeholder widths that
|
||||
// never render. Host rows show Resource + CPU + Memory + Disk (40/20/20/20 =
|
||||
// 100%). Service (PBS/PMG) rows show Resource + Health + Action (40/36/24 =
|
||||
// 100%). See UnifiedResourceHostTableCard / UnifiedResourcePBSTableSection /
|
||||
// UnifiedResourcePMGTableSection for the matching visibility predicates.
|
||||
resourceColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '40%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[220px] max-w-[220px]', 220),
|
||||
metricColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '20%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[144px] max-w-[144px]', 144),
|
||||
ioColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '20%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[192px] max-w-[192px]', 192),
|
||||
sourceColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '20%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[144px] max-w-[144px]', 144),
|
||||
uptimeColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '15%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[80px] max-w-[80px]', 80),
|
||||
tempColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '15%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[60px] max-w-[70px]', 60),
|
||||
serviceCountColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '20%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[110px] max-w-[130px]', 110),
|
||||
serviceQueueColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '20%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[120px] max-w-[140px]', 120),
|
||||
serviceHealthColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '36%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[140px] max-w-[170px]', 140),
|
||||
serviceActionColumn: isMobile
|
||||
? buildUnifiedResourceTableColumnPresentation('', '24%')
|
||||
: buildUnifiedResourceTableColumnPresentation('min-w-[120px] max-w-[140px]', 120),
|
||||
});
|
||||
layoutMode: UnifiedResourceTableLayoutMode,
|
||||
): UnifiedResourceTableColumnPresentations => {
|
||||
if (layoutMode === 'mobile') {
|
||||
// Mobile widths are percentages so visible columns fill the viewport
|
||||
// without horizontal scroll. Hidden columns keep placeholder widths that
|
||||
// never render.
|
||||
return {
|
||||
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '40%'),
|
||||
metricColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
|
||||
ioColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
|
||||
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
|
||||
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '15%'),
|
||||
tempColumn: buildUnifiedResourceTableColumnPresentation('', '15%'),
|
||||
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
|
||||
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '20%'),
|
||||
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '36%'),
|
||||
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '24%'),
|
||||
};
|
||||
}
|
||||
|
||||
if (layoutMode === 'tablet') {
|
||||
return {
|
||||
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '26%'),
|
||||
metricColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
|
||||
ioColumn: buildUnifiedResourceTableColumnPresentation('', '18%'),
|
||||
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '17%'),
|
||||
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
|
||||
tempColumn: buildUnifiedResourceTableColumnPresentation('', '6%'),
|
||||
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
|
||||
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
|
||||
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '16%'),
|
||||
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '17%'),
|
||||
};
|
||||
}
|
||||
|
||||
if (layoutMode === 'compact') {
|
||||
return {
|
||||
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'),
|
||||
metricColumn: buildUnifiedResourceTableColumnPresentation('', '10.5%'),
|
||||
ioColumn: buildUnifiedResourceTableColumnPresentation('', '13%'),
|
||||
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'),
|
||||
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
|
||||
tempColumn: buildUnifiedResourceTableColumnPresentation('', '7%'),
|
||||
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'),
|
||||
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '9.5%'),
|
||||
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
|
||||
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '12%'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
resourceColumn: buildUnifiedResourceTableColumnPresentation('', '18%'),
|
||||
metricColumn: buildUnifiedResourceTableColumnPresentation('', '10.5%'),
|
||||
ioColumn: buildUnifiedResourceTableColumnPresentation('', '12.5%'),
|
||||
sourceColumn: buildUnifiedResourceTableColumnPresentation('', '10%'),
|
||||
uptimeColumn: buildUnifiedResourceTableColumnPresentation('', '8%'),
|
||||
tempColumn: buildUnifiedResourceTableColumnPresentation('', '7.5%'),
|
||||
serviceCountColumn: buildUnifiedResourceTableColumnPresentation('', '8.5%'),
|
||||
serviceQueueColumn: buildUnifiedResourceTableColumnPresentation('', '8.5%'),
|
||||
serviceHealthColumn: buildUnifiedResourceTableColumnPresentation('', '14%'),
|
||||
serviceActionColumn: buildUnifiedResourceTableColumnPresentation('', '16%'),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import {
|
|||
getHostSpacerHeights,
|
||||
getNextUnifiedResourceTableSortState,
|
||||
getUnifiedResourceTableColumnPresentations,
|
||||
getUnifiedResourceTableHeaderLabels,
|
||||
getUnifiedResourceTableLayoutMode,
|
||||
isUnifiedResourceTableColumnVisible,
|
||||
getUnifiedResourceTableShellClass,
|
||||
getUnifiedResourceTableSortIndicator,
|
||||
|
|
@ -101,6 +103,7 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
|
|||
const tableLayoutWidth = createMemo(() =>
|
||||
normalizeUnifiedResourceTableLayoutWidth(tableContainerWidth(), fallbackViewportWidth()),
|
||||
);
|
||||
const layoutMode = createMemo(() => getUnifiedResourceTableLayoutMode(tableLayoutWidth()));
|
||||
const isMobile = createMemo(() => shouldUseUnifiedResourceTableMobileLayout(tableLayoutWidth()));
|
||||
const isVisible = (priority: ColumnPriority) =>
|
||||
isUnifiedResourceTableColumnVisible(priority, tableLayoutWidth());
|
||||
|
|
@ -108,8 +111,12 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
|
|||
const split = createMemo(() => splitPrimaryAndServiceResources(props.resources));
|
||||
const primaryResources = createMemo(() => split().primaryResources);
|
||||
const serviceResources = createMemo(() => split().services);
|
||||
const primaryResourceIds = createMemo(() => new Set(primaryResources().map((resource) => resource.id)));
|
||||
const serviceResourceIds = createMemo(() => new Set(serviceResources().map((resource) => resource.id)));
|
||||
const primaryResourceIds = createMemo(
|
||||
() => new Set(primaryResources().map((resource) => resource.id)),
|
||||
);
|
||||
const serviceResourceIds = createMemo(
|
||||
() => new Set(serviceResources().map((resource) => resource.id)),
|
||||
);
|
||||
|
||||
const sortedResources = createMemo(() =>
|
||||
sortResources(primaryResources(), sortKey(), sortDirection()),
|
||||
|
|
@ -118,7 +125,9 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
|
|||
const resolveResourceLabel = (resourceId: string): string | undefined =>
|
||||
resourceLabelById().get(resourceId);
|
||||
|
||||
const groupedResources = createMemo(() => groupResources(sortedResources(), props.groupingMode ?? 'grouped'));
|
||||
const groupedResources = createMemo(() =>
|
||||
groupResources(sortedResources(), props.groupingMode ?? 'grouped'),
|
||||
);
|
||||
const hostTableItems = createMemo<HostTableItem[]>(() =>
|
||||
buildHostTableItems(groupedResources(), props.groupingMode),
|
||||
);
|
||||
|
|
@ -179,15 +188,18 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
|
|||
props.onExpandedResourceChange(props.expandedResourceId === resourceId ? null : resourceId);
|
||||
};
|
||||
|
||||
const tableShellClass = createMemo(() => getUnifiedResourceTableShellClass(isMobile()));
|
||||
const columnPresentations = createMemo(() => getUnifiedResourceTableColumnPresentations(isMobile()));
|
||||
const tableShellClass = createMemo(() => getUnifiedResourceTableShellClass(layoutMode()));
|
||||
const columnPresentations = createMemo(() =>
|
||||
getUnifiedResourceTableColumnPresentations(layoutMode()),
|
||||
);
|
||||
const headerLabels = createMemo(() => getUnifiedResourceTableHeaderLabels(layoutMode()));
|
||||
const showHostTable = createMemo(() =>
|
||||
shouldShowUnifiedResourceHostTable(primaryResources().length, serviceResources().length),
|
||||
);
|
||||
const showHostClearAction = createMemo(() =>
|
||||
Boolean(
|
||||
props.focusedSummaryGroupId ||
|
||||
(props.expandedResourceId && primaryResourceIds().has(props.expandedResourceId)),
|
||||
(props.expandedResourceId && primaryResourceIds().has(props.expandedResourceId)),
|
||||
),
|
||||
);
|
||||
const showServiceClearAction = createMemo(() =>
|
||||
|
|
@ -195,6 +207,7 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
|
|||
);
|
||||
|
||||
return {
|
||||
layoutMode,
|
||||
isMobile,
|
||||
isVisible,
|
||||
handleSort,
|
||||
|
|
@ -210,6 +223,7 @@ export function useUnifiedResourceTableState(props: UnifiedResourceTableProps) {
|
|||
ioScale,
|
||||
...viewportSync,
|
||||
tableShellClass,
|
||||
headerLabels,
|
||||
resourceColumn: () => columnPresentations().resourceColumn,
|
||||
metricColumn: () => columnPresentations().metricColumn,
|
||||
ioColumn: () => columnPresentations().ioColumn,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue