Browsing resources

The left navigation, the resource lists it opens, and the detail drawer behind a row. This is the surface you spend most of your time in, so it is worth knowing exactly what it shows — and, in one important case, what it deliberately does not send.

1. The navigation

The left menu is a declarative registry, not a hand-built menu. NavCatalog holds 39 built-in kinds across 8 static categories, in this order:

Category Kinds Contents

Cluster

3

Nodes, Namespaces, Events

Workloads

8

Pods, Deployments, Stateful Sets, Daemon Sets, Replica Sets, Jobs, Cron Jobs, Replication Controllers

Autoscaling

1

Horizontal Pod Autoscalers, joined at runtime by the cluster’s Vertical Pod Autoscalers where that CRD is installed — see Autoscaling joins a built-in kind and a CRD-delivered one

Config

12

Config Maps, Secrets, Resource Quotas, Limit Ranges, Pod Disruption Budgets, Priority Classes, Runtime Classes, Leases, Mutating Webhook Configs, Validating Webhook Configs, Validating Admission Policies, Validating Admission Policy Bindings

Network

6

Services, Ingresses, Endpoint Slices, Endpoints, Ingress Classes, Network Policies

Storage

3

Persistent Volume Claims, Persistent Volumes, Storage Classes

Access Control

5

Service Accounts, Cluster Roles, Roles, Cluster Role Bindings, Role Bindings

Custom Resources

1

Definitions (the CRDs themselves)

The catalog is served per cluster at GET /api/v1/clusters/{id}/nav, because two of its parts depend on what the cluster actually has installed.

1.1. Custom Resources are discovered, not configured

Under Custom Resources, kweblens adds one collapsible sub-group per API group, generated from the cluster’s own CRDs and sorted alphabetically; the kinds inside each group are sorted by label. Nothing has to be registered, and a CRD installed after startup appears when the catalog is next fetched.

Custom resource lists are rendered with the CRD’s own printer columns — the same additionalPrinterColumns that kubectl get uses — so a custom kind gets meaningful columns rather than just name and age.

1.2. The Gateway category is promoted at runtime

Gateway API is delivered as CRDs, but it is networking, not a curiosity. When the gateway.networking.k8s.io group is discovered, its kinds are lifted out of Custom Resources into their own Gateway category, placed immediately after Network — where someone looking for ingress-shaped things will actually look. The kinds are removed from Custom Resources at the same time, so they never appear twice.

Inside the category the order follows traffic flow rather than the alphabet — GatewayClass, Gateway, ListenerSet, HTTPRoute, GRPCRoute, TLSRoute, ReferenceGrant, BackendTLSPolicy — because reading top to bottom then follows a request. Kinds outside that list (a newer API version, an extension of the same group) sort after the known ones rather than being dropped.

The promotion is conditional. A static entry for a CRD-delivered kind would give every cluster without Gateway API a dead category that errors when clicked, so the category only exists where the group does.

1.3. Autoscaling joins a built-in kind and a CRD-delivered one

The two answers to "what scales this workload" used to be filed in two places: the built-in HorizontalPodAutoscaler sat in Config, next to ConfigMaps and Secrets, while the CRD-delivered VerticalPodAutoscaler sat under its API group in Custom Resources. They are one category now — Autoscaling, immediately after Workloads, the thing they act on.

Inside it the order is by what each kind changes: an HPA changes how many pods there are, a VPA changes how big each one is, and "how many" is the question asked first when a workload’s capacity moves.

The category is on every cluster, and only the VPA half is conditional. HorizontalPodAutoscaler is a built-in kind — autoscaling/v2, served by every supported cluster — so there is always something to navigate to, and an empty list is a correct answer: it is how you see that a cluster has no HPAs yet. Every one of the other 38 built-in kinds is in the menu on those terms, and nothing is probed to decide it. The VPA kinds appear where their CRD is installed, which is the Gateway rule and the only thing that rule covers: a kind the cluster’s API does not serve does not go in the menu.

VerticalPodAutoscalerCheckpoint is deliberately not in the category. It is the recommender’s saved histogram for one target — the cluster writes them, and there are more of them than there are VPAs — while the recommendation an operator wants is on the VPA’s own status. It stays under Custom Resources with its API group, so it is demoted rather than hidden.

2. Namespace filter and badge counts

The top-bar namespace selector — All namespaces plus the cluster’s namespaces — scopes the lists. The filter is applied server-side, as a namespace query parameter on the list, watch and count requests, not by filtering a full list in the browser. Cluster-scoped kinds send no namespace at all: a PersistentVolume or a ClusterRole has none to filter on.

