frontend: Add first-class label and selector support to resource views - #4760
frontend: Add first-class label and selector support to resource views#4760illume wants to merge 7 commits into
Conversation
75b3e7b to
ca0d4f4
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds Kubernetes label selector filtering functionality to all resource list views in Headlamp, allowing users to filter resources using standard label selector syntax (e.g., app=nginx, env in (prod,staging)) similar to kubectl -l. The feature includes Redux state management, localStorage persistence, URL parameter support, and a new LabelSelectorInput component integrated into resource list headers.
Changes:
- Redux state management extended with label selector filter state, actions, and hooks with comprehensive test coverage
- New LabelSelectorInput component with accessibility support, keyboard shortcuts, and URL/localStorage persistence
- URL utilities extracted for reusable query parameter handling across namespace and label selector filters
- Integration into ResourceTable and resource list views with automatic filtering based on label selector
Reviewed changes
Copilot reviewed 109 out of 109 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/redux/filterSlice.ts | Added labelSelector state, setLabelSelectorFilter action, useLabelSelector hook, and resetFilter updates |
| frontend/src/redux/filterSlice.test.ts | Comprehensive tests for label selector state management and persistence |
| frontend/src/lib/storage.ts | Added getSavedLabelSelector/saveLabelSelector functions with per-cluster localStorage support |
| frontend/src/lib/storage.test.ts | Test coverage for label selector persistence including error handling |
| frontend/src/lib/urlUtils.ts | Extracted shared URL query parameter utilities (addQueryParams, getFilterValueFromURL, getFilterValuesFromURL) |
| frontend/src/lib/urlUtils.test.ts | Test coverage for URL utility functions |
| frontend/src/components/common/LabelSelectorInput.tsx | New component with text input, clear button, keyboard shortcuts, and Redux/URL integration |
| frontend/src/components/common/LabelSelectorInput.test.tsx | Comprehensive component tests covering interaction, persistence, and URL initialization |
| frontend/src/components/common/LabelSelectorInput.stories.tsx | Storybook stories demonstrating various label selector states |
| frontend/src/components/common/SectionFilterHeader.tsx | Integrated LabelSelectorInput alongside NamespacesAutocomplete with noLabelFilter prop |
| frontend/src/components/common/NamespacesAutocomplete.tsx | Refactored to use shared addQueryParams utility |
| frontend/src/components/common/Resource/ResourceTable.tsx | Added labelSelector from Redux state to resource useList calls |
| frontend/src/components/pod/List.tsx | Optimized to extract namespaces once and pass labelSelector to Pod.useList |
| frontend/src/i18n/locales/*/translation.json | Added "Label Selector" and "e.g. app=nginx" translation keys |
| docs/learn/filtering-resources.md | User guide explaining label selector syntax, usage, and keyboard shortcuts |
| Multiple snapshot files | Updated with label selector input rendering in resource list headers |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
unlikelyzero
left a comment
There was a problem hiding this comment.
podDisruptionBudget/Details.tsx dropped the optional chaining the old selectors getter had - it's now item.spec.selector.matchLabels instead of item.spec?.selector?.matchLabels. spec.selector is legitimately optional on a PDB, so a PDB with none (or one that arrives partially populated mid-watch) throws and takes out the whole details page.
Bigger picture: the label filter UI shows up on every list page but only actually applies inside TableFromResourceClass's resourceClass path - roles, CRDs, CR instances and a few others fetch their own data and silently ignore the selector while still showing it as active. The noLabelFilter prop that looks built for exactly this has no call sites anywhere, and wouldn't stop the filter from applying even if used.
Selector links also only read matchLabels, dropping matchExpressions - there's already a labelSelectorToQuery in lib/k8s that handles both, used elsewhere in this same PR, just not here. And the URL-to-Redux sync for the selector is one-way, so Back doesn't clear a filter and it can silently follow you across pages.
A few more things inline.
053b8ad to
fdcd417
Compare
|
Thanks @unlikelyzero and @Suyog241005 for the reviews!! Work in progress, marking it draft. |
@Suyog241005 |
c9e58c7 to
6bc4914
Compare
@unlikelyzero
Selectors remain cluster-global during ordinary navigation between supported resource lists, matching the existing namespace-filter behavior, but they are now always shown in the active-filter summary and are hidden on pages that cannot apply them. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 205 out of 223 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
frontend/src/components/common/LabelSelectorInput.tsx:98
- A malformed
labelSelectoralready present in the URL is dispatched and persisted as soon as this editor is opened, without callingvalidateLabelSelector. That also triggers list requests with the invalid selector, contradicting the validation path used for typed input. Keep the URL text visible for correction, but only dispatch it when validation succeeds.
frontend/src/components/common/SectionFilterHeader.tsx:59 - Defaulting this to
falseopts every directSectionFilterHeaderconsumer into Kubernetes label filtering. Non-resource pages such as Plugins and Notifications pass onlynoNamespaceFilter, so they now show a filter action that mutates global selector state but cannot filter their tables (the updated PluginSettings snapshots expose this). Make label filtering opt-in here;ResourceListViewalready passesnoLabelFilterexplicitly for resource-backed lists.
frontend/src/components/common/Resource/ResourceTable.tsx:212 - For the Namespace list with configured
allowedNamespaces,kubeObjectListQueryswitches toallowedNamespaceListQuery, which fetches each Namespace by name and discards all query parameters. Consequently this newly supplied selector is never applied: the header shows an active label filter while every allowed Namespace remains visible. Either disable this filter for that fallback or make the synthesized list honor the selector.
frontend/src/components/podDisruptionBudget/Details.tsx:54 - This renders the selector only when
matchLabelsexists. A valid PodDisruptionBudget selector may contain onlymatchExpressions, so that selector disappears entirely and cannot link to its matching Pods. Render expression-only selectors too (and keepKubePDB.spec.selector.matchLabelsoptional rather than narrowing the Kubernetes type).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 208 out of 226 changed files in this pull request and generated no new comments.
Suppressed comments (6)
frontend/src/components/common/LabelSelectorInput.tsx:101
- Opening the editor on a URL containing an invalid selector bypasses the validation in
SectionFilterHeader. The header initially rejects the URL, but this mount effect runs when the previously hidden input is opened and dispatches the raw value, which then persists it and sends it with list requests. Remove this duplicate URL hydration and let the validated header synchronization own URL state (or validate before dispatching here).
frontend/src/components/pod/List.tsx:583 - The Pod request is filtered, but the parallel PodMetrics request is still unfiltered. Since metrics pagination follows the unfiltered ordering and is only advanced when the filtered Pod list has
loadMore, matching Pods beyond the first metrics page can permanently show no metrics. Pass the samelabelSelectortoPodMetrics.useListso both result sets stay aligned.
frontend/src/components/podDisruptionBudget/Details.tsx:54 - A Kubernetes
LabelSelectorcan contain onlymatchExpressions. In that valid case this condition renders no selector at all, so users cannot follow the new PDB-to-Pods workflow even thoughlabelSelectorToQuerysupports the expression. Render a clickable representation of expression-only selectors as well asmatchLabels.
frontend/src/components/statefulset/Details.tsx:77 - StatefulSet selectors may validly use only
matchExpressions; in that casematchLabelsis undefined and this grid renders no entries, leaving no way to follow the new selector-to-Pods link. Add a display/link for expression requirements instead of relying on a match-label chip to host the complete selector URL.
frontend/src/components/daemonset/Details.tsx:132 - DaemonSet selectors may contain only
matchExpressions. With nomatchLabels, this grid is empty and the complete selector has no clickable entry, so the new selector-to-Pods navigation is unavailable. Render expression requirements as linked selector content too.
frontend/src/components/job/Details.tsx:133 - Job selectors can validly be expression-only. This
matchLabelsguard suppresses the selector row in that case, so users cannot navigate to the matching Pods even thoughlabelSelectorToQuerysupports those expressions. Render a clickable representation formatchExpressionswhen no match-label entries exist.
Reuse the shared query-state hook for filter URLs. Add persistence, Redux state, validation, and the selector input.
Apply selectors in Kubernetes requests and keep active filters visible.
Open filtered Pod and Node lists from resource detail metadata.
Probe resource kinds server-side before opening matching lists.
Verify validation, detail navigation, and global search against Kind.
Keep supported locale catalogs aligned with selector workflows.
Explain filtering, detail navigation, global search, and responsive UI.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 208 out of 226 changed files in this pull request and generated no new comments.
Suppressed comments (6)
frontend/src/components/common/LabelSelectorInput.tsx:98
- Malformed selectors from a bookmarked URL bypass validation here.
SectionFilterHeaderinitially rejects an invalid query, but when the user opens the editor this component mounts, dispatches that same value, persists it, and immediately sends it with list requests. Validate before dispatching; keep the invalid text/error local instead.
frontend/src/components/common/SectionFilterHeader.tsx:59 - Label filtering now defaults on for every direct
SectionFilterHeaderconsumer, including non-resource pages such as Plugins (PluginSettings.tsx:249) and Notifications (Notifications/List/List.tsx:139). Their new “Filter resources” control only mutates global Kubernetes filter state and cannot filter the displayed data. Default this to hidden and letResourceListView/other actual resource lists opt in explicitly.
frontend/src/components/App/Layout.tsx:232 - A bookmarked selector can be overwritten on initial load. The descendant
SectionFilterHeaderhydrates the URL selector in its mount effect, then this ancestor effect restores the persisted selector; because the header subsequently ignores unchanged URL values, Redux and requests retain the saved selector while the address bar shows another one. Coordinate restoration with URL hydration so a present, valid query parameter has precedence.
dispatch(restoreFiltersForCluster(cluster));
frontend/src/components/common/Resource/MetadataDisplay.tsx:301
- Links are only created while iterating
dict, so a valid selector containing onlymatchExpressionsrenders no clickable target even whencompleteLabelSelectoris present. Workload/PDB/Job callers pass onlymatchLabelsasdict, making expression-only selectors invisible and preventing the advertised navigation. Render a standalone complete-selector entry when the dictionary is empty, or model expression entries explicitly.
frontend/src/components/globalSearch/GlobalSearchContent.tsx:205 - Recognizing
app=nginxas a selector does not stop the existing namespace-option builder from also treating that text as a namespace. Global search therefore offers “Set namespace: app=nginx”; selecting it stores an invalid namespace and generates failing/namespaces/app=nginx/...requests. Exclude parsed selectors from the free-form namespace option path.
frontend/src/components/common/NamespacesAutocomplete.tsx:161 - This callback runs for every checkbox selection, so
SectionFilterHeadercloses the entire editor after the first namespace even though this autocomplete ismultipleand usesdisableCloseOnSelect. Users cannot select several namespaces in one interaction. Keep the editor open for ordinaryonChangeevents and invokeonApplyonly from the explicit Enter/apply path.
Summary
Imagine not being able to click on labels or tags. Now in Headlamp, you can.
This PR adds Kubernetes label filtering to resource list views. It supports the full Kubernetes
label selector syntax, exactly the same syntax accepted by
kubectl -l.It also supports the workflow described in #6932: labels on resource detail pages are links. Clicking one opens the list for the same resource type with that label applied as a URL-backed filter. The shared metadata component makes this available to Pods and other Kubernetes resource types.
Namespaces and Label Selector are positioned like subtitles beneath the resource title. Previously, on
larger browser windows, the namespace filter could appear far to the right and out of the user's
immediate view, making it unclear that the current page was filtered by namespace. Placing both
filters beneath the resource title lets people quickly understand what the page contains and how
it is scoped. The active values are subtitle links: they adopt link styling on hover or focus, and
clicking one opens the editor. A filter icon beside Create always toggles both fields, and applying
a value with Enter returns to subtitle display mode. The subtitle location is the right place for
Namespaces and Label Selector because together they define the current view.
Related requests:
Why Labels Matter
People use labels in kubernetes. A lot.
Namespaces are essential for scope, access control, and isolation, but they provide only one coarse
dimension: a namespaced resource belongs to one namespace. Labels complement that boundary with
many simultaneous dimensions. The same object can be grouped by application, component,
environment, team, owner, release, tier, and topology, without changing where it lives.
For discovery and day-to-day operations, labels can therefore be as important as namespaces, and
often more useful. Labels can identify related resources within a large shared namespace, across
multiple namespaces, and among cluster-scoped resources that have no namespace. Kubernetes also
understands label selectors directly: controllers, Services, and policies use them to associate
resources in ways that namespace membership alone cannot express.
Helm charts, GitOps workflows, monitoring, cost management, security policy, and automation also
rely heavily on labels. As clusters grow, namespaces remain the isolation boundary, while labels
become the flexible index used to find and operate on related resources. Making label selectors a
first-class list filter gives both organizational mechanisms appropriate weight in Headlamp.
Performance and Scalability
Label selectors are passed to the Kubernetes API, so Kubernetes filters resources before returning
them to Headlamp. Headlamp does not need to download every object and then filter the full set in
the browser, reducing network transfer, memory use, and client-side processing for large clusters.
Server-side filtering is also required for correct results when list requests are paginated. With
30,000 or more resources, client-side filtering can inspect only the pages fetched so far and miss
matching objects on pages that have not been downloaded. Passing the selector to Kubernetes makes
each returned page part of the complete filtered result set without fetching all unfiltered items.
Changes
app=nginx), inequality(
tier!=backend), set-based (env in (production,staging)andenv notin (dev)), existence(
partition), and non-existence (!partition) queries.medium and large screens.
limit=1instead of downloadingunfiltered resource lists for selector searches.
Steps to Test
app=nginxin Label Selector, and press Enter orleave the field.
labelSelector=app%3Dnginxappears in the URL.app: nginx.app=nginxapplied.app in (and verify the field shows an error withoutchanging the URL or results.
environment in (production),tier in (frontend), and verify aPods result appears when matching Pods exist.
Validation
npm --prefix frontend run tscnpm --prefix frontend test -- --run --reporter=dot --silent=passed-onlynpm --prefix frontend run buildnpx playwright test tests/labelSelectors.spec.tsselector search.
app: nginxopened#/c/kind-test/pods?labelSelector=app%3Dnginx.app=nginx.opacity: 1).corresponding filtered lists.
limit=1.Screenshots
Cleaner when no filters are set
If no selectors are set, then they are not shown. This is a bit cleaner than before, because now
there is no empty Namespaces input when Namespaces is empty. Instead, people can press the filter
icon to filter by namespace or label selector.
Detail pages: labels and selectors are clickable now
Before this change, labels and selectors on resource detail pages were static text. They are now
clickable links that open the corresponding resource list with the selected label or selector
applied.
Click on a label and it goes to the List view, so you can see all the pods with that label.
Pod labels and Node Selectors
Pod labels open a filtered Pods list. Node Selector chips open the filtered Nodes list, using the
same selector in the Kubernetes request.
Workload Selectors
Workload Selector chips open matching Pods, while Node Selector chips open matching Nodes.
Filtered Pod list
The URL, all three Namespaces, and the long Label Selector remain visible as subtitle links.
Global search works with kubernetes selectors now
Type a Kubernetes label selector into global search, for example
environment in (production),tier in (frontend). Headlamp shows a result for each resource typewith at least one match, such as Pods. Click the result to open that resource list with the
selector applied.
Headlamp asks Kubernetes for only one matching object per resource type while building these
results. This keeps the search lightweight and avoids downloading entire unfiltered resource lists.
Query examples tooltip
Seasoned Kubernetes users may already know label selector syntax from
kubectl -l. New users candiscover the same syntax by hovering over, focusing, or tapping the information icon. The tooltip
uses an opaque surface so the page behind it cannot interfere with readability.
Subtitles, before they could be missed
In this 'before' screenshot, look at 'Pods' title, now tell me which namespaces this view is for? All? Only some? Which ones? You can't tell.
On the main branch, a large screen pushes the selected namespace context far to the right of the
resource title. Even with three namespaces selected, the empty gap makes the active scope easy to
miss. Labels are not shown because the main branch does not yet have the Labels filter.
After
Now you know what namespaces this view is for. Before it was very easy to miss, and even if you
noticed, you needed to click the namespace filter box to see.
Hover
The values keep subtitle styling at rest. On hover or keyboard focus, link styling and a descriptive
tooltip make the editing action discoverable.
Clicking filter selector icon or clicking on a selector brings up selector edit input fields
When neither Namespaces nor Label Selector is set, the filter icon directly after Create shows both
editable fields. While editing, the same action hides the fields and returns to display mode.
Mobile
Namespaces and Label Selector are shown as compact subtitle links. Three matching Pods keep the
filtered result visible below them.
Input selectors variation
For comparison, the editor keeps both labeled inputs full-width while leaving matching Pods
visible below.
Medium
All three namespaces and the realistic long selector remain visible as editable subtitles.
Large
The subtitle treatment keeps page scope easy to scan without stretching controls across the row.
Assisted by copilot