Share the workspace building blocks extracted from the CloudNativePG workspace - #1969
Conversation
Facts, sections and the problem list become components/workspace in k8s-ui (Fact, FactGrid/Row/Value/Source, SectionHeading, FoldSection, FoldSummary, WorkspaceProblem with a rootKind prop, OpenIssueContext), RefLink moves to ui/ on the existing ResourceRef type, and toneTextClass and worseTone join toneFillClass in ui/status-tone. CloudNativePG keeps its categories, ordering, wording and builders, and binds WorkspaceProblem to its own categories. Badges read health through healthToSeverity like the rest of the app, and secondary buttons use a new .btn-secondary class instead of hand-rolled class strings.
The cluster-identity scope that keeps a query to this cluster's series already serves PVC usage and the single-claim chart, so it moves to series_scope.go under neutral names: SeriesIsolation, ErrScopeAmbiguous, ErrScopeMismatch, ClaimSelectors. JSON tags and decisions are unchanged.
namespacesWithinCache, cacheCoversNamespace and the informerScope interface move from capacity_auth.go to cache_scope.go: Capacity and CloudNativePG both use them to tell what Radar's informer holds from what the caller asked for. Behaviour, including nil (all namespaces) versus empty, is unchanged.
web/src/components/workspace holds what both workspaces' screens are built from: Notice, ScreenEmptyState, ScreenBody, Segments, FilterChips, SectionTable, the shared table classes, text helpers and the refresh-failed notice. CloudNativePG no longer imports Capacity's internals, and keeps only its own header, gate and coverage wrappers. The drawer-trail URL codec moves to utils/drawer-trail, the back label and the subject-filtered Issues link to utils/page-links, and CloudNativePG's detail kinds take kind and group from the workspace kind table.
listScope, typedKindScope, accessFromScope, kindAccess (with covers and coverage), KindCoverage and its states, readWorkspaceKind and keepGroups move to kind_access.go so the next workspace reads its kinds the same way. The group filter takes its groups as a parameter; CloudNativePG passes its two groups through cnpgWorkspaceReadKind and filterCNPGGroup. kindAccess now carries its denied namespaces instead of returning them alongside. Group filtering, the namespace-disclosure rule and every state are unchanged.
A grant arrives as {verb, group, resource, subresource, namespace} instead
of a sentence the client had to split back apart. formatGrant and
grantParts in k8s-ui word it exactly as the server's Grant.String does,
GrantText renders the parts without a regex, and every CloudNativePG type
and message that carries a grant takes the object.
A namespace the caller may list but Radar's informer does not hold was named in deniedNamespaces, and a scope the cache did not hold at all read as error. Kind coverage now names such namespaces in uncachedNamespaces, under the same disclosure rule as deniedNamespaces, and a scope the cache holds none of has the state uncached. A partial result may carry both lists. The HA operator-lease reason no longer tells the caller the Deployment is hidden from them when Radar simply does not watch it.
actions.go now holds what any integration's write actions share: ActionCapability and ActionRequest, actionError with refuseAction, changedAction, blockedAction and partialAction, the changed / context_changed / blocked / partial codes, decodeActionRequest (bounded body, required fields, the caller's dynamic client, reviewed-context check), decodeActionParams, the generic half of the error writer, and mergePatchAtVersion. The permission machinery moves with it: a Grant (verb, group, resource, subresource, namespace) in internal/auth so Prometheus-backed reads can name one too, grantPermission/grantDecision (the per-user SAR cache with resource/subresource keys under auth, the 30s SelfSubjectAccessReview memo locally), and capabilityVerdict. Grant.String() words a namespaced grant and a cluster-wide one exactly as the two CNPG methods did. CloudNativePG keeps its result fields, its all_fenced / operator_webhook_unavailable / invalid_schedule / outcome_unknown codes, the webhook wording, facts, guards, runners and grant templates. The wire is unchanged.
Every grant on the wire is now {verb, group?, resource, subresource?,
namespace?} (no namespace means cluster-wide) rather than a sentence the
client had to split apart: action capabilities, HA, recovery, storage,
fleet disk/lag/growth and history coverage, the runtime proxy and exec
permissions, and the Prometheus history charts. Grants that were built
as literal strings now come from grant templates too. Reason strings
still embed the worded grant, with unchanged text where it came from a
template; grantText words an optional one.
CNPGHASource, CNPGReadCoverage and CNPGStorageCoverage were the same
{state, grant, reason} struct under three names; ReadSource replaces the
first two and storage coverage embeds it beside its isolation field. Each
source keeps its own state constants and classifier, including how it
reads a timeout. JSON is unchanged.
Radar Hub embeds the newest frontend against whatever Radar a cluster runs, so every /api/cnpg endpoint the workspace added, and the GitOps write-evidence read, now has a capability flag (cnpgWorkspace, gitopsWriteEvidence) and a radarFeatures entry. Every CloudNativePG hook, the log fetch and stream, the report download and the write-evidence query go through the feature guard. On a Radar without the workspace the sidebar offers no workspace destinations, a /workload link stays on the standard view, the drawer and detail page drop the composed summary, actions and extra tabs, and a workspace screen opened directly says it needs a newer Radar. The two image-catalog lookups that predate the workspace stay ungated. A sidebar destination count taken over partly readable data is now a lower bound on screen as well as in its tooltip: it renders "≥N", and a zero renders the unknown dash rather than no badge. The certainty glyph moves into k8s-ui (components/workspace/certainty) for the sidebar and Capacity to share; CapacityCertainty is the same type under its old name.
The fleet metrics and fleet disk handlers each hand-rolled a semaphore, cancellation check, indexed results and join. fanOut does that scheduling; each handler still fans out over at most 64 namespaces, answers the rest as notRead itself and sorts its rows.
The Issues page and the workspace titled WAL archiving and the latest backup failure in two wordings. issueReasonTitle now answers for those reasons in both places; the workspace keeps titles only for reasons the Issues page does not title itself.
The workspace client re-parsed Argo CD and Flux labels itself and read an apps-in-any-namespace tracking ID's ns_app prefix as the Application name. topology.ManagedByFromMeta (the existing metadata-only detection, now exported; nothing else in pkg changes) answers instead, and /api/cnpg/workspace returns managedBy keyed Kind/namespace/name for every returned CNPG object with a manager signal. A native Helm release keeps an empty group and an Application named without a namespace keeps an empty one.
web/src/api/actions.ts holds the client half of the action contract: ActionCapability and ActionRequest, the shared refusal codes with actionErrorCode (an integration widens it with its own codes, as CloudNativePG does for all_fenced, operator_webhook_unavailable and invalid_schedule), actionOutcomeLocked, actionCompleted, and capabilityReason, which now words every disabled CloudNativePG action the same way and names the missing grant when the server gives no reason.
DESIGN.md states how a surface that reads several sources shows what it could not read, what it read in part and where a value came from, and names the shared components that do it. Capacity and CloudNativePG link to it and keep their own per-value tables.
Coverage gains the uncached state and uncachedNamespaces. One function, cnpgCoverageGap, words why a kind was not read for both the workspace facts and the related-object lookups: no access only when the namespace is named as denied, "Radar does not cache …" when it is named as uncached, and "not read" when the server names neither cause. Empty states and the coverage notice word the uncached case the same way.
The workspace read Argo CD and Flux labels itself, which misnamed Argo CD
applications that live outside Argo CD's namespace and ignored the Flux
namespace label. It now uses the managedBy the server derives for each
object, and the shared ManagedByText names the manager as "Argo CD
application argocd/app" or "Flux Kustomization …", linking it when its
namespace is recorded; gitOpsOwnerFromRef is exported for it.
The operator page's coverage notice words its gaps like the workspace
notice ("not cached by Radar in pg") instead of printing raw states.
The integration guide gains a Workspace integrations chapter: when a workspace is warranted, and the server, UI, navigation, version-skew and action pieces a new one imports instead of copying, plus the ones that are not shared yet because a second integration should shape them. CLAUDE.md points at it, and docs/cnpg.md describes the grant object, the uncached coverage state, managedBy and the cnpgWorkspace capability flag.
…space A /workload link and a drawer's Expand now open the workspace page only once the Radar is known to serve it, since the redirect replaces the URL; while capabilities load, or on an older Radar, they stay on the standard view, and a Cluster's Logs tab keeps the Pod logs instead of the merged instance stream. useRadarFeature returns a stable guard, so a callback built on it, like the Cluster log fetch, no longer reloads a running stream on every render. A namespace the server names as denied or uncached now decides the wording even when the kind's overall state is uncached, and an empty list names both causes when both apply.
PR Summary by QodoExtract shared workspace building blocks from CloudNativePG
AI Description
Diagram
High-Level Assessment
Files changed (181)
|
Code Review by Qodo
1. Action refusals bypass standard errors
|
An uncached read can still name namespaces the caller was denied. Empty
collection text and the coverage notice now list every cause the server
named ("no access in c; Radar does not cache Pods in b") and fall back to
the state only when it names none.
Facts, the certainty glyph and the GitOps manager text are how any surface shows observed values, single-kind renderers included, so they move from components/workspace to components/facts. The problem list with its sources moves to components/problems, and the section heading and folded section, plain layout, to ui/FoldSection. No behaviour change.
Radar Hub uses "workspace" for the customer's account, and the embedded app showed the same word as a heading inside the Resources sidebar. The block above a category's kinds now reads "Views", the upgrade note says "CloudNativePG views" and "CloudNativePG needs a newer Radar on this cluster", the error states name the CloudNativePG data, and the README, GitOps, Helm Compare and CloudNativePG docs describe these areas by their subject. "Workspace" stays an internal term for an integration with several kinds and its own screens.
Summary
The CloudNativePG workspace (#1921, #1922) carried a generic layer under CNPG names: the facts and problem list on every summary, per-kind coverage, the reviewed-action contract, grants, Prometheus series attribution, and screen layout borrowed from Capacity's internals. This PR moves that layer into shared, integration-neutral modules, so the next workspace-style integration (Velero is the likely one) imports it instead of copying CNPG.
Nothing here is released yet, so the three CNPG wire shapes that every future workspace would copy are fixed now:
It also adds the version-skew gate the CNPG endpoints were missing.
Only mechanisms whose interface is already evident in today's code are shared: each has two or more consumers, or is a contract every workspace must follow. The rest is listed below as not shared yet.
What changed
Shared UI (
@skyhook-io/k8s-ui)components/facts/, for any surface that shows observed values (single-kind renderers included):Fact, withFactGrid/FactRow/FactValue/FactSourceCertaintyGlyph, moved from Capacity;CapacityCertaintystays as the same typeManagedByTextcomponents/problems/:WorkspaceProblem<Category>andProblemCallout/ProblemList/ProblemMeta, which take the workspace'srootKindinstead of a hard-coded'Cluster', plusOpenIssueContext.ui/FoldSection:SectionHeading,FoldSection,FoldSummary.ui/RefLinknow uses the existingResourceRef.toneTextClassandworseTonelive inui/status-tone.utils/grantaddsGrant,formatGrantandgrantParts.issueReasonTitlegives a reason one title on both the Issues page and the workspace.WorkspaceProblemto its own categories.healthToSeveritylike the rest of the app..btn-secondaryclass (documented in DESIGN.md) instead of ten hand-rolled class strings.SidebarCategoryDestination.countLowerBoundrenders a count over partly read data as≥N, and its zero as unknown rather than none.App (
web/)components/workspace/holds the screen primitives Capacity and CNPG share:ScreenBody,ScreenEmptyState,Notice,Segments,FilterChips,SectionTableand its table classes,RefreshFailedNotice,GrantText, and the text helpers. CNPG no longer imports fromcapacity/shared.tsx.api/actions.tsis the client half of the action contract:ActionCapabilityandActionRequesttypesactionErrorCode, which an integration widens with its own codesactionOutcomeLockedandactionCompletedcapabilityReason, now the one "why is this disabled" wordingutils/drawer-trail.tsholds the?drawer=codec, andutils/page-links.tsthe back label and the subject-filtered Issues link.Server
internal/server/actions.go: the reviewed-action contract.ActionCapability,ActionRequest,decodeActionRequestanddecodeActionParamschangedAction,blockedAction,partialActionandwriteActionErrorgrantPermissionandcapabilityVerdict, with a real SelfSubjectAccessReview in local modemergePatchAtVersionGrantlives ininternal/authand carriesIn(ns)andString().kind_access.gocovers per-kind access and coverage (readWorkspaceKind,typedKindScope,KindCoverage);cache_scope.gohasnamespacesWithinCache.read_source.go: oneReadSourceshape for HA, recovery and storagefanout.go: the bounded per-namespace fan-outinternal/prometheus/series_scope.go:SeriesIsolation, already used by the PVC usage chartspkg/topologynow exportsManagedByFromMeta. This is additive.Wire changes (CNPG endpoints, unreleased)
grantis{verb, group?, resource, subresource?, namespace?}(no namespace means cluster-wide) on every capability, read source, chart and report item. The wording is unchanged and comes fromGrant.String()/formatGrant.Kind coverage gains:
uncached, for a scope Radar's cache does not cover at all;uncachedNamespaces, named under the same rule asdeniedNamespaces.Before, a namespace the caller can read but Radar does not cache was reported as denied, and the UI said "No access". It now says "Radar does not cache Pods in db".
/api/cnpg/workspacegainsmanagedBy, keyedKind/ns/name.ns_app) and ignored the Flux namespace label.Version skew
New
FeatureCapabilitiesflags:cnpgWorkspace(every/api/cnpg/*route except the two image-catalog lookups that predate it) andgitopsWriteEvidence. Each has aradarFeaturesentry.Every CNPG query, mutation, log stream, report download and the write-evidence query goes through
useRadarFeature. Mutations are never retried. On a Radar without the workspace:/workloadlinks and drawer Expand stay on the standard views;Vocabulary
On screen these areas are named by their subject. The block above a sidebar category's kinds reads Views, and the copy says "CloudNativePG views" and "CloudNativePG data", never "workspace", because Radar Hub uses "workspace" for the customer's account. "Workspace" remains the internal term for an integration with several kinds and its own screens (the integration guide,
SidebarCategoryWorkspace,/api/cnpg/workspace).Docs
capacity.mdandcnpg.mdlink to them and keep their per-value tables.docs/INTEGRATION_GUIDE.md: a new "Workspace integrations" chapter covering when a workspace is warranted, the pieces to import, and what is not shared yet. CLAUDE.md points to it.Not shared yet
These have one consumer, and their interface should come from the second:
pods/proxyreaderRollouts' capabilities answer "allowed" in local mode without asking the apiserver. That is a real bug, but Rollouts returns bare booleans and hides denied actions, so it needs its own PR.
Public surface
CapacityCertaintyis kept.pkg/: one additive export (topology.ManagedByFromMeta).pkg/capacityapiv1alpha1: unchanged.Testing
go build ./...;go test ./...in both modules. Thecmd/desktopenv test is flaky and passes alone.gofmt -lis clean./api/capabilities(an older Radar behind Radar Hub), the sidebar workspace, redirects, composed summaries and actions fell back to the standard views, and/cnpgshowed the upgrade note.Stacked on #1922 (which is stacked on #1921). Merge order: #1921, #1922, then this PR, before a release ships
/api/cnpg/*.Note
Medium Risk
Touches CNPG API shapes, RBAC/coverage semantics, and cluster write paths via the shared action layer; behavior is intended to be equivalent with clearer grant and cache messaging.
Overview
Extracts the generic workspace layer that CloudNativePG had under CNPG-specific names into shared modules, so future integrations (e.g. Capacity-style screens) import one contract instead of copying.
Server: Adds
internal/auth.Grant(structured RBAC on the wire),internal/server/actions.go(reviewedActionRequest, capabilities, 409/partial errors,mergePatchAtVersion),cache_scope.go/kind_accesspatterns, andprometheus/series_scope.go(shared Prometheus isolation). CNPG handlers now use these; history/PVC denial fields expose*Grantinstead of strings.FeatureCapabilitiesaddscnpgWorkspaceandgitopsWriteEvidence. CNPG workspace coverage gainsuncached/uncachedNamespaces(distinct from denied) andmanagedByfrom server-side GitOps detection.UI: New k8s-ui
facts/,problems/, fold sections,formatGrant, and appcomponents/workspace/plusapi/actions.ts. CNPG is rewired to structured grants and shared action types; secondary buttons use.btn-secondary.Docs: DESIGN.md documents unknown/partial/denied values once; INTEGRATION_GUIDE adds a workspace-integrations chapter; CLAUDE.md points builders at it.
Reviewed by Cursor Bugbot for commit 5af6ed1. Bugbot is set up for automated code reviews on this repo. Configure here.