The choice is remembered per cluster, so switching clusters does not carry one cluster’s namespace over to another where it may not exist.

Separately, the list header has its own filter box, which narrows the rows already loaded and reports N of M while it is active. That one is client-side — see Filtering a list.

Each nav entry carries a badge count. Those come from GET /api/v1/clusters/{id}/counts, which computes all of them without listing anything: one limit=1 request per kind plus the metadata.remainingItemCount the API server returns. That matters because the badges are recomputed on every namespace switch, and doing it by listing was measured at 22.1 MB of API-server traffic per call on a real cluster.

remainingItemCount is best-effort, so its absence is handled explicitly rather than guessed: no continue token means the page is the whole collection and the count is exact; a truncated page without the field falls back to a full list.

A category badge is the sum of its kinds' counts, and counts only the kinds it actually has a number for. A category with no counts available shows no badge at all rather than a number that looks like a total but is really the number of menu entries.

3. Filtering a list

Filtering the Pods list by status
Figure 1. The status chips are the status: vocabulary made clickable — the same words the filter box accepts

The box in the list header takes a filter expression. Typing a plain word is a case-insensitive substring search over name, namespace and kind, exactly as it has always been; everything else is opt-in on a character a substring never contained. The ? button beside the box shows the same table at any time.

A query is whitespace-separated terms, and every one of them has to match — the same conjunction kubectl -l a=b,c=d uses. A leading - negates any term.

Term Matches

nginx

name, namespace or kind contains nginx

-nginx

everything that does not

web prod

both, ANDed

"two words"

text with a space in it

/^web-\d+$/

a regex over name, namespace and kind, case-insensitive and Unicode-aware

ns:kube-system

one field only — also name: and kind:

name:/^web/

a field matched by regex

status:Pending

the state an overview card counts — the whole word, not part of it

status:/backoff/

states matched loosely — every kind of backoff

app=web, app==web

label equality

app!=db

the label differs or the object has no app label

env in (dev,stage)

set membership — also notin

label:app

the label exists; -label:app for objects without it

Label matching is Kubernetes' own, taken from apimachinery’s `labels.Requirement.Matches, including the part that surprises people: != and notin match an object that does not carry the key at all. Label values compare exactly, case and all, like kubectl -l; only the free-text matching is case-insensitive.

A /regex/ belongs to the engine that runs it — V8 in the browser, java.util.regex in the terminal — and the browser compiles it in Unicode mode (iu). That is what makes \p{L} the letter class rather than a search for the text p{L}, which is what it silently was until #410; it also makes . one whole character and folds case over the whole of Unicode, both of which the terminal always did. The cost is that a pattern leaning on JavaScript’s old web-compatibility rule — an escape the engine does not define, such as \- for a plain - — is now an error naming the reason instead of a different pattern. Refused means refused: the query is not applied and every row stays on screen.

Three deliberate differences from kubectl -l, stated in the help popover as well as here:

  • Kubernetes writes label presence as a bare partition and absence as !partition. Here they are label:partition and -label:partition, because a bare word is a text search and has to stay one.

  • The numeric requirements key>1 / key<1 are not supported, and a term shaped like one is an error rather than a silent substring search.

  • Arbitrary field selectors (--field-selector status.phase=Running) are not implemented — no JSON path is addressable. status: is the one field selector there is, and it selects on kweblens' own state vocabulary rather than on status.phase.

3.1. status: — one vocabulary for the cards and the lists

status: matches the state the server computed for the object, which is the same value the category overview cards count. A card reading 80 Running · 3 Pending · 2 Failed and the rows status:Pending selects are therefore the same objects, not two similar answers.

That is the reason the state is computed on the server and shipped on the row rather than derived in the browser. For several kinds the browser could not derive it at all: a Service is broken when nothing is behind it, which lives in the Endpoints object; a claim is "Nearly full" only according to the metrics backend; a ConfigMap is "Not referenced" only after scanning the namespace’s pods, service accounts and ingresses. Those three are shipped too — the list request opens the same second collection the card used, once, before it walks the rows (see Health and diagnostics for what each costs). And a rule transcribed into two languages drifts without either side going quiet — both keep answering, just differently.

