mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-21 05:30:35 +00:00
Canonicalize Proxmox web interface links
This commit is contained in:
parent
2169d94b41
commit
ddb5bd5e1a
9 changed files with 70 additions and 48 deletions
|
|
@ -2364,8 +2364,11 @@ instead of page-local table columns or one-off external-link anchors.
|
|||
`frontend-modern/src/components/shared/WebInterfaceNameLink.tsx` owns the
|
||||
resource-name link shell, new-tab safety attributes, row-click/key propagation
|
||||
containment, fallback text, and accessible launch labels. Workload guest rows
|
||||
and standalone machine rows compose that primitive so a saved URL is opened by
|
||||
clicking the resource name on every comparable runtime table.
|
||||
standalone machine rows, and Proxmox node rows compose that primitive so a saved
|
||||
or inferred URL is opened by clicking the resource name on every comparable
|
||||
runtime table. Runtime/platform tables must not add separate `Web` columns,
|
||||
page-local external-link anchors, or duplicated new-tab safety handling for
|
||||
that launch affordance.
|
||||
Shared-template drift enforcement is registry-backed:
|
||||
`frontend-modern/scripts/shared-template-registry.json` is the canonical list of
|
||||
standardized repeated affordances, required consumers, and forbidden local
|
||||
|
|
|
|||
|
|
@ -5199,6 +5199,7 @@
|
|||
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx",
|
||||
|
|
@ -5365,6 +5366,7 @@
|
|||
"frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx",
|
||||
"frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx",
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ cross-source deduplication.
|
|||
120. `frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx`
|
||||
121. `frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx`
|
||||
122. `frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx`
|
||||
122a. `frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts`
|
||||
123. `frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx`
|
||||
124. `frontend-modern/src/features/truenas/TrueNASAppsTable.tsx`
|
||||
125. `frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx`
|
||||
|
|
@ -222,6 +223,13 @@ affordance itself must compose
|
|||
`PlatformResourceDetailTableRow.tsx` contract. Future platform tables must not
|
||||
add page-local chevron buttons, bespoke `aria-expanded` handling, or local
|
||||
event-propagation variants for row detail expansion.
|
||||
The split also applies to web-interface launch affordances. Unified-resource
|
||||
tables own whether a row has a saved, inferred, or source-native web-interface
|
||||
URL and how that URL is derived, but the visible launch affordance belongs on
|
||||
the resource name through the shared `WebInterfaceNameLink` primitive. Proxmox
|
||||
host table columns are governed by `proxmoxHostTableModel.ts`; that model must
|
||||
not reintroduce a separate `Web` column or move the web-interface launch back
|
||||
into a page-local icon cell.
|
||||
Product-originated resource references may arrive as registered unified
|
||||
resource IDs, source-specific IDs, or canonical identity aliases. The
|
||||
unified-resource registry owns resolving those references through
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
{
|
||||
"id": "runtime-web-interface-name-link",
|
||||
"category": "resource-launch",
|
||||
"summary": "Runtime resource rows must launch saved web-interface URLs through the shared name-link template, not a page-local link or separate web column.",
|
||||
"summary": "Runtime resource rows must launch saved or inferred web-interface URLs through the shared name-link template, not a page-local link or separate web column.",
|
||||
"canonical": {
|
||||
"path": "src/components/shared/WebInterfaceNameLink.tsx",
|
||||
"export": "WebInterfaceNameLink"
|
||||
|
|
@ -15,6 +15,9 @@
|
|||
},
|
||||
{
|
||||
"path": "src/features/standalone/AgentsMachinesTable.tsx"
|
||||
},
|
||||
{
|
||||
"path": "src/features/proxmox/ProxmoxNodesTable.tsx"
|
||||
}
|
||||
],
|
||||
"forbiddenPatterns": [
|
||||
|
|
@ -34,12 +37,26 @@
|
|||
{
|
||||
"path": "src/features/standalone/agentMachineTableModel.ts",
|
||||
"patterns": ["id: 'web'", "label: 'Web'"]
|
||||
},
|
||||
{
|
||||
"path": "src/features/proxmox/ProxmoxNodesTable.tsx",
|
||||
"patterns": [
|
||||
"target=\"_blank\"",
|
||||
"rel=\"noopener noreferrer\"",
|
||||
"data-proxmox-host-web-link"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/features/proxmox/proxmoxHostTableModel.ts",
|
||||
"patterns": ["id: 'web'", "label: 'Web'"]
|
||||
}
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/features/standalone/__tests__/AgentsMachinesTable.test.tsx",
|
||||
"src/components/Workloads/__tests__/GuestRow.test.tsx"
|
||||
"src/components/Workloads/__tests__/GuestRow.test.tsx",
|
||||
"src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx",
|
||||
"src/features/proxmox/__tests__/proxmoxHostTableModel.test.ts"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ import dockerHostsTableSource from '@/features/docker/DockerHostsTable.tsx?raw';
|
|||
import kubernetesNodesTableSource from '@/features/kubernetes/KubernetesNodesTable.tsx?raw';
|
||||
import proxmoxCoverageTableSource from '@/features/proxmox/ProxmoxCoverageTable.tsx?raw';
|
||||
import proxmoxNodesTableSource from '@/features/proxmox/ProxmoxNodesTable.tsx?raw';
|
||||
import proxmoxHostTableModelSource from '@/features/proxmox/proxmoxHostTableModel.ts?raw';
|
||||
import vsphereHostsTableSource from '@/features/vmware/VsphereHostsTable.tsx?raw';
|
||||
import agentsMachinesTableSource from '@/features/standalone/AgentsMachinesTable.tsx?raw';
|
||||
import agentMachineTableModelSource from '@/features/standalone/agentMachineTableModel.ts?raw';
|
||||
|
|
@ -925,6 +926,7 @@ describe('shared primitive guardrails', () => {
|
|||
rules: Array<{
|
||||
id: string;
|
||||
canonical?: { path?: string; export?: string };
|
||||
requiredConsumers?: Array<{ path?: string }>;
|
||||
}>;
|
||||
};
|
||||
const registeredRule = registry.rules.find(
|
||||
|
|
@ -938,15 +940,27 @@ describe('shared primitive guardrails', () => {
|
|||
expect(webInterfaceNameLinkSource).toContain('rel="noopener noreferrer"');
|
||||
expect(webInterfaceNameLinkSource).toContain('event.stopPropagation()');
|
||||
expect(webInterfaceNameLinkSource).toContain('Open web interface for');
|
||||
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'src/components/Workloads/GuestRow.tsx',
|
||||
'src/features/standalone/AgentsMachinesTable.tsx',
|
||||
'src/features/proxmox/ProxmoxNodesTable.tsx',
|
||||
]),
|
||||
);
|
||||
|
||||
expect(guestRowSource).toContain('WebInterfaceNameLink');
|
||||
expect(agentsMachinesTableSource).toContain('WebInterfaceNameLink');
|
||||
expect(proxmoxNodesTableSource).toContain('WebInterfaceNameLink');
|
||||
expect(guestRowSource).not.toContain('target="_blank"');
|
||||
expect(agentsMachinesTableSource).not.toContain('target="_blank"');
|
||||
expect(proxmoxNodesTableSource).not.toContain('target="_blank"');
|
||||
expect(agentsMachinesTableSource).not.toContain('AgentMachineWebLinkCell');
|
||||
expect(agentsMachinesTableSource).not.toContain('data-agent-machine-web-link');
|
||||
expect(proxmoxNodesTableSource).not.toContain('data-proxmox-host-web-link');
|
||||
expect(agentMachineTableModelSource).not.toContain("id: 'web'");
|
||||
expect(agentMachineTableModelSource).not.toContain("label: 'Web'");
|
||||
expect(proxmoxHostTableModelSource).not.toContain("id: 'web'");
|
||||
expect(proxmoxHostTableModelSource).not.toContain("label: 'Web'");
|
||||
});
|
||||
|
||||
it('keeps platform table frames on the shared shell template', () => {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import {
|
|||
type Component,
|
||||
type JSX,
|
||||
} from 'solid-js';
|
||||
import ExternalLinkIcon from 'lucide-solid/icons/external-link';
|
||||
import { useWebSocket } from '@/contexts/appRuntime';
|
||||
import { useAlertsActivation } from '@/stores/alertsActivation';
|
||||
import { getAlertStyles } from '@/utils/alerts';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { WebInterfaceNameLink } from '@/components/shared/WebInterfaceNameLink';
|
||||
import { ResponsiveMetricCell } from '@/components/shared/responsive';
|
||||
import { NodeDrawer } from '@/components/Workloads/NodeDrawer';
|
||||
import { toDiscoveryConfig } from '@/components/Infrastructure/resourceDetailDiscoveryModel';
|
||||
|
|
@ -122,7 +122,7 @@ const formatPercentLabel = (value: number | null | undefined): string => {
|
|||
return `${Math.round(Math.max(0, normalized))}%`;
|
||||
};
|
||||
|
||||
type HostSortKey = Exclude<ProxmoxHostTableColumnId, 'web'>;
|
||||
type HostSortKey = ProxmoxHostTableColumnId;
|
||||
|
||||
const TEXT_SORT_KEYS = new Set<ProxmoxHostTableColumnId>(['node', 'version', 'cluster']);
|
||||
|
||||
|
|
@ -197,7 +197,6 @@ export const ProxmoxNodesTable: Component<{
|
|||
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
|
||||
|
||||
const handleSort = (column: ProxmoxHostTableColumnId) => {
|
||||
if (column === 'web') return;
|
||||
const key = column as HostSortKey;
|
||||
const defaultDirection = TEXT_SORT_KEYS.has(key) ? 'asc' : 'desc';
|
||||
// Cycle: default direction → flipped → cleared.
|
||||
|
|
@ -270,9 +269,7 @@ export const ProxmoxNodesTable: Component<{
|
|||
<For each={visibleColumns()}>
|
||||
{(column) => (
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind(column.kind)} ${
|
||||
column.id !== 'web' ? 'cursor-pointer hover:bg-surface-hover' : ''
|
||||
}`}
|
||||
class={`${getPlatformTableHeadClassForKind(column.kind)} cursor-pointer hover:bg-surface-hover`}
|
||||
aria-sort={
|
||||
sortKey() === column.id
|
||||
? sortDirection() === 'asc'
|
||||
|
|
@ -381,9 +378,13 @@ export const ProxmoxNodesTable: Component<{
|
|||
title={node.status || 'unknown'}
|
||||
ariaHidden
|
||||
/>
|
||||
<span class="truncate font-semibold text-base-content" title={name()}>
|
||||
{name()}
|
||||
</span>
|
||||
<WebInterfaceNameLink
|
||||
name={name()}
|
||||
url={externalUrl()}
|
||||
class="truncate font-semibold text-base-content transition-colors hover:text-sky-600 dark:hover:text-sky-400"
|
||||
fallbackClass="truncate font-semibold text-base-content"
|
||||
title={`Open ${name()} web interface`}
|
||||
/>
|
||||
<Show when={isOnline() && pendingUpdates() > 0}>
|
||||
<span
|
||||
class={`rounded px-1 py-0 text-[9px] font-medium whitespace-nowrap ${
|
||||
|
|
@ -551,28 +552,6 @@ export const ProxmoxNodesTable: Component<{
|
|||
</span>
|
||||
</TableCell>
|
||||
);
|
||||
case 'web':
|
||||
return (
|
||||
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
|
||||
<Show when={externalUrl()} fallback={<span class="text-muted">—</span>}>
|
||||
{(href) => (
|
||||
<a
|
||||
data-proxmox-host-web-link
|
||||
href={href()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex h-6 w-6 items-center justify-center rounded-md text-blue-600 transition-colors hover:bg-blue-50 hover:text-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60 dark:text-blue-400 dark:hover:bg-blue-950 dark:hover:text-blue-300"
|
||||
title={`Open ${name()} web interface`}
|
||||
aria-label={`Open ${name()} web interface`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<ExternalLinkIcon class="h-3.5 w-3.5" />
|
||||
</a>
|
||||
)}
|
||||
</Show>
|
||||
</TableCell>
|
||||
);
|
||||
default:
|
||||
column.id satisfies never;
|
||||
return <></>;
|
||||
|
|
|
|||
|
|
@ -101,9 +101,12 @@ describe('ProxmoxNodesTable', () => {
|
|||
/>
|
||||
));
|
||||
|
||||
const link = screen.getByRole('link', { name: 'Open pve-node-1 web interface' });
|
||||
const link = screen.getByRole('link', { name: 'Open web interface for pve-node-1' });
|
||||
expect(link).toHaveAttribute('href', 'https://pve.example.com:8006');
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveTextContent('pve-node-1');
|
||||
expect(screen.queryByRole('columnheader', { name: 'Web' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('proxmox-host-web-link')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('node-drawer')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -118,10 +121,9 @@ describe('ProxmoxNodesTable', () => {
|
|||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Open pve-node-1 web interface' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://pve-node-1:8006',
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'Open web interface for pve-node-1' }),
|
||||
).toHaveAttribute('href', 'https://pve-node-1:8006');
|
||||
});
|
||||
|
||||
it('restores the v5 row signals: alert accent, pending updates badge, full uptime, offline dimming', () => {
|
||||
|
|
|
|||
|
|
@ -46,10 +46,9 @@ describe('proxmoxHostTableModel', () => {
|
|||
'vms',
|
||||
'cts',
|
||||
'cluster',
|
||||
'web',
|
||||
]);
|
||||
expect(getProxmoxHostColumnWidthStyle('cluster', 'compact', compactIds)).toEqual({
|
||||
width: '10.3093%',
|
||||
width: '10.7527%',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ export type ProxmoxHostTableColumnId =
|
|||
| 'temp'
|
||||
| 'vms'
|
||||
| 'cts'
|
||||
| 'cluster'
|
||||
| 'web';
|
||||
| 'cluster';
|
||||
|
||||
export type ProxmoxHostTableColumn = {
|
||||
id: ProxmoxHostTableColumnId;
|
||||
|
|
@ -40,7 +39,6 @@ const HOST_COLUMN_MIN_LAYOUT: Record<ProxmoxHostTableColumnId, WorkloadTableLayo
|
|||
version: 'compact',
|
||||
uptime: 'compact',
|
||||
cluster: 'compact',
|
||||
web: 'compact',
|
||||
};
|
||||
|
||||
// CPU, memory, and disk render the same kind of usage bar, so they share
|
||||
|
|
@ -60,7 +58,6 @@ const HOST_COLUMN_DESKTOP_WIDTHS: Record<ProxmoxHostTableColumnId, number> = {
|
|||
vms: 5,
|
||||
cts: 5,
|
||||
cluster: 10,
|
||||
web: 4,
|
||||
};
|
||||
|
||||
const HOST_COLUMN_RESPONSIVE_WEIGHTS: Record<
|
||||
|
|
@ -90,7 +87,9 @@ const formatPercentage = (value: number): string => `${Number(value.toFixed(4))}
|
|||
// Column order follows the canonical recommended ordering documented in
|
||||
// columnAlignment.ts: identity → context → bars (CPU/Memory/Disk
|
||||
// contiguous) → diagnostic (Temp) → time (Uptime) → inventory counts
|
||||
// → external owner reference at end.
|
||||
// → source context at end. Web-interface launches live on the node name so
|
||||
// every runtime table uses the canonical shared name-link affordance instead
|
||||
// of adding a separate action column.
|
||||
export const PROXMOX_HOST_TABLE_COLUMNS: ProxmoxHostTableColumn[] = [
|
||||
{ id: 'node', label: 'Node', kind: 'name' },
|
||||
{ id: 'version', label: 'Version', kind: 'text' },
|
||||
|
|
@ -102,7 +101,6 @@ export const PROXMOX_HOST_TABLE_COLUMNS: ProxmoxHostTableColumn[] = [
|
|||
{ id: 'vms', label: 'VMs', kind: 'numeric-value' },
|
||||
{ id: 'cts', label: 'CTs', kind: 'numeric-value' },
|
||||
{ id: 'cluster', label: 'Cluster', kind: 'text' },
|
||||
{ id: 'web', label: 'Web', kind: 'badge' },
|
||||
];
|
||||
|
||||
export const getProxmoxHostVisibleColumnsForLayout = (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue