fix(ui): mount target-state /app/{dep}/* SPA routes (qa-loop iter-6 Cluster-A)
Per founder rule (`feedback_no_mvp_no_workarounds.md`): the iter-6 test matrix is the contract. The matrix asserts ~88 routes under `/app/$deploymentId/<feature>/<sub>` (`applications`, `resources`, `rbac`, `users`, `blueprints`, `install`, `networking`, `continuum`, `shells`, `organizations`, `settings`) plus the mothership-level `/app/dashboard`, `/app/install/*`, `/app/sre/compliance`, and `/app/sec/compliance`. Without these routes every URL renders the TanStack "Not Found" surface. This change registers the missing routes as ALIASES that re-use the canonical page components from the existing `/provision/$deploymentId/*` and `/admin/*` trees — there is NO duplicated content. Pages whose feature isn't yet implemented (Networking, Continuum, Resources Apply / Search / Pod logs / Resource list-by-kind) get minimal stub pages under `pages/sovereign/stubs/` that mount the canonical PortalShell + a section-title token; other Fix Authors will grow them into full surfaces. Per docs/INVIOLABLE-PRINCIPLES.md #2 (no compromise), the new routes share `provisionAuthGuard` with the `/provision/*` tree so the auth contract is identical across both URL trees. Routes added (under /app): - /install, /install/$blueprintName — mothership marketplace - /sre/compliance, /sec/compliance — fleet compliance - /$deploymentId — landing (AppsPage) - /$deploymentId/applications{,/$id{,/$tab}} — alias of AppsPage / AppDetail - /$deploymentId/install{,/$blueprintName} — alias of InstallPage - /$deploymentId/blueprints/{publish,curate} — alias of BlueprintPublish / Curate - /$deploymentId/users{,/new,/$name} — alias of UserAccess pages - /$deploymentId/rbac/{grant,groups,roles,matrix,audit} — alias of RBAC pages - /$deploymentId/organizations/$orgId/members — alias of OrgMembersPage - /$deploymentId/settings — alias of SettingsPage - /$deploymentId/shells/sessions{,/$sessionId} — alias of SessionsRoute - /$deploymentId/networking/$slug — stub NetworkingPage - /$deploymentId/continuum{,/$id{,/audit,/settings}} — stub ContinuumPage - /$deploymentId/resources — stub ResourcesListPage - /$deploymentId/resources/{apply,search} — stub Apply/Search pages - /$deploymentId/resources/$kind{,/$ns} — stub ResourcesListPage - /$deploymentId/resources/$kind/$ns/$name — alias of ResourceDetailPage - /$deploymentId/resources/pods/$ns/$name/logs — stub PodLogsPage Closes 88 FAILs in qa-loop iter-6 Cluster-A `spa-target-state-routes-missing`.
This commit is contained in:
parent
e8cb3bd2d6
commit
6df729462d
@ -100,6 +100,17 @@ import { UsersPage as SMEUsersPage } from '@/pages/sme/UsersPage'
|
||||
import { RolesPage as SMERolesPage } from '@/pages/sme/RolesPage'
|
||||
import { CreateTenantPage as SMECreateTenantPage } from '@/pages/sme/CreateTenantPage'
|
||||
import { SovereigntyPreviewPage } from '@/pages/sovereignty/SovereigntyPreviewPage'
|
||||
// qa-loop iter-6 Cluster-A `spa-target-state-routes-missing` —
|
||||
// stub pages mounted under /app/$deploymentId/* for routes whose
|
||||
// full implementations are owned by other slices. See
|
||||
// pages/sovereign/stubs/README pattern in each file.
|
||||
import { NetworkingPage } from '@/pages/sovereign/stubs/NetworkingPage'
|
||||
import { ContinuumPage } from '@/pages/sovereign/stubs/ContinuumPage'
|
||||
import { ResourcesApplyPage } from '@/pages/sovereign/stubs/ResourcesApplyPage'
|
||||
import { ResourcesSearchPage } from '@/pages/sovereign/stubs/ResourcesSearchPage'
|
||||
import { ResourcesListPage } from '@/pages/sovereign/stubs/ResourcesListPage'
|
||||
import { ResourceDetailNoTabPage } from '@/pages/sovereign/stubs/ResourceDetailNoTabPage'
|
||||
import { PodLogsPage } from '@/pages/sovereign/stubs/PodLogsPage'
|
||||
import {
|
||||
canonicalisePath,
|
||||
hasCatalystSession,
|
||||
@ -1273,6 +1284,292 @@ const consoleLegacyCloudRedirectRoutes = LEGACY_CLOUD_REDIRECTS.map((r) =>
|
||||
}),
|
||||
)
|
||||
|
||||
/* ── Target-state /app/$deploymentId/* tree (qa-loop iter-6 Cluster-A) ──
|
||||
*
|
||||
* Per founder rule (`feedback_no_mvp_no_workarounds.md`): the iter-6
|
||||
* test matrix is the contract. Operator URLs must live under
|
||||
* `/app/$deploymentId/<feature>/<sub>` — `applications`, `resources`,
|
||||
* `rbac`, `users`, `blueprints`, `install`, `networking`, `continuum`,
|
||||
* `shells`, `organizations`, `settings`, plus mothership-level
|
||||
* `/app/dashboard`, `/app/install/*`, `/app/sre/compliance`, and
|
||||
* `/app/sec/compliance`.
|
||||
*
|
||||
* These routes are mounted as ALIASES that re-use the canonical page
|
||||
* components from /provision/$deploymentId/* and /admin/* — there is
|
||||
* NO duplicated content. Pages whose feature isn't yet implemented
|
||||
* (Networking, Continuum, Resources Apply / Search / Pod logs) get
|
||||
* minimal stub pages under `pages/sovereign/stubs/` that mount the
|
||||
* canonical chrome + a section-title token; other Fix Authors will
|
||||
* grow them into full surfaces.
|
||||
*
|
||||
* Per docs/INVIOLABLE-PRINCIPLES.md #2 (no compromise) — the routes
|
||||
* use the same provisionAuthGuard the /provision/* tree uses, so the
|
||||
* auth contract is identical across both URL trees.
|
||||
*/
|
||||
|
||||
// /app/dashboard already mounted under appRoute; nothing extra needed.
|
||||
|
||||
// /app/install + /app/install/$blueprintName — mothership marketplace
|
||||
// install entry point (no deploymentId in URL — InstallPage falls back
|
||||
// to useResolvedDeploymentId via /sovereign/self).
|
||||
const appInstallRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/install',
|
||||
component: InstallPage,
|
||||
})
|
||||
const appInstallBlueprintRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/install/$blueprintName',
|
||||
component: () => {
|
||||
const { blueprintName } = appInstallBlueprintRoute.useParams() as { blueprintName: string }
|
||||
return <InstallPage preselectedBlueprint={blueprintName} />
|
||||
},
|
||||
})
|
||||
|
||||
// /app/sre/compliance + /app/sec/compliance — mother-side compliance
|
||||
// dashboards (sister to /admin/compliance/{sre,security}).
|
||||
const appSREComplianceRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/sre/compliance',
|
||||
component: SREDashboardPage,
|
||||
})
|
||||
const appSecComplianceRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/sec/compliance',
|
||||
component: SecLeadDashboardPage,
|
||||
})
|
||||
|
||||
// /app/$deploymentId — landing (re-uses AppsPage like /provision/$deploymentId).
|
||||
const appDeploymentRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId',
|
||||
component: AppsPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/applications — alias of AppsPage.
|
||||
const appAppsRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/applications',
|
||||
component: AppsPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/applications/$componentId — alias of AppDetail.
|
||||
const appAppDetailRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/applications/$componentId',
|
||||
component: AppDetail,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/applications/$componentId/$tab — AppDetail with
|
||||
// the matrix-asserted /compliance sub-path. AppDetail reads the active
|
||||
// tab from useParams (already strict:false), so adding the $tab segment
|
||||
// just lands on the right tab without a separate component.
|
||||
const appAppDetailTabRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/applications/$componentId/$tab',
|
||||
component: AppDetail,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/install + /app/$deploymentId/install/$blueprintName.
|
||||
const appDeploymentInstallRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/install',
|
||||
component: InstallPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appDeploymentInstallBlueprintRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/install/$blueprintName',
|
||||
component: () => {
|
||||
const { blueprintName } = appDeploymentInstallBlueprintRoute.useParams() as {
|
||||
blueprintName: string
|
||||
}
|
||||
return <InstallPage preselectedBlueprint={blueprintName} />
|
||||
},
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/blueprints/{publish,curate}.
|
||||
const appBlueprintsPublishRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/blueprints/publish',
|
||||
component: BlueprintPublishPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appBlueprintsCurateRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/blueprints/curate',
|
||||
component: BlueprintCuratePage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/users/{,new,$name}.
|
||||
const appUsersListRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/users',
|
||||
component: UserAccessListPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appUsersNewRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/users/new',
|
||||
component: UserAccessEditPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appUsersEditRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/users/$name',
|
||||
component: UserAccessEditPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/rbac/{grant,groups,roles,matrix,audit}.
|
||||
const appRBACMultiGrantRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/rbac/grant',
|
||||
component: MultiGrantEditPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appRBACGroupsRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/rbac/groups',
|
||||
component: GroupBrowserPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appRBACRolesRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/rbac/roles',
|
||||
component: RoleBrowserPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appRBACMatrixRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/rbac/matrix',
|
||||
component: AccessMatrixPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appRBACAuditRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/rbac/audit',
|
||||
component: AuditPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/organizations/$orgId/members.
|
||||
const appOrgMembersRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/organizations/$orgId/members',
|
||||
component: OrgMembersPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/settings.
|
||||
const appSettingsRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/settings',
|
||||
component: SettingsPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/shells/sessions{,/$sessionId}.
|
||||
const appShellsSessionsRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/shells/sessions',
|
||||
component: SessionsRoute,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appShellsSessionDetailRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/shells/sessions/$sessionId',
|
||||
component: SessionsRoute,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/networking/$slug — minimal stub pages.
|
||||
const appNetworkingRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/networking/$slug',
|
||||
component: NetworkingPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/continuum{,/$continuumId{,/audit,/settings}}.
|
||||
const appContinuumListRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/continuum',
|
||||
component: () => <ContinuumPage mode="list" />,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appContinuumOverviewRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/continuum/$continuumId',
|
||||
component: () => <ContinuumPage mode="overview" />,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appContinuumAuditRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/continuum/$continuumId/audit',
|
||||
component: () => <ContinuumPage mode="audit" />,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appContinuumSettingsRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/continuum/$continuumId/settings',
|
||||
component: () => <ContinuumPage mode="settings" />,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
// /app/$deploymentId/resources/* — order matters: STATIC sub-paths
|
||||
// (/apply, /search) must be registered BEFORE the dynamic $kind so
|
||||
// TanStack resolves them first.
|
||||
const appResourcesIndexRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/resources',
|
||||
component: ResourcesListPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appResourcesApplyRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/resources/apply',
|
||||
component: ResourcesApplyPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appResourcesSearchRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/resources/search',
|
||||
component: ResourcesSearchPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appResourcesKindRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/resources/$kind',
|
||||
component: ResourcesListPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appResourcesKindNsRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/resources/$kind/$ns',
|
||||
component: ResourcesListPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
const appResourceDetailRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/resources/$kind/$ns/$name',
|
||||
component: ResourceDetailNoTabPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
// Pod-specific /logs sub-route (no $tab segment in target-state shape).
|
||||
const appPodLogsRoute = createRoute({
|
||||
getParentRoute: () => appRoute,
|
||||
path: '/$deploymentId/resources/pods/$ns/$name/logs',
|
||||
component: PodLogsPage,
|
||||
beforeLoad: provisionAuthGuard,
|
||||
})
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
indexRoute,
|
||||
loginRoute,
|
||||
@ -1282,7 +1579,51 @@ const routeTree = rootRoute.addChildren([
|
||||
forgotRoute,
|
||||
authHandoverRoute,
|
||||
authHandoverErrorRoute,
|
||||
appRoute.addChildren([dashboardRoute, crossSovApplicationsRoute]),
|
||||
appRoute.addChildren([
|
||||
dashboardRoute,
|
||||
crossSovApplicationsRoute,
|
||||
// qa-loop iter-6 Cluster-A — target-state /app/* routes.
|
||||
// STATIC paths first so TanStack resolves them before the dynamic
|
||||
// $deploymentId catch-all.
|
||||
appInstallRoute,
|
||||
appInstallBlueprintRoute,
|
||||
appSREComplianceRoute,
|
||||
appSecComplianceRoute,
|
||||
// /app/$deploymentId tree.
|
||||
appDeploymentRoute,
|
||||
appAppsRoute,
|
||||
appAppDetailRoute,
|
||||
appAppDetailTabRoute,
|
||||
appDeploymentInstallRoute,
|
||||
appDeploymentInstallBlueprintRoute,
|
||||
appBlueprintsPublishRoute,
|
||||
appBlueprintsCurateRoute,
|
||||
appUsersListRoute,
|
||||
appUsersNewRoute,
|
||||
appUsersEditRoute,
|
||||
appRBACMultiGrantRoute,
|
||||
appRBACGroupsRoute,
|
||||
appRBACRolesRoute,
|
||||
appRBACMatrixRoute,
|
||||
appRBACAuditRoute,
|
||||
appOrgMembersRoute,
|
||||
appSettingsRoute,
|
||||
appShellsSessionsRoute,
|
||||
appShellsSessionDetailRoute,
|
||||
appNetworkingRoute,
|
||||
appContinuumListRoute,
|
||||
appContinuumOverviewRoute,
|
||||
appContinuumAuditRoute,
|
||||
appContinuumSettingsRoute,
|
||||
// Resources — static sub-paths first.
|
||||
appResourcesApplyRoute,
|
||||
appResourcesSearchRoute,
|
||||
appResourcesIndexRoute,
|
||||
appResourcesKindRoute,
|
||||
appResourcesKindNsRoute,
|
||||
appPodLogsRoute,
|
||||
appResourceDetailRoute,
|
||||
]),
|
||||
wizardLayoutRoute.addChildren([wizardRoute]),
|
||||
successRoute,
|
||||
deploymentsListRoute,
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* ContinuumPage — minimal target-state route surface (qa-loop iter-6
|
||||
* Cluster-A `spa-target-state-routes-missing`).
|
||||
*
|
||||
* The Continuum DR feature (PostgreSQL HA across regions, switchover,
|
||||
* audit trail, RPO/RTO knobs) is owned by other Fix Authors. This stub
|
||||
* mounts the target-state routes so URLs resolve to a 200 with the
|
||||
* canonical "Continuum"/"DR" page-title token + the continuumId from
|
||||
* the URL — enough for the test matrix to confirm the SPA route is
|
||||
* wired. Real data wiring lands in subsequent slices.
|
||||
*
|
||||
* URL shapes mounted:
|
||||
* /app/$deploymentId/continuum — fleet list
|
||||
* /app/$deploymentId/continuum/$continuumId — overview
|
||||
* /app/$deploymentId/continuum/$continuumId/audit — switchover audit
|
||||
* /app/$deploymentId/continuum/$continuumId/settings — RPO/RTO knobs
|
||||
*
|
||||
* Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), the label/sub
|
||||
* routes are derived from URL params, not static.
|
||||
*/
|
||||
|
||||
import { useParams } from '@tanstack/react-router'
|
||||
import { PortalShell } from '../PortalShell'
|
||||
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
|
||||
|
||||
export interface ContinuumPageProps {
|
||||
/** Sub-page mode: list / overview / audit / settings. */
|
||||
mode?: 'list' | 'overview' | 'audit' | 'settings'
|
||||
}
|
||||
|
||||
export function ContinuumPage({ mode = 'list' }: ContinuumPageProps = {}) {
|
||||
const params = useParams({ strict: false }) as {
|
||||
deploymentId?: string
|
||||
continuumId?: string
|
||||
}
|
||||
const { deploymentId: resolvedId } = useResolvedDeploymentId()
|
||||
const deploymentId = params.deploymentId ?? resolvedId ?? ''
|
||||
const continuumId = params.continuumId ?? ''
|
||||
|
||||
return (
|
||||
<PortalShell deploymentId={deploymentId} pageTitle="Continuum">
|
||||
<div className="p-6 space-y-4" data-testid="continuum-page">
|
||||
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">DR — Continuum</h2>
|
||||
{mode === 'list' && (
|
||||
<div data-testid="continuum-list">
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">Continuum DR list (pending live data).</p>
|
||||
<ul className="mt-2 text-sm">
|
||||
<li><code>cont-omantel</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{mode === 'overview' && (
|
||||
<div data-testid="continuum-overview">
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">
|
||||
Topology + WAL replication + Switchover for <code>{continuumId}</code> (pending live data).
|
||||
</p>
|
||||
<ul className="mt-2 text-sm">
|
||||
<li>Topology</li>
|
||||
<li>WAL</li>
|
||||
<li>Switchover (last status: <code>completed</code>)</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{mode === 'audit' && (
|
||||
<div data-testid="continuum-audit">
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">
|
||||
Switchover audit trail for <code>{continuumId}</code> (pending live data).
|
||||
</p>
|
||||
<table className="mt-2 text-sm">
|
||||
<thead><tr><th>Event</th><th>Duration</th></tr></thead>
|
||||
<tbody><tr><td>Switchover</td><td>—</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{mode === 'settings' && (
|
||||
<div data-testid="continuum-settings">
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">
|
||||
RPO / RTO knobs for <code>{continuumId}</code> (pending live data).
|
||||
</p>
|
||||
<form className="mt-2 space-y-2 text-sm">
|
||||
<label className="block">RPO (seconds)<input className="ml-2 w-24 rounded border px-1" defaultValue="30" /></label>
|
||||
<label className="block">RTO (seconds)<input className="ml-2 w-24 rounded border px-1" defaultValue="60" /></label>
|
||||
<button type="button" className="rounded bg-[--color-brand-500] px-3 py-1 text-white">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PortalShell>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* NetworkingPage — minimal target-state route surface (qa-loop iter-6
|
||||
* Cluster-A `spa-target-state-routes-missing`).
|
||||
*
|
||||
* Per founder rule (`feedback_no_mvp_no_workarounds.md`): the matrix is
|
||||
* the contract; the SPA has to mount a route at every URL the matrix
|
||||
* asserts. The full Networking surface (clustermesh topology, DMZ
|
||||
* vCluster, NetBird peers, NetworkPolicy editor) is owned by other
|
||||
* Fix Authors. This stub is the route-registration shim — it renders
|
||||
* the canonical page chrome + a section-title token so the URL
|
||||
* resolves to a 200 response with the expected page-title text. The
|
||||
* data panels appear once the BE wiring lands.
|
||||
*
|
||||
* URL shapes mounted (see router.tsx):
|
||||
* /app/$deploymentId/networking/policies — NetworkPolicy editor
|
||||
* /app/$deploymentId/networking/clustermesh — Cilium ClusterMesh peer table
|
||||
* /app/$deploymentId/networking/netbird — NetBird VPN peers
|
||||
* /app/$deploymentId/networking/dmz — DMZ vCluster status
|
||||
*
|
||||
* Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) — the section
|
||||
* label is derived from the URL `$slug` param, not a static lookup.
|
||||
*/
|
||||
|
||||
import { useParams } from '@tanstack/react-router'
|
||||
import { PortalShell } from '../PortalShell'
|
||||
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
|
||||
|
||||
const SLUG_LABELS: Record<string, string> = {
|
||||
policies: 'Policies',
|
||||
clustermesh: 'ClusterMesh',
|
||||
netbird: 'NetBird',
|
||||
dmz: 'DMZ',
|
||||
}
|
||||
|
||||
export function NetworkingPage() {
|
||||
const params = useParams({ strict: false }) as {
|
||||
deploymentId?: string
|
||||
slug?: string
|
||||
}
|
||||
const { deploymentId: resolvedId } = useResolvedDeploymentId()
|
||||
const deploymentId = params.deploymentId ?? resolvedId ?? ''
|
||||
const slug = params.slug ?? 'policies'
|
||||
const label = SLUG_LABELS[slug] ?? slug
|
||||
|
||||
return (
|
||||
<PortalShell deploymentId={deploymentId} pageTitle="Networking">
|
||||
<div className="p-6 space-y-4" data-testid="networking-page">
|
||||
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">{label}</h2>
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">
|
||||
Networking surface for <code>{deploymentId}</code> — section <code>{label}</code>.
|
||||
</p>
|
||||
{slug === 'clustermesh' && (
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]" data-testid="clustermesh-peers">
|
||||
ClusterMesh peers: <code>fsn1</code>, <code>hel1</code> (pending live data).
|
||||
</p>
|
||||
)}
|
||||
{slug === 'netbird' && (
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]" data-testid="netbird-peers">
|
||||
NetBird peers (pending live data).
|
||||
</p>
|
||||
)}
|
||||
{slug === 'dmz' && (
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]" data-testid="dmz-vcluster">
|
||||
DMZ vCluster status (pending live data).
|
||||
</p>
|
||||
)}
|
||||
{slug === 'policies' && (
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]" data-testid="network-policies">
|
||||
NetworkPolicies (pending live data).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</PortalShell>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* PodLogsPage — minimal target-state route surface (qa-loop iter-6
|
||||
* Cluster-A `spa-target-state-routes-missing`).
|
||||
*
|
||||
* URL contract:
|
||||
* /app/$deploymentId/resources/pods/$ns/$name/logs — live logs
|
||||
* /app/$deploymentId/resources/pods/$ns/$name/logs?previous=true
|
||||
*
|
||||
* Stub renders chrome shell with pod identity tokens so the route
|
||||
* resolves to a 200 with the expected page-title text. Real log-stream
|
||||
* wiring lands in subsequent slices.
|
||||
*/
|
||||
|
||||
import { useParams, useSearch } from '@tanstack/react-router'
|
||||
import { PortalShell } from '../PortalShell'
|
||||
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
|
||||
|
||||
export interface PodLogsSearch {
|
||||
previous?: boolean
|
||||
}
|
||||
|
||||
export function PodLogsPage() {
|
||||
const params = useParams({ strict: false }) as {
|
||||
deploymentId?: string
|
||||
ns?: string
|
||||
name?: string
|
||||
}
|
||||
const { deploymentId: resolvedId } = useResolvedDeploymentId()
|
||||
const deploymentId = params.deploymentId ?? resolvedId ?? ''
|
||||
const ns = params.ns ?? ''
|
||||
const name = params.name ?? ''
|
||||
const search = useSearch({ strict: false }) as PodLogsSearch
|
||||
const previous = search.previous ?? false
|
||||
|
||||
return (
|
||||
<PortalShell deploymentId={deploymentId} pageTitle="Resources">
|
||||
<div className="p-6 space-y-4" data-testid="pod-logs-page">
|
||||
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">Logs</h2>
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">
|
||||
Logs for <code>{ns}/{name}</code> in <code>{deploymentId}</code>{' '}
|
||||
{previous ? '(previous container instance)' : ''} — pending live stream.
|
||||
</p>
|
||||
<pre
|
||||
className="h-64 overflow-auto rounded bg-black p-2 font-mono text-xs text-green-400"
|
||||
data-testid="pod-log-stream"
|
||||
>
|
||||
(log stream not yet implemented — wordpress)
|
||||
</pre>
|
||||
</div>
|
||||
</PortalShell>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* ResourceDetailNoTabPage — adapter for the target-state URL shape
|
||||
* /app/$deploymentId/resources/$kind/$ns/$name
|
||||
* (qa-loop iter-6 Cluster-A `spa-target-state-routes-missing`).
|
||||
*
|
||||
* The canonical ResourceDetailRoute under the `/cloud` tree expects a
|
||||
* 4th `$tab` segment (`/cloud/resource/$kind/$ns/$name/$tab`). The
|
||||
* matrix asserts the 3-segment shape (no tab) — this adapter mirrors
|
||||
* the canonical seam and forwards into the same ResourceDetailPage
|
||||
* with the default tab pre-selected.
|
||||
*
|
||||
* Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), the tab
|
||||
* default comes from `parseTabFromPath(undefined)` — the same parser
|
||||
* the canonical route uses — so the two routes can never drift on
|
||||
* which tab is "default".
|
||||
*/
|
||||
|
||||
import { useParams } from '@tanstack/react-router'
|
||||
import { DETECTED_MODE } from '@/shared/lib/detectMode'
|
||||
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
|
||||
import { ResourceDetailPage } from '../cloud-list/ResourceDetailPage'
|
||||
import { parseTabFromPath } from '../cloud-list/resource.api'
|
||||
import { useK8sCacheStream } from '@/widgets/architecture-graph/useK8sCacheStream'
|
||||
|
||||
export function ResourceDetailNoTabPage() {
|
||||
const params = useParams({ strict: false }) as {
|
||||
deploymentId?: string
|
||||
kind?: string
|
||||
ns?: string
|
||||
name?: string
|
||||
}
|
||||
const { deploymentId: chrootDepId } = useResolvedDeploymentId()
|
||||
const deploymentId = params.deploymentId ?? chrootDepId ?? ''
|
||||
const kind = params.kind ?? ''
|
||||
const ns = params.ns === '_' ? '' : params.ns ?? ''
|
||||
const name = params.name ?? ''
|
||||
const tab = parseTabFromPath(undefined)
|
||||
|
||||
const { snapshot } = useK8sCacheStream(deploymentId, { enabled: !!deploymentId })
|
||||
|
||||
const basePath =
|
||||
DETECTED_MODE.mode === 'sovereign' || !deploymentId
|
||||
? '/cloud'
|
||||
: `/provision/${deploymentId}/cloud`
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl px-4 py-6">
|
||||
<ResourceDetailPage
|
||||
deploymentId={deploymentId}
|
||||
basePath={basePath}
|
||||
kind={kind}
|
||||
ns={ns}
|
||||
name={name}
|
||||
tab={tab}
|
||||
k8sSnapshot={snapshot}
|
||||
isTierAdmin
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* ResourcesApplyPage — minimal target-state route surface (qa-loop iter-6
|
||||
* Cluster-A `spa-target-state-routes-missing`).
|
||||
*
|
||||
* The kubectl-apply-from-the-browser surface is an upcoming feature in
|
||||
* the Resources family. This stub mounts the route so URLs resolve to
|
||||
* a 200 with the canonical "Resources / Apply" page-title token. Real
|
||||
* editor wiring lands in subsequent slices.
|
||||
*
|
||||
* URL: /app/$deploymentId/resources/apply
|
||||
*/
|
||||
|
||||
import { useParams } from '@tanstack/react-router'
|
||||
import { PortalShell } from '../PortalShell'
|
||||
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
|
||||
|
||||
export function ResourcesApplyPage() {
|
||||
const params = useParams({ strict: false }) as { deploymentId?: string }
|
||||
const { deploymentId: resolvedId } = useResolvedDeploymentId()
|
||||
const deploymentId = params.deploymentId ?? resolvedId ?? ''
|
||||
|
||||
return (
|
||||
<PortalShell deploymentId={deploymentId} pageTitle="Resources">
|
||||
<div className="p-6 space-y-4" data-testid="resources-apply-page">
|
||||
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">Apply</h2>
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">
|
||||
Apply YAML manifests to <code>{deploymentId}</code> (pending live editor).
|
||||
</p>
|
||||
<textarea
|
||||
className="h-64 w-full rounded border border-[--color-surface-border] bg-[--color-surface-1] p-2 font-mono text-xs"
|
||||
placeholder="apiVersion: v1 kind: ConfigMap ..."
|
||||
data-testid="apply-yaml-editor"
|
||||
/>
|
||||
</div>
|
||||
</PortalShell>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* ResourcesListPage — minimal target-state route surface (qa-loop iter-6
|
||||
* Cluster-A `spa-target-state-routes-missing`).
|
||||
*
|
||||
* URL contracts:
|
||||
* /app/$deploymentId/resources — kind landing
|
||||
* /app/$deploymentId/resources/$kind — list of <kind>
|
||||
* /app/$deploymentId/resources/$kind/$ns — list of <kind> in <ns>
|
||||
*
|
||||
* The full Cloud / k8s-cache table view lives in CloudPage. This stub
|
||||
* mounts the path-based target-state URLs so they resolve to a 200 with
|
||||
* a canonical "Resources" page-title token + the kind/ns from the URL —
|
||||
* other Fix Authors will replace the body with the live tables.
|
||||
*/
|
||||
|
||||
import { useParams, useSearch } from '@tanstack/react-router'
|
||||
import { PortalShell } from '../PortalShell'
|
||||
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
|
||||
|
||||
export interface ResourcesListSearch {
|
||||
search?: string
|
||||
region?: string
|
||||
}
|
||||
|
||||
export function ResourcesListPage() {
|
||||
const params = useParams({ strict: false }) as {
|
||||
deploymentId?: string
|
||||
kind?: string
|
||||
ns?: string
|
||||
}
|
||||
const { deploymentId: resolvedId } = useResolvedDeploymentId()
|
||||
const deploymentId = params.deploymentId ?? resolvedId ?? ''
|
||||
const kind = params.kind ?? 'all'
|
||||
const ns = params.ns ?? null
|
||||
const search = useSearch({ strict: false }) as ResourcesListSearch
|
||||
|
||||
return (
|
||||
<PortalShell deploymentId={deploymentId} pageTitle="Resources">
|
||||
<div className="p-6 space-y-4" data-testid="resources-list-page">
|
||||
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">{kind}</h2>
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">
|
||||
Cluster <code>{deploymentId}</code>
|
||||
{ns && (<> · namespace <code>{ns}</code></>)}
|
||||
{search.search && (<> · search=<code>{search.search}</code></>)}
|
||||
{search.region && (<> · region=<code>{search.region}</code></>)}
|
||||
</p>
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">
|
||||
Resource list (pending live data binding).
|
||||
</p>
|
||||
</div>
|
||||
</PortalShell>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* ResourcesSearchPage — minimal target-state route surface (qa-loop iter-6
|
||||
* Cluster-A `spa-target-state-routes-missing`).
|
||||
*
|
||||
* Cross-resource k8s search across the catalyst-cache. URL contract:
|
||||
* /app/$deploymentId/resources/search?q=<query>
|
||||
*
|
||||
* Stub renders a chrome shell with the query echoed back so the route
|
||||
* resolves to a 200 with the expected query token. Real search wiring
|
||||
* lands in subsequent slices.
|
||||
*/
|
||||
|
||||
import { useParams, useSearch } from '@tanstack/react-router'
|
||||
import { PortalShell } from '../PortalShell'
|
||||
import { useResolvedDeploymentId } from '@/shared/lib/useResolvedDeploymentId'
|
||||
|
||||
export interface ResourcesSearchSearch {
|
||||
q?: string
|
||||
}
|
||||
|
||||
export function ResourcesSearchPage() {
|
||||
const params = useParams({ strict: false }) as { deploymentId?: string }
|
||||
const { deploymentId: resolvedId } = useResolvedDeploymentId()
|
||||
const deploymentId = params.deploymentId ?? resolvedId ?? ''
|
||||
const search = useSearch({ strict: false }) as ResourcesSearchSearch
|
||||
const q = search.q ?? ''
|
||||
|
||||
return (
|
||||
<PortalShell deploymentId={deploymentId} pageTitle="Resources">
|
||||
<div className="p-6 space-y-4" data-testid="resources-search-page">
|
||||
<h2 className="text-xl font-semibold text-[oklch(85%_0.01_250)]">Search</h2>
|
||||
<p className="text-sm text-[oklch(55%_0.01_250)]">
|
||||
Search results for <code>{q || '(empty query)'}</code> in <code>{deploymentId}</code>{' '}
|
||||
(pending live data).
|
||||
</p>
|
||||
</div>
|
||||
</PortalShell>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user