Two consequences worth stating:

  • The match is on the whole label, case-insensitively. Every other term is a substring search, because half a pod name is a useful question. Half a state is not: as a substring, Complete would take Completed with it, and the card’s number and the list’s rows would stop agreeing without anything on screen saying so. Use a regex — status:/backoff/ — when the loose question is the one you meant, and quote a state that contains a space: status:"No endpoints".

  • A kind kweblens has no verdict for carries no state, and no status: term selects it. That is the absence of a claim rather than a claim of health. Today the states are computed for the workload kinds — Pods, Deployments, StatefulSets, ReplicaSets, DaemonSets, Jobs and CronJobs — for the cluster-scoped Nodes and Namespaces, and for the four kinds whose verdict needs a second collection: Services (Serving, No endpoints, Not ready), PersistentVolumeClaims (Bound, Nearly full, and the other binding phases), ConfigMaps and Secrets (Referenced, Not referenced, and Cluster-managed for the ones the cluster owns). Node states are kubectl’s own spelling, so status:Ready does not select a cordoned node: that one is counted as status:Ready,SchedulingDisabled, on its own line of the card.

A query that does not parse is not applied, and the header says why — an invalid regex, an unterminated quote, a missing label key. Every row stays on screen and the count goes back to the plain total, because "nothing matched" and "your pattern is broken" are different claims and only one of them is true. The parser is kweblens-ui/src/objectFilter.ts; its grammar and every malformed input are pinned by objectFilter.test.ts.

The filter is client-side, and that is a decision rather than an accident. A filter evaluated over a truncated page reports "no matches" for an object that exists, which is the failure the Performance and scale page’s paging question was closed to avoid. The list endpoint returns the whole collection — chunked internally, which the client never sees and which never truncates — so a filter that runs in the browser sees every object and cannot give that answer.

4. What a list row shows

The cluster Events list
Figure 2. The Events list. Normal and Warning wear the same green/amber vocabulary the status column uses, so one glance reads the same way on every table

Every list has the same frame — a selection checkbox, Name, Namespace (for namespaced kinds; clicking it sets the namespace filter), the kind’s own columns, Age, and a ⋮ menu — with the middle columns varying by kind. ConfigMaps add a Keys count; Secrets add Type and Keys. Custom resources get the CRD’s printer columns, taking the CRD’s stored version and dropping the wide-only ones, which is the same view kubectl get gives you by default.

5. Live updates

Lists are live. GET …/resources/{resourceId}/objects fetches the current objects and …/objects/watch streams Kubernetes watch events over SSE, so rows appear, change and disappear without a refresh.

Two properties of that stream are worth knowing:

  • Events are batched per animation frame. A burst of watch events on a large list is coalesced into at most one update per frame, which is what stops a busy namespace freezing the tab. This is guarded by a test, not just by intent.

  • Every stream carries a keepalive. An SseEmitter only learns its client is gone from a failed write, and a quiet cluster produces no writes — so a departed subscriber would otherwise hold a cluster-side watch open indefinitely (measured at over five minutes). A :keepalive comment every 15 seconds fails fast on a closed socket, completes the emitter and closes the watch.

6. A list row is not the whole object

This is the one place where what you see in a list is deliberately less than what exists, and it is worth being precise about, because it is not redaction.

List payloads pass through a projection at the web boundary that does two things:

  • managedFields is stripped from every object. It is per-manager field-ownership bookkeeping, nothing in the UI renders it, and it is 37–48% of a real Pods or ReplicaSets payload.

  • ConfigMap and Secret values are dropped, keys kept. data, stringData and binaryData arrive as their keys mapped to JSON null. The list only ever shows a key count, and that data is 96–99% of those payloads.

Across six kinds on a real cluster this took a listing from 9.85 MB to 1.28 MB, 98.8% of the saving on Secrets alone.

The strip is restricted to ConfigMap and Secret on purpose — on a custom resource data can be the entire spec, and a CRD’s printer columns are free to render it.

A null value renders as —, never as an empty string. The two are different claims: — means kweblens did not send this value, whereas "" would mean the value is empty. Inventing an empty string here would be a lie about the cluster.

Both the objects endpoint and the objects/watch stream go through the same projection — separate code paths, one rule.

6.1. One thing a row has that the cluster does not

The same projection adds a field: kweblensState, {"label": …, "tone": …} — the object’s state in the vocabulary described under status:, computed by the same call the overview cards tally their numbers from. It is what lets a filter in the browser select exactly the objects a card counted. Kinds with no verdict get no field at all, and it is added in the projection rather than in either controller for the same reason the strips are: the list and the watch are separate code paths feeding one table, and a field added to only one of them would be erased by the first watch event to arrive.

