feat(catalyst-ui): rename InfrastructureTopology/Compute/Network/Storage files + testids

Renames the four Sovereign-Cloud sub-page files + classes + testids
(issue #309). The component contents stay otherwise unchanged in P1
— the force-graph rewrite (P2) and per-resource list pages (P3) are
separate phases.

Renames:
  InfrastructureTopology.tsx → Architecture.tsx
  InfrastructureTopology  → Architecture
  InfrastructureCompute.tsx → CloudCompute.tsx
  InfrastructureCompute   → CloudCompute
  InfrastructureNetwork.tsx → CloudNetwork.tsx
  InfrastructureNetwork   → CloudNetwork
  InfrastructureStorage.tsx → CloudStorage.tsx
  InfrastructureStorage   → CloudStorage

Testid prefix renames (data-testid + FlatTable testId props):
  infrastructure-topology-* → cloud-architecture-*
  infrastructure-compute-*  → cloud-compute-*
  infrastructure-network-*  → cloud-network-*
  infrastructure-storage-*  → cloud-storage-*
  infrastructure-pools-*    → cloud-pools-*
  infrastructure-pool-row-* → cloud-pool-row-*
  infrastructure-nodes-*    → cloud-nodes-*
  infrastructure-node-row-* → cloud-node-row-*
  infrastructure-pvcs-*     → cloud-pvcs-*
  infrastructure-pvc-row-*  → cloud-pvc-row-*
  infrastructure-buckets-*  → cloud-buckets-*
  infrastructure-bucket-row-* → cloud-bucket-row-*
  infrastructure-volumes-*  → cloud-volumes-*
  infrastructure-volume-row-* → cloud-volume-row-*
  infrastructure-lbs-*      → cloud-lbs-*
  infrastructure-lb-row-*   → cloud-lb-row-*
  infrastructure-peerings-* → cloud-peerings-*
  infrastructure-peering-row-* → cloud-peering-row-*
  infrastructure-firewalls-* → cloud-firewalls-*
  infrastructure-firewall-row-* → cloud-firewall-row-*
  infra-edge-*              → cloud-edge-*
  infra-node-*              → cloud-node-*
  infra-topology-arrow      → cloud-architecture-arrow

Modal testids (`infrastructure-modal-*`) are out of scope for P1 and
keep their current shape — those modal components are reused beyond
the Cloud surface.

Architecture sub-page user-visible strings updated:
  "Loading topology…" → "Loading architecture…"
  "Couldn't load topology" → "Couldn't load architecture"
  "Topology will appear here..." → "The cloud architecture will appear here..."
  aria-label: "Sovereign infrastructure topology" → "Sovereign cloud architecture"

Router imports + component references switched to the renamed
exports. Test files updated alongside.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hatiyildiz 2026-04-30 20:51:31 +02:00 committed by e3mrah
parent 344a8009df
commit 4ba99525f1
10 changed files with 205 additions and 209 deletions

View File

@ -25,10 +25,10 @@ import { JobsTimeline } from '@/pages/sovereign/JobsTimeline'
import { Dashboard } from '@/pages/sovereign/Dashboard'
import { BatchDetail } from '@/pages/sovereign/BatchDetail'
import { CloudPage } from '@/pages/sovereign/CloudPage'
import { InfrastructureTopology } from '@/pages/sovereign/InfrastructureTopology'
import { InfrastructureCompute } from '@/pages/sovereign/InfrastructureCompute'
import { InfrastructureStorage } from '@/pages/sovereign/InfrastructureStorage'
import { InfrastructureNetwork } from '@/pages/sovereign/InfrastructureNetwork'
import { Architecture } from '@/pages/sovereign/Architecture'
import { CloudCompute } from '@/pages/sovereign/CloudCompute'
import { CloudStorage } from '@/pages/sovereign/CloudStorage'
import { CloudNetwork } from '@/pages/sovereign/CloudNetwork'
// Root
const rootRoute = createRootRoute({ component: RootLayout })
@ -140,10 +140,9 @@ const provisionDashboardRoute = createRoute({
// /cloud redirects to the architecture sub-route so the URL shape is
// always explicit.
//
// The legacy /infrastructure/* routes below are preserved for now and
// render the same components — a follow-up commit converts them to
// 301-style redirects to the /cloud/* equivalents. Keeping both
// resolvable in this initial commit keeps the diff additive.
// The legacy /infrastructure/* paths below are preserved as
// redirect-only routes so deep links and bookmarks land on the
// renamed surface without 404'ing.
const provisionCloudRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/provision/$deploymentId/cloud',
@ -164,25 +163,25 @@ const provisionCloudIndexRoute = createRoute({
const provisionCloudArchitectureRoute = createRoute({
getParentRoute: () => provisionCloudRoute,
path: '/architecture',
component: InfrastructureTopology,
component: Architecture,
})
const provisionCloudComputeRoute = createRoute({
getParentRoute: () => provisionCloudRoute,
path: '/compute',
component: InfrastructureCompute,
component: CloudCompute,
})
const provisionCloudStorageRoute = createRoute({
getParentRoute: () => provisionCloudRoute,
path: '/storage',
component: InfrastructureStorage,
component: CloudStorage,
})
const provisionCloudNetworkRoute = createRoute({
getParentRoute: () => provisionCloudRoute,
path: '/network',
component: InfrastructureNetwork,
component: CloudNetwork,
})
// Legacy /infrastructure/* — every legacy path now redirects to its

View File

@ -1,6 +1,6 @@
/**
* _shared.tsx modal shell + form atoms used by every CRUD modal in
* the Sovereign Infrastructure surface (issue #228).
* the Sovereign Cloud surface (issue #309 supersedes #228).
*
* Per docs/INVIOLABLE-PRINCIPLES.md:
* #2 (no compromise) every CRUD modal speaks the same vocabulary

View File

@ -1,6 +1,7 @@
/**
* InfrastructureTopology.test.tsx render lock-in for the hierarchical
* Topology canvas (issue #228).
* Architecture.test.tsx render lock-in for the Sovereign Cloud /
* Architecture sub-page hierarchical canvas (issue #309 supersedes
* #228).
*
* Coverage:
* 1. Empty state shows when the tree has no nodes.
@ -25,7 +26,7 @@ import {
} from '@tanstack/react-router'
import { CloudPage } from './CloudPage'
import { InfrastructureTopology } from './InfrastructureTopology'
import { Architecture } from './Architecture'
import { infrastructureTopologyFixture } from '@/test/fixtures/infrastructure-topology.fixture'
import type { HierarchicalInfrastructure } from '@/lib/infrastructure.types'
import { useWizardStore } from '@/entities/deployment/store'
@ -48,7 +49,7 @@ function renderTopologyPage(data: HierarchicalInfrastructure) {
const architectureRoute = createRoute({
getParentRoute: () => cloudRoute,
path: '/architecture',
component: InfrastructureTopology,
component: Architecture,
})
const tree = rootRoute.addChildren([cloudRoute.addChildren([architectureRoute])])
const router = createRouter({
@ -69,7 +70,7 @@ function renderTopologyPage(data: HierarchicalInfrastructure) {
afterEach(() => cleanup())
describe('InfrastructureTopology — empty', () => {
describe('Architecture — empty', () => {
it('renders the Provisioning… overlay when the tree is empty', async () => {
const empty: HierarchicalInfrastructure = {
cloud: [],
@ -77,35 +78,35 @@ describe('InfrastructureTopology — empty', () => {
storage: { pvcs: [], buckets: [], volumes: [] },
}
renderTopologyPage(empty)
expect(await screen.findByTestId('infrastructure-topology-empty')).toBeTruthy()
expect(await screen.findByTestId('cloud-architecture-empty')).toBeTruthy()
})
})
describe('InfrastructureTopology — hierarchical render', () => {
describe('Architecture — hierarchical render', () => {
it('renders the canvas with all 4 depths', async () => {
renderTopologyPage(infrastructureTopologyFixture)
expect(await screen.findByTestId('infrastructure-topology-svg')).toBeTruthy()
expect(await screen.findByTestId('cloud-architecture-svg')).toBeTruthy()
// Depth 0 — cloud
expect(screen.getByTestId('infra-node-cloud-hetzner')).toBeTruthy()
expect(screen.getByTestId('cloud-node-cloud-hetzner')).toBeTruthy()
// Depth 1 — region
expect(screen.getByTestId('infra-node-region-eu-central')).toBeTruthy()
expect(screen.getByTestId('cloud-node-region-eu-central')).toBeTruthy()
// Depth 2 — cluster
expect(screen.getByTestId('infra-node-cluster-eu-central-primary')).toBeTruthy()
expect(screen.getByTestId('cloud-node-cluster-eu-central-primary')).toBeTruthy()
// Depth 3 — vcluster
expect(screen.getByTestId('infra-node-vc-eu-central-dmz')).toBeTruthy()
expect(screen.getByTestId('cloud-node-vc-eu-central-dmz')).toBeTruthy()
})
it('renders edges between parent and child', async () => {
renderTopologyPage(infrastructureTopologyFixture)
await screen.findByTestId('infrastructure-topology-svg')
expect(screen.getByTestId('infra-edge-cloud-hetzner-region-eu-central')).toBeTruthy()
expect(screen.getByTestId('infra-edge-region-eu-central-cluster-eu-central-primary')).toBeTruthy()
await screen.findByTestId('cloud-architecture-svg')
expect(screen.getByTestId('cloud-edge-cloud-hetzner-region-eu-central')).toBeTruthy()
expect(screen.getByTestId('cloud-edge-region-eu-central-cluster-eu-central-primary')).toBeTruthy()
})
it('opens the right-side detail panel on node click and closes it on dismiss', async () => {
renderTopologyPage(infrastructureTopologyFixture)
const node = await screen.findByTestId('infra-node-cluster-eu-central-primary')
const node = await screen.findByTestId('cloud-node-cluster-eu-central-primary')
expect(screen.queryByTestId('infrastructure-detail-panel')).toBeNull()
fireEvent.click(node)
expect(screen.getByTestId('infrastructure-detail-panel')).toBeTruthy()
@ -116,26 +117,26 @@ describe('InfrastructureTopology — hierarchical render', () => {
it('zooms in on a cluster click — vClusters lose data-dim=true', async () => {
renderTopologyPage(infrastructureTopologyFixture)
const cluster = await screen.findByTestId('infra-node-cluster-eu-central-primary')
const cluster = await screen.findByTestId('cloud-node-cluster-eu-central-primary')
// Before zoom — vClusters are dim by default.
const vcBefore = screen.getByTestId('infra-node-vc-eu-central-dmz')
const vcBefore = screen.getByTestId('cloud-node-vc-eu-central-dmz')
expect(vcBefore.getAttribute('data-dim')).toBe('true')
fireEvent.click(cluster)
// After zoom — vClusters of THIS cluster are bright.
const vcAfter = screen.getByTestId('infra-node-vc-eu-central-dmz')
const vcAfter = screen.getByTestId('cloud-node-vc-eu-central-dmz')
expect(vcAfter.getAttribute('data-dim')).toBe('false')
// Zoom-status banner is visible.
expect(screen.getByTestId('infrastructure-topology-zoom-status')).toBeTruthy()
expect(screen.getByTestId('cloud-architecture-zoom-status')).toBeTruthy()
})
})
describe('InfrastructureTopology — CRUD modal triggers', () => {
describe('Architecture — CRUD modal triggers', () => {
it('opens the Add Region modal when the top-level button is clicked', async () => {
renderTopologyPage(infrastructureTopologyFixture)
const btn = await screen.findByTestId('infrastructure-topology-add-region')
const btn = await screen.findByTestId('cloud-architecture-add-region')
fireEvent.click(btn)
const modal = screen.getByTestId('infrastructure-modal-add-region')
expect(modal).toBeTruthy()
@ -144,14 +145,14 @@ describe('InfrastructureTopology — CRUD modal triggers', () => {
it('opens the Add Cluster modal from a region detail panel', async () => {
renderTopologyPage(infrastructureTopologyFixture)
fireEvent.click(await screen.findByTestId('infra-node-region-eu-central'))
fireEvent.click(await screen.findByTestId('cloud-node-region-eu-central'))
fireEvent.click(screen.getByTestId('infrastructure-detail-panel-action-add-cluster'))
expect(screen.getByTestId('infrastructure-modal-add-cluster')).toBeTruthy()
})
it('opens the Add vCluster modal from a cluster detail panel', async () => {
renderTopologyPage(infrastructureTopologyFixture)
fireEvent.click(await screen.findByTestId('infra-node-cluster-eu-central-primary'))
fireEvent.click(await screen.findByTestId('cloud-node-cluster-eu-central-primary'))
fireEvent.click(screen.getByTestId('infrastructure-detail-panel-action-add-vcluster'))
expect(screen.getByTestId('infrastructure-modal-add-vcluster')).toBeTruthy()
})

View File

@ -1,24 +1,19 @@
/**
* InfrastructureTopology hierarchical layered SVG canvas for the
* Sovereign Infrastructure Topology tab (default landing).
* Architecture hierarchical layered SVG canvas for the Sovereign
* Cloud / Architecture sub-page (default landing under /cloud).
*
* Per founder spec (issue #228):
* 4 visual depths: Cloud Region Cluster vCluster
* Click node graph zooms in (NOT accordion)
* vClusters render dim until their parent cluster is zoomed
* Right-side detail panel slides in (InfrastructureDetailPanel)
* Layered, NOT force-directed pure topologyLayout in
* `@/lib/topologyLayout`
* The four sub-pages (Architecture / Compute / Network / Storage) are
* filtered lenses over ONE backend response. Architecture reads the
* hierarchical tree directly; the others use flatten* helpers.
*
* The 4 tabs (Topology / Compute / Storage / Network) are filtered
* lenses over ONE backend response. Topology reads the tree directly;
* the others use flatten* helpers.
* The graph rewrite (force-directed layout, relations beyond pure
* containment) is tracked separately as P2 of issue #309 this file
* keeps the existing layered topologyLayout intact in P1; only the
* file name, class name, and user-visible testids change.
*
* Per docs/INVIOLABLE-PRINCIPLES.md:
* #2 (no compromise) pure layout function, no `reactflow`, no
* simulation.
* #4 (never hardcode) every status colour comes from the
* `--color-*` CSS variables the rest of the portal uses.
* Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) every
* status colour comes from the `--color-*` CSS variables the rest of
* the portal uses.
*/
import { useMemo, useState } from 'react'
@ -61,7 +56,7 @@ interface ModalState {
| 'delete'
}
export function InfrastructureTopology() {
export function Architecture() {
const { deploymentId, data, isLoading, isError, refetch } = useCloud()
const [zoom, setZoom] = useState<ZoomState>({
@ -162,10 +157,10 @@ export function InfrastructureTopology() {
const hasNodes = !!layout && layout.nodes.length > 0
return (
<div data-testid="infrastructure-topology" className="relative">
<div data-testid="cloud-architecture" className="relative">
{(zoom.zoomedClusterId || zoom.zoomedRegionId) && (
<div
data-testid="infrastructure-topology-zoom-status"
data-testid="cloud-architecture-zoom-status"
className="mb-2 flex items-center justify-between rounded-md border border-[var(--color-border)] bg-[var(--color-bg-2)] px-3 py-1.5 text-xs"
>
<span className="text-[var(--color-text-dim)]">
@ -175,7 +170,7 @@ export function InfrastructureTopology() {
</span>
<button
type="button"
data-testid="infrastructure-topology-zoom-reset"
data-testid="cloud-architecture-zoom-reset"
onClick={() => setZoom({ zoomedClusterId: null, zoomedRegionId: null })}
className="rounded-md border border-[var(--color-border)] bg-transparent px-2 py-0.5 text-xs text-[var(--color-text)] hover:bg-[var(--color-bg)]"
>
@ -186,25 +181,25 @@ export function InfrastructureTopology() {
<div
className="relative w-full overflow-auto rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-2)]"
data-testid="infrastructure-topology-canvas"
data-testid="cloud-architecture-canvas"
style={{ minHeight: 480 }}
>
{isLoading && (
<div
className="flex h-[480px] items-center justify-center text-sm text-[var(--color-text-dim)]"
data-testid="infrastructure-topology-loading"
data-testid="cloud-architecture-loading"
>
Loading topology
Loading architecture
</div>
)}
{isError && !data && (
<div
className="flex h-[480px] flex-col items-center justify-center gap-2 px-6 text-center text-sm"
data-testid="infrastructure-topology-error"
data-testid="cloud-architecture-error"
>
<p className="font-medium text-[var(--color-danger)]">
Couldn&rsquo;t load topology
Couldn&rsquo;t load architecture
</p>
<p className="text-[var(--color-text-dim)]">
The Catalyst API is temporarily unreachable. Retry will start
@ -223,30 +218,30 @@ export function InfrastructureTopology() {
{!hasNodes && !isLoading && !isError && (
<div
className="flex h-[480px] flex-col items-center justify-center gap-2 px-6 text-center text-sm"
data-testid="infrastructure-topology-empty"
data-testid="cloud-architecture-empty"
>
<div className="h-3 w-3 animate-spin rounded-full border-2 border-[var(--color-accent)] border-t-transparent" />
<p className="font-medium text-[var(--color-text)]">Provisioning&hellip;</p>
<p className="text-[var(--color-text-dim)]">
Topology will appear here as soon as the Sovereign cluster
reports its first nodes.
The cloud architecture will appear here as soon as the
Sovereign cluster reports its first nodes.
</p>
</div>
)}
{hasNodes && layout && (
<svg
data-testid="infrastructure-topology-svg"
data-testid="cloud-architecture-svg"
width={layout.width}
height={layout.height}
viewBox={`0 0 ${layout.width} ${layout.height}`}
role="img"
aria-label="Sovereign infrastructure topology"
aria-label="Sovereign cloud architecture"
style={{ display: 'block', minWidth: '100%' }}
>
<defs>
<marker
id="infra-topology-arrow"
id="cloud-architecture-arrow"
viewBox="0 0 10 10"
refX="9"
refY="5"
@ -259,7 +254,7 @@ export function InfrastructureTopology() {
</defs>
{/* Depth row labels — anchor the layered intent. */}
<g data-testid="infrastructure-topology-depth-labels">
<g data-testid="cloud-architecture-depth-labels">
{(['Cloud', 'Region', 'Cluster', 'vCluster'] as const).map((label, i) => {
const sample = layout.nodes.find((n) => n.depth === i)
if (!sample) return null
@ -280,23 +275,23 @@ export function InfrastructureTopology() {
</g>
{/* Edges first so they sit beneath the nodes. */}
<g data-testid="infrastructure-topology-edges">
<g data-testid="cloud-architecture-edges">
{layout.edges.map((e) => (
<polyline
key={e.id}
data-testid={`infra-edge-${e.fromId}-${e.toId}`}
data-testid={`cloud-edge-${e.fromId}-${e.toId}`}
points={e.points.map((p) => `${p.x},${p.y}`).join(' ')}
fill="none"
stroke="var(--color-border-strong)"
strokeWidth={1.5}
markerEnd="url(#infra-topology-arrow)"
markerEnd="url(#cloud-architecture-arrow)"
opacity={0.6}
/>
))}
</g>
{/* Nodes. */}
<g data-testid="infrastructure-topology-nodes">
<g data-testid="cloud-architecture-nodes">
{layout.nodes.map((n) => {
const fill = STATUS_FILL[n.status]
const ring = STATUS_RING[n.status]
@ -304,7 +299,7 @@ export function InfrastructureTopology() {
return (
<g
key={n.id}
data-testid={`infra-node-${n.id}`}
data-testid={`cloud-node-${n.id}`}
data-kind={n.kind}
data-depth={n.depth}
data-status={n.status}
@ -363,12 +358,12 @@ export function InfrastructureTopology() {
{/* Top-level Add Region button visible at all times when the
tree has at least one cloud. Founder spec: every CRUD action
must be reachable from the Topology view. */}
must be reachable from the Architecture canvas. */}
{hasNodes && data && (
<div className="mt-3 flex items-center justify-end gap-2">
<button
type="button"
data-testid="infrastructure-topology-add-region"
data-testid="cloud-architecture-add-region"
onClick={() => setModal({ kind: 'add-region' })}
className="rounded-md border border-[var(--color-border)] bg-[var(--color-bg-2)] px-3 py-1.5 text-xs font-medium text-[var(--color-text)] hover:bg-[var(--color-bg)]"
>

View File

@ -1,5 +1,5 @@
/**
* InfrastructureCompute.test.tsx render lock-in for the Compute tab.
* CloudCompute.test.tsx render lock-in for the Compute tab.
*
* Coverage:
* 1. Empty state shows when the tree has no clusters / nodes.
@ -21,7 +21,7 @@ import {
} from '@tanstack/react-router'
import { CloudPage } from './CloudPage'
import { InfrastructureCompute } from './InfrastructureCompute'
import { CloudCompute } from './CloudCompute'
import { infrastructureTopologyFixture } from '@/test/fixtures/infrastructure-topology.fixture'
import type { HierarchicalInfrastructure } from '@/lib/infrastructure.types'
import { useWizardStore } from '@/entities/deployment/store'
@ -44,7 +44,7 @@ function renderComputePage(data: HierarchicalInfrastructure) {
const computeRoute = createRoute({
getParentRoute: () => cloudRoute,
path: '/compute',
component: InfrastructureCompute,
component: CloudCompute,
})
const tree = rootRoute.addChildren([cloudRoute.addChildren([computeRoute])])
const router = createRouter({
@ -65,7 +65,7 @@ function renderComputePage(data: HierarchicalInfrastructure) {
afterEach(() => cleanup())
describe('InfrastructureCompute — empty', () => {
describe('CloudCompute — empty', () => {
it('renders the empty state when there are no clusters or nodes', async () => {
const empty: HierarchicalInfrastructure = {
cloud: [],
@ -73,36 +73,36 @@ describe('InfrastructureCompute — empty', () => {
storage: { pvcs: [], buckets: [], volumes: [] },
}
renderComputePage(empty)
expect(await screen.findByTestId('infrastructure-compute-empty')).toBeTruthy()
expect(await screen.findByTestId('cloud-compute-empty')).toBeTruthy()
})
})
describe('InfrastructureCompute — populated', () => {
describe('CloudCompute — populated', () => {
it('renders the Pools and Nodes tables', async () => {
renderComputePage(infrastructureTopologyFixture)
expect(await screen.findByTestId('infrastructure-pools-table')).toBeTruthy()
expect(screen.getByTestId('infrastructure-nodes-table')).toBeTruthy()
expect(screen.getByTestId('infrastructure-pools-count').textContent).toBe('3')
expect(await screen.findByTestId('cloud-pools-table')).toBeTruthy()
expect(screen.getByTestId('cloud-nodes-table')).toBeTruthy()
expect(screen.getByTestId('cloud-pools-count').textContent).toBe('3')
// 4 nodes in cluster-eu-central + 2 in helsinki = 6 total
expect(screen.getByTestId('infrastructure-nodes-count').textContent).toBe('6')
expect(screen.getByTestId('cloud-nodes-count').textContent).toBe('6')
})
it('renders the bulk-actions strip', async () => {
renderComputePage(infrastructureTopologyFixture)
expect(await screen.findByTestId('infrastructure-compute-bulk')).toBeTruthy()
expect(screen.getByTestId('infrastructure-compute-bulk-scale')).toBeTruthy()
expect(screen.getByTestId('infrastructure-compute-bulk-drain')).toBeTruthy()
expect(await screen.findByTestId('cloud-compute-bulk')).toBeTruthy()
expect(screen.getByTestId('cloud-compute-bulk-scale')).toBeTruthy()
expect(screen.getByTestId('cloud-compute-bulk-drain')).toBeTruthy()
})
it('opens ScalePoolModal when row-level Scale is clicked', async () => {
renderComputePage(infrastructureTopologyFixture)
fireEvent.click(await screen.findByTestId('infrastructure-pool-row-pool-eu-cp-scale'))
fireEvent.click(await screen.findByTestId('cloud-pool-row-pool-eu-cp-scale'))
expect(screen.getByTestId('infrastructure-modal-scale-pool')).toBeTruthy()
})
it('opens NodeActionConfirm (drain) when row-level Drain is clicked', async () => {
renderComputePage(infrastructureTopologyFixture)
fireEvent.click(await screen.findByTestId('infrastructure-node-row-node-eu-w-0-drain'))
fireEvent.click(await screen.findByTestId('cloud-node-row-node-eu-w-0-drain'))
expect(screen.getByTestId('infrastructure-modal-node-drain')).toBeTruthy()
})
})

View File

@ -1,11 +1,10 @@
/**
* InfrastructureCompute Compute tab. Flat table grouped by [Cluster ·
* Node Pool], reads off the shared infrastructure tree provided by
* InfrastructurePage.
* CloudCompute Sovereign Cloud / Compute sub-page. Flat table
* grouped by [Cluster · Node Pool], reads off the shared
* infrastructure tree provided by CloudPage.
*
* Per founder spec (issue #228): "Compute flat table grouped
* [Cluster · Node Pool], each row links back to topology. Bulk
* actions: scale, drain."
* Per founder spec (issue #309 supersedes #228): each row links back
* to the Architecture canvas. Bulk actions: scale, drain.
*/
import { useMemo, useState } from 'react'
@ -32,7 +31,7 @@ interface NodeRow {
region: RegionSpec
}
export function InfrastructureCompute() {
export function CloudCompute() {
const { deploymentId, data, isLoading } = useCloud()
const { pools, nodes } = useMemo(() => {
@ -61,18 +60,18 @@ export function InfrastructureCompute() {
const isEmpty = !isLoading && pools.length === 0 && nodes.length === 0
return (
<div data-testid="infrastructure-compute">
<div data-testid="cloud-compute">
{isLoading && (
<div
className="flex h-48 items-center justify-center text-sm text-[var(--color-text-dim)]"
data-testid="infrastructure-compute-loading"
data-testid="cloud-compute-loading"
>
Loading compute resources
</div>
)}
{isEmpty && (
<div className="infra-empty" data-testid="infrastructure-compute-empty">
<div className="infra-empty" data-testid="cloud-compute-empty">
<p className="title">No clusters or worker nodes yet.</p>
<p className="sub">
Once the Sovereign cluster comes up, every k3s cluster and node VM
@ -83,11 +82,11 @@ export function InfrastructureCompute() {
{!isEmpty && data && (
<>
<div className="infra-bulk-actions" data-testid="infrastructure-compute-bulk">
<div className="infra-bulk-actions" data-testid="cloud-compute-bulk">
<span className="label">Bulk · {selectedPools.length} pool{selectedPools.length === 1 ? '' : 's'} / {selectedNodes.length} node{selectedNodes.length === 1 ? '' : 's'}</span>
<button
type="button"
data-testid="infrastructure-compute-bulk-scale"
data-testid="cloud-compute-bulk-scale"
disabled={selectedPools.length !== 1}
onClick={() => {
const row = pools.find((p) => p.pool.id === selectedPools[0])
@ -98,7 +97,7 @@ export function InfrastructureCompute() {
</button>
<button
type="button"
data-testid="infrastructure-compute-bulk-drain"
data-testid="cloud-compute-bulk-drain"
disabled={selectedNodes.length !== 1}
onClick={() => {
const row = nodes.find((n) => n.node.id === selectedNodes[0])
@ -109,29 +108,29 @@ export function InfrastructureCompute() {
</button>
</div>
<section className="infra-section" data-testid="infrastructure-pools-section">
<section className="infra-section" data-testid="cloud-pools-section">
<h2>
Node Pools <span className="count" data-testid="infrastructure-pools-count">{pools.length}</span>
Node Pools <span className="count" data-testid="cloud-pools-count">{pools.length}</span>
</h2>
<FlatTable
testId="infrastructure-pools-table"
testId="cloud-pools-table"
headers={['', 'Cluster', 'Pool', 'SKU', 'Replicas', 'Status', '']}
>
{pools.map(({ pool, cluster, region }) => (
<tr key={pool.id} data-testid={`infrastructure-pool-row-${pool.id}`}>
<tr key={pool.id} data-testid={`cloud-pool-row-${pool.id}`}>
<td>
<input
type="checkbox"
checked={selectedPools.includes(pool.id)}
onChange={(e) => toggle(e.target.checked, pool.id, setSelectedPools)}
data-testid={`infrastructure-pool-row-${pool.id}-select`}
data-testid={`cloud-pool-row-${pool.id}-select`}
/>
</td>
<td>
<Link
to={`/provision/$deploymentId/cloud/architecture` as never}
params={{ deploymentId } as never}
data-testid={`infrastructure-pool-row-${pool.id}-cluster-link`}
data-testid={`cloud-pool-row-${pool.id}-cluster-link`}
>
{cluster.name}
</Link>
@ -149,7 +148,7 @@ export function InfrastructureCompute() {
<button
type="button"
onClick={() => setScalePool({ pool, cluster, region })}
data-testid={`infrastructure-pool-row-${pool.id}-scale`}
data-testid={`cloud-pool-row-${pool.id}-scale`}
style={rowBtn}
>
Scale
@ -157,7 +156,7 @@ export function InfrastructureCompute() {
<button
type="button"
onClick={() => setChangeSku({ pool, cluster, region })}
data-testid={`infrastructure-pool-row-${pool.id}-change-sku`}
data-testid={`cloud-pool-row-${pool.id}-change-sku`}
style={rowBtn}
>
Change SKU
@ -192,7 +191,7 @@ export function InfrastructureCompute() {
provider: region.provider as CloudProvider,
})
}
data-testid={`infrastructure-pool-add-for-${cluster.id}`}
data-testid={`cloud-pool-add-for-${cluster.id}`}
>
+ Add pool to {cluster.name}
</button>
@ -201,22 +200,22 @@ export function InfrastructureCompute() {
</div>
</section>
<section className="infra-section" data-testid="infrastructure-nodes-section">
<section className="infra-section" data-testid="cloud-nodes-section">
<h2>
Worker Nodes <span className="count" data-testid="infrastructure-nodes-count">{nodes.length}</span>
Worker Nodes <span className="count" data-testid="cloud-nodes-count">{nodes.length}</span>
</h2>
<FlatTable
testId="infrastructure-nodes-table"
testId="cloud-nodes-table"
headers={['', 'Cluster', 'Node', 'SKU', 'Role', 'IP', 'Status', '']}
>
{nodes.map(({ node, cluster }) => (
<tr key={node.id} data-testid={`infrastructure-node-row-${node.id}`}>
<tr key={node.id} data-testid={`cloud-node-row-${node.id}`}>
<td>
<input
type="checkbox"
checked={selectedNodes.includes(node.id)}
onChange={(e) => toggle(e.target.checked, node.id, setSelectedNodes)}
data-testid={`infrastructure-node-row-${node.id}-select`}
data-testid={`cloud-node-row-${node.id}-select`}
/>
</td>
<td>{cluster.name}</td>
@ -231,7 +230,7 @@ export function InfrastructureCompute() {
<button
type="button"
onClick={() => setDrainNode({ node, cluster, region: data.topology.regions.find((r) => r.clusters.some((c) => c.id === cluster.id))! })}
data-testid={`infrastructure-node-row-${node.id}-drain`}
data-testid={`cloud-node-row-${node.id}-drain`}
style={rowBtn}
>
Drain

View File

@ -1,5 +1,5 @@
/**
* InfrastructureNetwork.test.tsx render lock-in for the Network tab.
* CloudNetwork.test.tsx render lock-in for the Network tab.
*
* Coverage:
* 1. Empty state.
@ -22,7 +22,7 @@ import {
} from '@tanstack/react-router'
import { CloudPage } from './CloudPage'
import { InfrastructureNetwork } from './InfrastructureNetwork'
import { CloudNetwork } from './CloudNetwork'
import { infrastructureTopologyFixture } from '@/test/fixtures/infrastructure-topology.fixture'
import type { HierarchicalInfrastructure } from '@/lib/infrastructure.types'
import { useWizardStore } from '@/entities/deployment/store'
@ -45,7 +45,7 @@ function renderNetworkPage(data: HierarchicalInfrastructure) {
const networkRoute = createRoute({
getParentRoute: () => cloudRoute,
path: '/network',
component: InfrastructureNetwork,
component: CloudNetwork,
})
const tree = rootRoute.addChildren([cloudRoute.addChildren([networkRoute])])
const router = createRouter({
@ -66,7 +66,7 @@ function renderNetworkPage(data: HierarchicalInfrastructure) {
afterEach(() => cleanup())
describe('InfrastructureNetwork — empty', () => {
describe('CloudNetwork — empty', () => {
it('renders the empty state when no LBs / peerings / firewalls exist', async () => {
const empty: HierarchicalInfrastructure = {
cloud: [],
@ -74,43 +74,43 @@ describe('InfrastructureNetwork — empty', () => {
storage: { pvcs: [], buckets: [], volumes: [] },
}
renderNetworkPage(empty)
expect(await screen.findByTestId('infrastructure-network-empty')).toBeTruthy()
expect(await screen.findByTestId('cloud-network-empty')).toBeTruthy()
})
})
describe('InfrastructureNetwork — populated', () => {
describe('CloudNetwork — populated', () => {
it('renders LB / Peering / Firewall tables with counts', async () => {
renderNetworkPage(infrastructureTopologyFixture)
expect(await screen.findByTestId('infrastructure-lbs-table')).toBeTruthy()
expect(screen.getByTestId('infrastructure-peerings-table')).toBeTruthy()
expect(screen.getByTestId('infrastructure-firewalls-table')).toBeTruthy()
expect(screen.getByTestId('infrastructure-lbs-count').textContent).toBe('1')
expect(screen.getByTestId('infrastructure-peerings-count').textContent).toBe('1')
expect(screen.getByTestId('infrastructure-firewalls-count').textContent).toBe('1')
expect(await screen.findByTestId('cloud-lbs-table')).toBeTruthy()
expect(screen.getByTestId('cloud-peerings-table')).toBeTruthy()
expect(screen.getByTestId('cloud-firewalls-table')).toBeTruthy()
expect(screen.getByTestId('cloud-lbs-count').textContent).toBe('1')
expect(screen.getByTestId('cloud-peerings-count').textContent).toBe('1')
expect(screen.getByTestId('cloud-firewalls-count').textContent).toBe('1')
})
it('renders the bulk-actions strip', async () => {
renderNetworkPage(infrastructureTopologyFixture)
expect(await screen.findByTestId('infrastructure-network-bulk')).toBeTruthy()
expect(screen.getByTestId('infrastructure-network-add-peering')).toBeTruthy()
expect(screen.getByTestId('infrastructure-network-edit-dns')).toBeTruthy()
expect(await screen.findByTestId('cloud-network-bulk')).toBeTruthy()
expect(screen.getByTestId('cloud-network-add-peering')).toBeTruthy()
expect(screen.getByTestId('cloud-network-edit-dns')).toBeTruthy()
})
it('opens AddPeeringModal when Add Peering is clicked', async () => {
renderNetworkPage(infrastructureTopologyFixture)
fireEvent.click(await screen.findByTestId('infrastructure-network-add-peering'))
fireEvent.click(await screen.findByTestId('cloud-network-add-peering'))
expect(screen.getByTestId('infrastructure-modal-add-peering')).toBeTruthy()
})
it('opens AddLBModal when per-region Add LB is clicked', async () => {
renderNetworkPage(infrastructureTopologyFixture)
fireEvent.click(await screen.findByTestId('infrastructure-network-add-lb-region-eu-central'))
fireEvent.click(await screen.findByTestId('cloud-network-add-lb-region-eu-central'))
expect(screen.getByTestId('infrastructure-modal-add-lb')).toBeTruthy()
})
it('opens EditFirewallRulesModal from row-level edit', async () => {
renderNetworkPage(infrastructureTopologyFixture)
fireEvent.click(await screen.findByTestId('infrastructure-firewall-row-fw-eu-central-edit'))
fireEvent.click(await screen.findByTestId('cloud-firewall-row-fw-eu-central-edit'))
expect(screen.getByTestId('infrastructure-modal-edit-firewall-rules')).toBeTruthy()
})
})

View File

@ -1,9 +1,10 @@
/**
* InfrastructureNetwork Network tab. Flat table [LB · Peering ·
* Firewall · DNS zone], reads off the shared infrastructure tree.
* CloudNetwork Sovereign Cloud / Network sub-page. Flat tables for
* load balancers, peerings, firewalls and DNS zones; reads off the
* shared infrastructure tree provided by CloudPage.
*
* Per founder spec (issue #228): "Network flat table [LB · Peering
* · Firewall · DNS zone]. Bulk: add rule, attach."
* Per founder spec (issue #309 supersedes #228): bulk actions cover
* add rule + attach.
*/
import { useMemo, useState } from 'react'
@ -38,7 +39,7 @@ interface FirewallRow {
region: RegionSpec
}
export function InfrastructureNetwork() {
export function CloudNetwork() {
const { deploymentId, data, isLoading } = useCloud()
const { lbs, peerings, firewalls, networks } = useMemo(() => {
@ -75,15 +76,15 @@ export function InfrastructureNetwork() {
!isLoading && lbs.length === 0 && peerings.length === 0 && firewalls.length === 0
return (
<div data-testid="infrastructure-network">
<div data-testid="cloud-network">
{isLoading && (
<div className="flex h-48 items-center justify-center text-sm text-[var(--color-text-dim)]" data-testid="infrastructure-network-loading">
<div className="flex h-48 items-center justify-center text-sm text-[var(--color-text-dim)]" data-testid="cloud-network-loading">
Loading network resources
</div>
)}
{isEmpty && (
<div className="infra-empty" data-testid="infrastructure-network-empty">
<div className="infra-empty" data-testid="cloud-network-empty">
<p className="title">No network resources yet.</p>
<p className="sub">Load balancers, peerings, firewalls and DNS zones will appear here.</p>
</div>
@ -91,32 +92,32 @@ export function InfrastructureNetwork() {
{!isEmpty && data && (
<>
<div className="infra-bulk-actions" data-testid="infrastructure-network-bulk">
<div className="infra-bulk-actions" data-testid="cloud-network-bulk">
<span className="label">Bulk actions</span>
<button
type="button"
className="primary"
data-testid="infrastructure-network-add-peering"
data-testid="cloud-network-add-peering"
onClick={() => setAddPeeringOpen(true)}
>
+ Add peering
</button>
<button
type="button"
data-testid="infrastructure-network-edit-dns"
data-testid="cloud-network-edit-dns"
onClick={() => setEditDNS(`zone-${data.topology.regions[0]?.id ?? 'default'}`)}
>
Edit DNS zone
</button>
</div>
<section className="infra-section" data-testid="infrastructure-lbs-section">
<section className="infra-section" data-testid="cloud-lbs-section">
<h2>
Load Balancers <span className="count" data-testid="infrastructure-lbs-count">{lbs.length}</span>
Load Balancers <span className="count" data-testid="cloud-lbs-count">{lbs.length}</span>
</h2>
<FlatTable testId="infrastructure-lbs-table" headers={['Name', 'Public IP', 'Listeners', 'Targets', 'Region', 'Status', '']}>
<FlatTable testId="cloud-lbs-table" headers={['Name', 'Public IP', 'Listeners', 'Targets', 'Region', 'Status', '']}>
{lbs.map(({ lb, region }) => (
<tr key={lb.id} data-testid={`infrastructure-lb-row-${lb.id}`}>
<tr key={lb.id} data-testid={`cloud-lb-row-${lb.id}`}>
<td>{lb.name}</td>
<td style={{ fontFamily: 'monospace' }}>{lb.publicIP}</td>
<td>{lb.listeners.map((l) => `${l.protocol}:${l.port}`).join(', ')}</td>
@ -136,7 +137,7 @@ export function InfrastructureNetwork() {
type="button"
style={{ ...rowBtn, borderColor: 'var(--color-accent)', color: 'var(--color-accent)' }}
onClick={() => setAddLBFor(r)}
data-testid={`infrastructure-network-add-lb-${r.id}`}
data-testid={`cloud-network-add-lb-${r.id}`}
>
+ Add LB to {r.name}
</button>
@ -144,13 +145,13 @@ export function InfrastructureNetwork() {
</div>
</section>
<section className="infra-section" data-testid="infrastructure-peerings-section">
<section className="infra-section" data-testid="cloud-peerings-section">
<h2>
Peerings <span className="count" data-testid="infrastructure-peerings-count">{peerings.length}</span>
Peerings <span className="count" data-testid="cloud-peerings-count">{peerings.length}</span>
</h2>
<FlatTable testId="infrastructure-peerings-table" headers={['Name', 'VPCs', 'Subnets', 'Region', 'Status']}>
<FlatTable testId="cloud-peerings-table" headers={['Name', 'VPCs', 'Subnets', 'Region', 'Status']}>
{peerings.map(({ peering, region }) => (
<tr key={peering.id} data-testid={`infrastructure-peering-row-${peering.id}`}>
<tr key={peering.id} data-testid={`cloud-peering-row-${peering.id}`}>
<td>{peering.name}</td>
<td>{peering.vpcPair}</td>
<td style={{ fontFamily: 'monospace' }}>{peering.subnets}</td>
@ -170,13 +171,13 @@ export function InfrastructureNetwork() {
</FlatTable>
</section>
<section className="infra-section" data-testid="infrastructure-firewalls-section">
<section className="infra-section" data-testid="cloud-firewalls-section">
<h2>
Firewalls <span className="count" data-testid="infrastructure-firewalls-count">{firewalls.length}</span>
Firewalls <span className="count" data-testid="cloud-firewalls-count">{firewalls.length}</span>
</h2>
<FlatTable testId="infrastructure-firewalls-table" headers={['Name', 'Rules', 'Region', 'Status', '']}>
<FlatTable testId="cloud-firewalls-table" headers={['Name', 'Rules', 'Region', 'Status', '']}>
{firewalls.map(({ firewall, region }) => (
<tr key={firewall.id} data-testid={`infrastructure-firewall-row-${firewall.id}`}>
<tr key={firewall.id} data-testid={`cloud-firewall-row-${firewall.id}`}>
<td>{firewall.name}</td>
<td>{firewall.rules.length}</td>
<td>{region.providerRegion}</td>
@ -188,7 +189,7 @@ export function InfrastructureNetwork() {
type="button"
style={rowBtn}
onClick={() => setEditFirewall(firewall)}
data-testid={`infrastructure-firewall-row-${firewall.id}-edit`}
data-testid={`cloud-firewall-row-${firewall.id}-edit`}
>
Edit rules
</button>

View File

@ -1,5 +1,5 @@
/**
* InfrastructureStorage.test.tsx render lock-in for the Storage tab.
* CloudStorage.test.tsx render lock-in for the Storage tab.
*
* Coverage:
* 1. Empty state.
@ -21,7 +21,7 @@ import {
} from '@tanstack/react-router'
import { CloudPage } from './CloudPage'
import { InfrastructureStorage } from './InfrastructureStorage'
import { CloudStorage } from './CloudStorage'
import { infrastructureTopologyFixture } from '@/test/fixtures/infrastructure-topology.fixture'
import type { HierarchicalInfrastructure } from '@/lib/infrastructure.types'
import { useWizardStore } from '@/entities/deployment/store'
@ -44,7 +44,7 @@ function renderStoragePage(data: HierarchicalInfrastructure) {
const storageRoute = createRoute({
getParentRoute: () => cloudRoute,
path: '/storage',
component: InfrastructureStorage,
component: CloudStorage,
})
const tree = rootRoute.addChildren([cloudRoute.addChildren([storageRoute])])
const router = createRouter({
@ -65,7 +65,7 @@ function renderStoragePage(data: HierarchicalInfrastructure) {
afterEach(() => cleanup())
describe('InfrastructureStorage — empty', () => {
describe('CloudStorage — empty', () => {
it('renders the empty state when no PVCs / buckets / volumes exist', async () => {
const empty: HierarchicalInfrastructure = {
cloud: [],
@ -73,32 +73,32 @@ describe('InfrastructureStorage — empty', () => {
storage: { pvcs: [], buckets: [], volumes: [] },
}
renderStoragePage(empty)
expect(await screen.findByTestId('infrastructure-storage-empty')).toBeTruthy()
expect(await screen.findByTestId('cloud-storage-empty')).toBeTruthy()
})
})
describe('InfrastructureStorage — populated', () => {
describe('CloudStorage — populated', () => {
it('renders PVC, bucket and volume tables with counts', async () => {
renderStoragePage(infrastructureTopologyFixture)
expect(await screen.findByTestId('infrastructure-pvcs-table')).toBeTruthy()
expect(screen.getByTestId('infrastructure-buckets-table')).toBeTruthy()
expect(screen.getByTestId('infrastructure-volumes-table')).toBeTruthy()
expect(screen.getByTestId('infrastructure-pvcs-count').textContent).toBe('2')
expect(screen.getByTestId('infrastructure-buckets-count').textContent).toBe('1')
expect(screen.getByTestId('infrastructure-volumes-count').textContent).toBe('1')
expect(await screen.findByTestId('cloud-pvcs-table')).toBeTruthy()
expect(screen.getByTestId('cloud-buckets-table')).toBeTruthy()
expect(screen.getByTestId('cloud-volumes-table')).toBeTruthy()
expect(screen.getByTestId('cloud-pvcs-count').textContent).toBe('2')
expect(screen.getByTestId('cloud-buckets-count').textContent).toBe('1')
expect(screen.getByTestId('cloud-volumes-count').textContent).toBe('1')
})
it('renders the bulk-actions strip', async () => {
renderStoragePage(infrastructureTopologyFixture)
expect(await screen.findByTestId('infrastructure-storage-bulk')).toBeTruthy()
expect(screen.getByTestId('infrastructure-storage-bulk-snapshot')).toBeTruthy()
expect(screen.getByTestId('infrastructure-storage-bulk-expand')).toBeTruthy()
expect(screen.getByTestId('infrastructure-storage-bulk-delete')).toBeTruthy()
expect(await screen.findByTestId('cloud-storage-bulk')).toBeTruthy()
expect(screen.getByTestId('cloud-storage-bulk-snapshot')).toBeTruthy()
expect(screen.getByTestId('cloud-storage-bulk-expand')).toBeTruthy()
expect(screen.getByTestId('cloud-storage-bulk-delete')).toBeTruthy()
})
it('opens ExpandPVCModal on row-level Expand', async () => {
renderStoragePage(infrastructureTopologyFixture)
fireEvent.click(await screen.findByTestId('infrastructure-pvc-row-pvc-postgres-data-expand'))
fireEvent.click(await screen.findByTestId('cloud-pvc-row-pvc-postgres-data-expand'))
expect(screen.getByTestId('infrastructure-modal-expand-pvc')).toBeTruthy()
})
})

View File

@ -1,9 +1,10 @@
/**
* InfrastructureStorage Storage tab. Flat table [PVC · Bucket ·
* Volume], reads off the shared infrastructure tree.
* CloudStorage Sovereign Cloud / Storage sub-page. Flat tables for
* PVCs, object buckets and block volumes; reads off the shared
* infrastructure tree provided by CloudPage.
*
* Per founder spec (issue #228): "Storage flat table [PVC · Bucket
* · Volume]. Bulk: snapshot, expand, delete."
* Per founder spec (issue #309 supersedes #228): bulk actions cover
* snapshot, expand, delete.
*/
import { useMemo, useState } from 'react'
@ -13,7 +14,7 @@ import { ModalShell, FormRow, TextInput } from '@/components/CrudModals/_shared'
import { pvcAction } from '@/lib/infrastructure-crud'
import type { PVCItem } from '@/lib/infrastructure.types'
export function InfrastructureStorage() {
export function CloudStorage() {
const { deploymentId, data, isLoading } = useCloud()
const { pvcs, buckets, volumes } = useMemo(() => {
@ -44,15 +45,15 @@ export function InfrastructureStorage() {
}
return (
<div data-testid="infrastructure-storage">
<div data-testid="cloud-storage">
{isLoading && (
<div className="flex h-48 items-center justify-center text-sm text-[var(--color-text-dim)]" data-testid="infrastructure-storage-loading">
<div className="flex h-48 items-center justify-center text-sm text-[var(--color-text-dim)]" data-testid="cloud-storage-loading">
Loading storage resources
</div>
)}
{isEmpty && (
<div className="infra-empty" data-testid="infrastructure-storage-empty">
<div className="infra-empty" data-testid="cloud-storage-empty">
<p className="title">No storage resources yet.</p>
<p className="sub">PVCs, buckets and volumes will appear here as the cluster reports them.</p>
</div>
@ -60,11 +61,11 @@ export function InfrastructureStorage() {
{!isEmpty && (
<>
<div className="infra-bulk-actions" data-testid="infrastructure-storage-bulk">
<div className="infra-bulk-actions" data-testid="cloud-storage-bulk">
<span className="label">Bulk · {selected.length} selected</span>
<button
type="button"
data-testid="infrastructure-storage-bulk-snapshot"
data-testid="cloud-storage-bulk-snapshot"
disabled={selected.filter((s) => s.kind === 'pvc').length === 0}
onClick={() => {
const pick = selected.find((s) => s.kind === 'pvc')
@ -77,7 +78,7 @@ export function InfrastructureStorage() {
</button>
<button
type="button"
data-testid="infrastructure-storage-bulk-expand"
data-testid="cloud-storage-bulk-expand"
disabled={selected.filter((s) => s.kind === 'pvc').length !== 1}
onClick={() => {
const pick = selected.find((s) => s.kind === 'pvc')
@ -90,7 +91,7 @@ export function InfrastructureStorage() {
</button>
<button
type="button"
data-testid="infrastructure-storage-bulk-delete"
data-testid="cloud-storage-bulk-delete"
disabled={selected.length !== 1}
onClick={() => {
const pick = selected[0]
@ -108,22 +109,22 @@ export function InfrastructureStorage() {
</button>
</div>
<section className="infra-section" data-testid="infrastructure-pvcs-section">
<section className="infra-section" data-testid="cloud-pvcs-section">
<h2>
Persistent Volume Claims <span className="count" data-testid="infrastructure-pvcs-count">{pvcs.length}</span>
Persistent Volume Claims <span className="count" data-testid="cloud-pvcs-count">{pvcs.length}</span>
</h2>
<FlatTable
testId="infrastructure-pvcs-table"
testId="cloud-pvcs-table"
headers={['', 'Name', 'Namespace', 'Capacity', 'Used', 'Class', 'Status', '']}
>
{pvcs.map((p) => (
<tr key={p.id} data-testid={`infrastructure-pvc-row-${p.id}`}>
<tr key={p.id} data-testid={`cloud-pvc-row-${p.id}`}>
<td>
<input
type="checkbox"
checked={selected.some((s) => s.kind === 'pvc' && s.id === p.id)}
onChange={(e) => toggle('pvc', p.id, e.target.checked)}
data-testid={`infrastructure-pvc-row-${p.id}-select`}
data-testid={`cloud-pvc-row-${p.id}-select`}
/>
</td>
<td>{p.name}</td>
@ -138,7 +139,7 @@ export function InfrastructureStorage() {
<button
type="button"
onClick={() => setExpandPvc(p)}
data-testid={`infrastructure-pvc-row-${p.id}-expand`}
data-testid={`cloud-pvc-row-${p.id}-expand`}
style={rowBtn}
>
Expand
@ -146,7 +147,7 @@ export function InfrastructureStorage() {
<button
type="button"
onClick={() => void pvcAction({ deploymentId, pvcId: p.id, action: 'snapshot' }).catch(() => {})}
data-testid={`infrastructure-pvc-row-${p.id}-snapshot`}
data-testid={`cloud-pvc-row-${p.id}-snapshot`}
style={rowBtn}
>
Snapshot
@ -157,22 +158,22 @@ export function InfrastructureStorage() {
</FlatTable>
</section>
<section className="infra-section" data-testid="infrastructure-buckets-section">
<section className="infra-section" data-testid="cloud-buckets-section">
<h2>
Object Buckets <span className="count" data-testid="infrastructure-buckets-count">{buckets.length}</span>
Object Buckets <span className="count" data-testid="cloud-buckets-count">{buckets.length}</span>
</h2>
<FlatTable
testId="infrastructure-buckets-table"
testId="cloud-buckets-table"
headers={['', 'Name', 'Endpoint', 'Capacity', 'Used', 'Retention']}
>
{buckets.map((b) => (
<tr key={b.id} data-testid={`infrastructure-bucket-row-${b.id}`}>
<tr key={b.id} data-testid={`cloud-bucket-row-${b.id}`}>
<td>
<input
type="checkbox"
checked={selected.some((s) => s.kind === 'bucket' && s.id === b.id)}
onChange={(e) => toggle('bucket', b.id, e.target.checked)}
data-testid={`infrastructure-bucket-row-${b.id}-select`}
data-testid={`cloud-bucket-row-${b.id}-select`}
/>
</td>
<td>{b.name}</td>
@ -185,22 +186,22 @@ export function InfrastructureStorage() {
</FlatTable>
</section>
<section className="infra-section" data-testid="infrastructure-volumes-section">
<section className="infra-section" data-testid="cloud-volumes-section">
<h2>
Block Volumes <span className="count" data-testid="infrastructure-volumes-count">{volumes.length}</span>
Block Volumes <span className="count" data-testid="cloud-volumes-count">{volumes.length}</span>
</h2>
<FlatTable
testId="infrastructure-volumes-table"
testId="cloud-volumes-table"
headers={['', 'Name', 'Capacity', 'Region', 'Attached', 'Status']}
>
{volumes.map((v) => (
<tr key={v.id} data-testid={`infrastructure-volume-row-${v.id}`}>
<tr key={v.id} data-testid={`cloud-volume-row-${v.id}`}>
<td>
<input
type="checkbox"
checked={selected.some((s) => s.kind === 'volume' && s.id === v.id)}
onChange={(e) => toggle('volume', v.id, e.target.checked)}
data-testid={`infrastructure-volume-row-${v.id}-select`}
data-testid={`cloud-volume-row-${v.id}-select`}
/>
</td>
<td>{v.name}</td>