6.2. The drawer still shows every value

Opening a row’s detail drawer refetches the full object from GET …/object whenever the row it was given contains a null inside one of those fields. The predicate reads the row rather than consulting a hard-coded list of kinds, deliberately: a kind list in the browser would be a second copy of the server’s rule, and the copy that goes stale silently.

So the projection bounds what a listing pushes into the browser. It does not hide anything from you. Per the identity model, an operator may read any Secret the underlying credentials can reach, and the drawer shows the real values — see Security and identity.

The command palette
Figure 3. Ctrl/⌘-K: switch cluster, or jump straight to a kind

The command palette (Ctrl/⌘-K) is both the navigation jump — switch cluster, jump to a kind — and the global object search. Typing at least two characters queries GET /api/v1/clusters/{id}/search?q=…, debounced, and merges matching objects into the results alongside the navigation entries.

The search spans 13 kinds rather than every kind in the catalog, querying up to 8 at a time: Deployments, StatefulSets, DaemonSets, CronJobs, Services, Ingresses, ConfigMaps, Secrets, PersistentVolumeClaims, Namespaces, Nodes, and — ranked below those — Pods and Jobs. ReplicaSets are deliberately excluded: they are generated, numerous, and almost never what someone is looking for by name.

Ranking prefers a prefix match over a word-boundary match over a substring, prefers shorter names, and gives a bonus to the eleven primary kinds — so searching a Deployment’s name finds the Deployment before the pods it generated.

Results are capped (20 by default, 100 maximum). Crucially, a kind that was not searched is reported rather than silently omitted: if a query is superseded by a newer one, or a kind is skipped, the response names it and the reason. An incomplete search says it is incomplete.

Search respects the namespace filter.

8. The detail drawer

Clicking a row opens a drawer served by GET /api/v1/clusters/{id}/detail/{resourceId}/{namespace}/{name}, which returns the object plus its resolved relations. The joins are done server-side, in one request — the alternative was N extra requests per drawer open from the browser, and the same payload also serves other consumers.

Alongside the overview it offers the object’s YAML (see Editing and applying), its events, and — for pods — logs, a shell, and optionally files.

8.1. The twelve relation sections

Sections appear only where they apply, so you will never see all twelve at once:

Section Shown for, and what it answers

Endpoints

A Service — its backing addresses, ready and not-ready distinguished

Routed By

A Service — the Ingresses that route traffic to it

Selected Pods

Anything with a pod selector — the pods that selector actually matches

Mounted By

A ConfigMap, Secret or PersistentVolumeClaim — which pods mount it

Replica Sets

A Deployment — its ReplicaSets, which is also its rollout history

Autoscaled By

A Deployment, StatefulSet, ReplicaSet or ReplicationController — the HPAs targeting it

Disruption Budgets

A Pod, Deployment, StatefulSet, DaemonSet or ReplicaSet — the PDBs covering it

Bound Volume

A PersistentVolumeClaim — the PersistentVolume it is bound to

Bound Claim

A PersistentVolume — the claim bound to it

Service Account

A Pod — the ServiceAccount it runs as

Granted By

A ServiceAccount — the RoleBindings and ClusterRoleBindings that grant it permissions

Owned By

Anything with owner references — the owner chain upwards, to a depth of three

Three of them — Endpoints, Selected Pods and Mounted By — get kind-specific columns; the rest render as name/kind/namespace tables.

Each section is bounded at 100 items, and says so when it truncates rather than quietly showing a prefix: Showing the first N only — there are more. An empty section renders the literal word None., and a section kweblens is not permitted to read says that too — a permission error is reported, not folded into an empty list.

The drawer requests relations for namespaced objects only, because the detail endpoint takes the namespace in its path. Bound Claim is therefore reachable through the API but never requested by the current UI, since PersistentVolumes are cluster-scoped.

8.2. What relations deliberately do not answer

Relation carries objects, so a join whose interesting answer is that the referenced object is missing cannot be expressed: an absent reference could only be dropped (claiming a completeness that is false) or fabricated (claiming an existence that is false). Ingress TLS secret expiry is the usual request and is not implemented for that reason.

Requests-and-limits versus actual usage is not a relation either — that is the metrics path, see Metrics.

9. Limits

  • No server-side pagination. Very large clusters will be slow to list. See Performance and scale for what has been measured and where the limits bite.

  • Not RBAC-aware. The nav offers every kind in the catalog whether or not the credentials kweblens holds can list it; a kind it cannot read fails when opened rather than being greyed out.