From fd35c547730a62ad7118de6cf898408f308d8317 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 20 Aug 2026 17:22:37 +0100 Subject: [PATCH] Add canonical infrastructure search completion --- .../subsystems/frontend-primitives.md | 28 ++- .../subsystems/performance-and-scalability.md | 14 ++ .../v6/internal/subsystems/registry.json | 8 +- .../internal/subsystems/storage-recovery.md | 7 +- .../internal/subsystems/unified-resources.md | 10 + frontend-modern/browser-verification.json | 197 ++++++++++++++---- .../components/Workloads/WorkloadsFilter.tsx | 6 + .../components/Workloads/WorkloadsSurface.tsx | 1 + .../workloadSearchSuggestions.test.ts | 32 +++ .../Workloads/workloadSearchSuggestions.ts | 94 +++++++++ .../components/Workloads/workloadSelectors.ts | 7 +- .../Workloads/workloadsFilterModel.ts | 1 + .../shared/FilterBar/FilterBar.test.tsx | 130 ++++++++++++ .../components/shared/FilterBar/FilterBar.tsx | 119 ++++++++++- .../shared/FilterBar/filterCatalog.ts | 43 ++++ .../src/components/shared/SearchField.tsx | 14 ++ .../src/components/shared/SearchInput.tsx | 12 +- .../shared/__tests__/SearchInput.test.tsx | 71 ++++++- .../shared/__tests__/searchInputModel.test.ts | 51 +++++ .../src/components/shared/searchFieldModel.ts | 4 + .../shared/searchInputEnhancementsModel.ts | 1 - .../src/components/shared/searchInputModel.ts | 152 ++++++++++++++ .../components/shared/useSearchFieldState.ts | 10 +- .../shared/useSearchInputEnhancements.ts | 183 ++++++++++++---- .../components/shared/useSearchInputState.ts | 2 + .../src/features/docker/DockerAlertsTable.tsx | 1 + .../features/docker/DockerConfigsTable.tsx | 1 + .../features/docker/DockerContainersTable.tsx | 1 + .../src/features/docker/DockerHostsTable.tsx | 1 + .../src/features/docker/DockerImagesTable.tsx | 1 + .../features/docker/DockerNetworksTable.tsx | 2 + .../src/features/docker/DockerPageSurface.tsx | 5 + .../features/docker/DockerSecretsTable.tsx | 1 + .../features/docker/DockerServicesTable.tsx | 1 + .../docker/DockerStorageUsageTable.tsx | 1 + .../features/docker/DockerSwarmNodesTable.tsx | 1 + .../src/features/docker/DockerTasksTable.tsx | 1 + .../features/docker/DockerVolumesTable.tsx | 1 + .../kubernetes/KubernetesAlertsTable.tsx | 1 + .../kubernetes/KubernetesAutoscalingTable.tsx | 1 + .../kubernetes/KubernetesClustersTable.tsx | 1 + .../kubernetes/KubernetesConfigTable.tsx | 1 + .../kubernetes/KubernetesControllersTable.tsx | 1 + .../kubernetes/KubernetesDeploymentsTable.tsx | 1 + .../kubernetes/KubernetesEventsTable.tsx | 1 + .../kubernetes/KubernetesNetworkingTable.tsx | 1 + .../kubernetes/KubernetesNodesTable.tsx | 1 + .../kubernetes/KubernetesPageSurface.tsx | 13 ++ .../kubernetes/KubernetesPodsTable.tsx | 1 + .../kubernetes/KubernetesPolicyTable.tsx | 1 + .../kubernetes/KubernetesServicesTable.tsx | 1 + .../kubernetes/KubernetesStorageTable.tsx | 1 + .../platformOverviewLayout.guardrails.test.ts | 6 + .../platformSearchSuggestions.test.ts | 52 +++++ .../platformPage/platformSearchSuggestions.ts | 119 +++++++++++ .../platformPage/sharedPlatformPage.tsx | 17 +- .../src/features/proxmox/ProxmoxCephTable.tsx | 12 +- .../proxmox/ProxmoxMailGatewayTable.tsx | 1 + .../features/proxmox/ProxmoxPageSurface.tsx | 11 +- .../proxmox/ProxmoxReplicationTable.tsx | 31 ++- .../ProxmoxPageSurface.contract.test.tsx | 41 +++- .../__tests__/proxmoxPageModel.test.ts | 62 ------ .../src/features/proxmox/proxmoxPageModel.ts | 22 -- .../standalone/AgentsMachinesTable.tsx | 1 + .../standalone/AvailabilityChecksTable.tsx | 1 + .../features/truenas/TrueNASAlertsTable.tsx | 1 + .../src/features/truenas/TrueNASAppsTable.tsx | 1 + .../truenas/TrueNASNetworkSharesTable.tsx | 1 + .../truenas/TrueNASProtectionTable.tsx | 1 + .../features/truenas/TrueNASServicesTable.tsx | 1 + .../truenas/TrueNASStorageTopologyTable.tsx | 1 + .../features/truenas/TrueNASSystemsTable.tsx | 1 + .../truenas/TrueNASVirtualMachinesTable.tsx | 1 + .../src/features/vmware/VmwarePageSurface.tsx | 1 + .../features/vmware/VsphereActivityTable.tsx | 1 + .../features/vmware/VsphereAlertsTable.tsx | 1 + .../vmware/VsphereDatastoresTable.tsx | 1 + .../src/features/vmware/VsphereHostsTable.tsx | 1 + .../features/vmware/VsphereNetworksTable.tsx | 1 + .../src/utils/__tests__/searchQuery.test.ts | 9 + frontend-modern/src/utils/searchQuery.ts | 11 +- 81 files changed, 1453 insertions(+), 198 deletions(-) create mode 100644 frontend-modern/src/components/Workloads/__tests__/workloadSearchSuggestions.test.ts create mode 100644 frontend-modern/src/components/Workloads/workloadSearchSuggestions.ts create mode 100644 frontend-modern/src/components/shared/__tests__/searchInputModel.test.ts create mode 100644 frontend-modern/src/features/platformPage/platformSearchSuggestions.test.ts create mode 100644 frontend-modern/src/features/platformPage/platformSearchSuggestions.ts diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index ec51f0323..ae2d40d2d 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -212,10 +212,11 @@ puts the same machine on two surfaces that do not share an identity. 129. `frontend-modern/scripts/shared-template-audit.mjs` 130. `frontend-modern/scripts/shared-template-registry.json` 131. `frontend-modern/src/features/platformPage/sharedPlatformPage.tsx` - 131a. `frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx` - 131b. `frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx` - 131c. `frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx` - 131d. `frontend-modern/src/features/platformPage/platformEstateOverviewModel.ts` + 131a. `frontend-modern/src/features/platformPage/platformSearchSuggestions.ts` + 131b. `frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx` + 131c. `frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx` + 131d. `frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx` + 131e. `frontend-modern/src/features/platformPage/platformEstateOverviewModel.ts` 132. `frontend-modern/src/utils/platformSupportManifest.generated.ts` 133. `frontend-modern/src/utils/platformSupportManifest.ts` 134. `frontend-modern/src/utils/sourcePlatformOptions.ts` @@ -4185,6 +4186,15 @@ and `frontend-modern/src/components/shared/searchInputModel.ts` owns the shared search-input contract plus shortcut-hint and trailing-control policy. Future search-input work should extend those owners instead of pushing type-to-search or enhancement wiring back into the shared shell. +Infrastructure-aware completion follows that owner split. Product surfaces +provide safe canonical identity projections through `SearchInput` suggestions; +`useSearchInputEnhancements.ts` ranks those identities and exposes only the +single dimmed inline suffix after the current query. It must not introduce a +second results dropdown. When several identities match, the suffix stops at +their unambiguous common prefix instead of selecting the first object. Tab or +Right Arrow accepts that inline text. Enter commits either an exact identity or +a shorter query that still resolves to known suggestions; unmatched prose +remains ordinary free-text search. The shared page-controls bar now follows that same owner split. `frontend-modern/src/components/shared/PageControls.tsx` stays the render shell for canonical page-level control composition, while @@ -4214,6 +4224,16 @@ inherit the same ownership through `PlatformTableToolbar`. Recovery is event-fir and does not use equal workspace subtabs for protected rollups versus event history; Storage subtabs (Pools / Physical Disks) sit above the bar as navigation, not filters. +`FilterBar` owns committed infrastructure search terms as removable pills, +separate from its consumer-owned structured `FilterDef` chips. An exact +infrastructure completion or a recognized abbreviated query clears the draft +field, adds one search-term pill, and serializes committed terms as +comma-separated inclusive alternatives so operators can select several +objects without turning arbitrary phrases into chips. Removing a search-term +pill must immediately update the consumer search state. Platform consumers +project only canonical object identity, type, scope, status, and safe aliases through +`frontend-modern/src/features/platformPage/platformSearchSuggestions.ts`; +arbitrary resource metadata and secrets are not autocomplete candidates. `PlatformTableToolbar` must always expose contextual Clear filters for a non-empty search, including simple table-local search/status state that does not need a feature-owned reset. Route-owned or multi-facet platform surfaces diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index ae1524ff6..aaac53244 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -84,6 +84,7 @@ start a goroutine, timer, or notification lifecycle per target. 50. `frontend-modern/src/components/Workloads/useGuestDrawerState.ts` 51. `frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts` 52. `frontend-modern/src/components/Workloads/workloadSelectors.ts` + 52a. `frontend-modern/src/components/Workloads/workloadSearchSuggestions.ts` 53. `frontend-modern/src/components/Workloads/workloadTopology.ts` 54. `frontend-modern/src/components/Workloads/workloadSelectionModel.ts` 55. `frontend-modern/src/components/Workloads/workloadRouteModel.ts` @@ -719,6 +720,19 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. reads the unfiltered guest set so narrowing the table by search or status never removes the column. 18. Extend workload filter active-count, reset semantics, and mobile toolbar state through `frontend-modern/src/components/Workloads/workloadsFilterModel.ts` (defaults, `countActiveWorkloadsFilters`, `hasActiveWorkloadsFilters`) rather than rebuilding filter-local state inside `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx`. Workloads filter presentation now composes the shared `FilterBar` (`frontend-modern/src/components/shared/FilterBar/FilterBar.tsx`) with a per-page `FilterDef[]` catalog rather than the legacy `PageControls` structured control deck. High-frequency Type and Status filters stay in that catalog but render as inline compact segmented controls (`inline: true`), while longer or dynamic scope filters continue through the "+ Filter" menu and chip popovers. The Add filter control inherits FilterBar's compact accessible-only label by default instead of paying for a page-local labelled-field shell. Durable presentation controls pass only their panel content through `FilterBar.viewOptions`; the shared FilterBar owns the single View trigger and popover. Contextual actions use `leadingControls`, while frequently changed analytical orientation such as the active trend range uses `trailingControls`. + Workload autocomplete must project from the same unfiltered guest inventory + and the same search-candidate helper used by the workload predicate. The + projection includes workload identity plus deduplicated node, Docker host, + Kubernetes context, cluster, and namespace scopes so Proxmox, vSphere, + Docker, and Kubernetes embedded workload surfaces all inherit the same + inline completion and multi-term pill behavior. Committed comma-separated + workload terms, including recognized abbreviated queries, retain the + existing inclusive OR semantics; arbitrary unmatched draft text remains the + existing workload free-text query. + On the Proxmox overview, those workload terms filter only the workload + table. The node summary retains the real already-loaded node inventory so a + workload query can never produce or appear to produce the estate-empty "No + Proxmox VE nodes" state. Desktop filter/action wrap alignment must remain a pure shared flex-layout contract: the parent distributes the two rails while they fit and a wrapped action rail starts at the leading edge. It must not add resize observers, diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index f986f0fe8..dfddb1d7c 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -4731,6 +4731,7 @@ "frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx", "frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx", "frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx", + "frontend-modern/src/features/platformPage/platformSearchSuggestions.ts", "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", "frontend-modern/src/hooks/createNonSuspendingQuery.ts", "frontend-modern/src/i18n/index.ts", @@ -5245,6 +5246,7 @@ "frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx", "frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx", "frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx", + "frontend-modern/src/features/platformPage/platformSearchSuggestions.ts", "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx" ], "allow_same_subsystem_tests": false, @@ -5253,7 +5255,8 @@ "frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts", "frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx", "frontend-modern/src/features/platformPage/__tests__/platformEstateOverviewModel.test.ts", - "frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts" + "frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts", + "frontend-modern/src/features/platformPage/platformSearchSuggestions.test.ts" ] }, { @@ -6382,6 +6385,7 @@ "frontend-modern/src/components/Workloads/WorkloadPanel.tsx", "frontend-modern/src/components/Workloads/workloadRouteModel.ts", "frontend-modern/src/components/Workloads/workloadRouteStateModel.ts", + "frontend-modern/src/components/Workloads/workloadSearchSuggestions.ts", "frontend-modern/src/components/Workloads/workloadSelectionModel.ts", "frontend-modern/src/components/Workloads/workloadSelectors.ts", "frontend-modern/src/components/Workloads/WorkloadsFilter.tsx", @@ -6545,6 +6549,7 @@ "frontend-modern/src/components/Workloads/WorkloadPanel.tsx", "frontend-modern/src/components/Workloads/workloadRouteModel.ts", "frontend-modern/src/components/Workloads/workloadRouteStateModel.ts", + "frontend-modern/src/components/Workloads/workloadSearchSuggestions.ts", "frontend-modern/src/components/Workloads/workloadSelectionModel.ts", "frontend-modern/src/components/Workloads/workloadSelectors.ts", "frontend-modern/src/components/Workloads/WorkloadsFilter.tsx", @@ -6586,6 +6591,7 @@ "frontend-modern/src/components/Workloads/__tests__/workloadMetricHistoryModel.test.ts", "frontend-modern/src/components/Workloads/__tests__/workloadRouteModel.test.ts", "frontend-modern/src/components/Workloads/__tests__/workloadRouteStateModel.test.ts", + "frontend-modern/src/components/Workloads/__tests__/workloadSearchSuggestions.test.ts", "frontend-modern/src/components/Workloads/__tests__/workloadSelectionModel.test.ts", "frontend-modern/src/components/Workloads/__tests__/workloadSelectors.test.ts", "frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx", diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 248964e89..f1df569c6 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -413,8 +413,11 @@ the shared bounded table-preview disclosure so workload controls remain in the initial reading flow, but it must retain canonical node ordering and reveal the same already-loaded collection rather than changing backup or recovery scope. The workload filter belongs immediately before the workload table it controls, -after the node preview; it must not appear to filter the node table. Those -counts and layout controls must not imply +after the node preview; it must not filter the node table. The nodes table must +always receive the real already-loaded PVE node collection, so committed or +draft workload terms can never manufacture the estate-empty `No Proxmox VE +nodes` state. That empty state is reserved for genuinely absent node inventory. +Those counts and layout controls must not imply protection, verification, or restore readiness beyond the evidence held by the workflow-owned Storage, Backups, Ceph, and Mail surfaces. Proxmox backup inventory loading and load-failure chrome is likewise a diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index f43b4b511..3794c0c78 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -382,6 +382,16 @@ touch- and keyboard-operable inline detail disclosure. Future platform tables must keep that split: row data and platform semantics stay in the unified-resource consumer, and the repeated table shell stays in the shared frontend primitive. +Platform search completion follows the same ownership split. Unified-resource +consumers must project canonical resource identity, display name, type, scope, +status, and safe aliases through +`frontend-modern/src/features/platformPage/platformSearchSuggestions.ts` and +pass that projection to the shared platform toolbar. They must not expose raw +`platformData`, secret payloads, or arbitrary metadata as completion +candidates. The frontend primitive owns ranking, common-prefix completion, +recognized abbreviated terms, removable pills, and inclusive multi-term query +serialization; each platform consumer remains responsible for applying that +query to the same canonical resource collection its table renders. Large platform inventories may bound their initial row projection through the shared platform-table preview primitive so the next canonical surface remains in the initial reading flow. Proxmox node consumers retain the page model's diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 841ff0ea0..802f06492 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,38 +1,148 @@ { "version": 1, - "base_sha": "a64dd2f0052aa498e7e771466fc61a3055c77721", - "verified_at": "2026-08-20T13:26:51Z", + "base_sha": "23304bc0173919390e7953f9de671306064af78e", + "verified_at": "2026-08-20T16:13:18Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx", - "frontend-modern/src/components/Settings/GuestDockerDiscoverySettingsCard.tsx", - "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx", - "frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx", - "frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx", - "frontend-modern/src/components/Settings/useSystemSettingsState.ts", - "frontend-modern/src/i18n/messages.de.ts", - "frontend-modern/src/i18n/messages.es.ts", - "frontend-modern/src/i18n/messages.ts", - "frontend-modern/src/types/config.ts", - "frontend-modern/src/utils/systemSettingsPresentation.ts" + "frontend-modern/src/components/Workloads/WorkloadsFilter.tsx", + "frontend-modern/src/components/Workloads/WorkloadsSurface.tsx", + "frontend-modern/src/components/Workloads/workloadSearchSuggestions.ts", + "frontend-modern/src/components/Workloads/workloadSelectors.ts", + "frontend-modern/src/components/Workloads/workloadsFilterModel.ts", + "frontend-modern/src/components/shared/FilterBar/FilterBar.tsx", + "frontend-modern/src/components/shared/FilterBar/filterCatalog.ts", + "frontend-modern/src/components/shared/SearchField.tsx", + "frontend-modern/src/components/shared/SearchInput.tsx", + "frontend-modern/src/components/shared/searchFieldModel.ts", + "frontend-modern/src/components/shared/searchInputEnhancementsModel.ts", + "frontend-modern/src/components/shared/searchInputModel.ts", + "frontend-modern/src/components/shared/useSearchFieldState.ts", + "frontend-modern/src/components/shared/useSearchInputEnhancements.ts", + "frontend-modern/src/components/shared/useSearchInputState.ts", + "frontend-modern/src/features/docker/DockerAlertsTable.tsx", + "frontend-modern/src/features/docker/DockerConfigsTable.tsx", + "frontend-modern/src/features/docker/DockerContainersTable.tsx", + "frontend-modern/src/features/docker/DockerHostsTable.tsx", + "frontend-modern/src/features/docker/DockerImagesTable.tsx", + "frontend-modern/src/features/docker/DockerNetworksTable.tsx", + "frontend-modern/src/features/docker/DockerPageSurface.tsx", + "frontend-modern/src/features/docker/DockerSecretsTable.tsx", + "frontend-modern/src/features/docker/DockerServicesTable.tsx", + "frontend-modern/src/features/docker/DockerStorageUsageTable.tsx", + "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx", + "frontend-modern/src/features/docker/DockerTasksTable.tsx", + "frontend-modern/src/features/docker/DockerVolumesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx", + "frontend-modern/src/features/platformPage/platformSearchSuggestions.ts", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", + "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx", + "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx", + "frontend-modern/src/features/proxmox/proxmoxPageModel.ts", + "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx", + "frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx", + "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx", + "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx", + "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx", + "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx", + "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx", + "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx", + "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx", + "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx", + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx", + "frontend-modern/src/features/vmware/VsphereActivityTable.tsx", + "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx", + "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx", + "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx", + "frontend-modern/src/utils/searchQuery.ts" ], "content_sha256": { - "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx": "28dcc409ee274cb87100bad34c5923141f2991428bff5530cd3f40d005cb016b", - "frontend-modern/src/components/Settings/GuestDockerDiscoverySettingsCard.tsx": "12dcae6e8e515e3d0e60a5f93e3eb43e324643c9e8c7e36733b0fdcd0c5710dc", - "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx": "dde3334841438625575add15d5fa8becf46af2ad4db91d9aaff2a6e944024d1f", - "frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx": "155682d67bb2a9e482160d98d21b69bfb6600adbab1a5a8bffb1186a0c343467", - "frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx": "aede4d05bd687dd28606b0c621d2c2a13ab9ac0e2782c688b3c016ee766ea320", - "frontend-modern/src/components/Settings/useSystemSettingsState.ts": "216e99c171fed89fb8dd1b2df87fe54b3e6f28aaf9fdd2335d22ec49e87aa906", - "frontend-modern/src/i18n/messages.de.ts": "9313d26271469cdd8f53d89ddecfd7223b08766b47333a7be79d080504a0c424", - "frontend-modern/src/i18n/messages.es.ts": "3306c37ad2155c46e4f453ea80d6633131760733dd345484f5d3df238da72aa5", - "frontend-modern/src/i18n/messages.ts": "5001d13ef5fce44bdadf54343ae260e44e4e07cf1d02b8bb2e2a0da38dfe96b3", - "frontend-modern/src/types/config.ts": "275188a0f2c35fe521a3ae1bc67ca222966bc7460f1bcfd396c7fb236b90c81c", - "frontend-modern/src/utils/systemSettingsPresentation.ts": "e24b3b467cacd963723bd83c00c55b511a41928d66930b7c662226cd2939e436" + "frontend-modern/src/components/Workloads/WorkloadsFilter.tsx": "b94ca6ca1c6f713cba7b9e7363d3e4b637c46bf26de6176e6497557f7e3e6075", + "frontend-modern/src/components/Workloads/WorkloadsSurface.tsx": "32d512c214f965ebf5b910b87f2e34658c3d27533c9ed544259f731735ab0420", + "frontend-modern/src/components/Workloads/workloadSearchSuggestions.ts": "68cb9c975d9698262fd7259514f738747b2e83f3274dd37df402c96a53f48642", + "frontend-modern/src/components/Workloads/workloadSelectors.ts": "ca011fd00dccffb6cc5abdbd16bbeeab6c3880f4793273445a869c3639ca64d5", + "frontend-modern/src/components/Workloads/workloadsFilterModel.ts": "35c02e57b1f64c1c2b2e553242db00fa579e780a0a0407c1d38b13217f7e641c", + "frontend-modern/src/components/shared/FilterBar/FilterBar.tsx": "ff8b016b41e7d044bb0d587d1fb7a83bdcd0ffd48cd865ba2fe8f68650a6b901", + "frontend-modern/src/components/shared/FilterBar/filterCatalog.ts": "ee1d4e7bc8a92d9314041a403385c548dfc3651c970d2be99c05bfe2fe8b92ad", + "frontend-modern/src/components/shared/SearchField.tsx": "7729c081135ada657cf3321c780c30ab1bac6ce485a74f061fa4f47f366eac85", + "frontend-modern/src/components/shared/SearchInput.tsx": "815e420fba5d57a9b384a4f24e0da8717b42717a572871033c02eb7896826f41", + "frontend-modern/src/components/shared/searchFieldModel.ts": "9fc0197e2fa357eb419935326f8f279b11ce07dc9af3d264ffdf6a5063116cc6", + "frontend-modern/src/components/shared/searchInputEnhancementsModel.ts": "0da3597962d0267e7ca8f388029796390f61b831352148dd3ccbc8107a70531d", + "frontend-modern/src/components/shared/searchInputModel.ts": "dd01c4f223003c9e339bd10266fdd6ad3af5d6df61d491b987a6a004b7878115", + "frontend-modern/src/components/shared/useSearchFieldState.ts": "1dd0190af3c6387044315414182e5133feeb709a289f65fa4e074691a655c8ff", + "frontend-modern/src/components/shared/useSearchInputEnhancements.ts": "c4229e312439676cde3eaac1db305191f1cfa0dc0822ca35792222777ece2817", + "frontend-modern/src/components/shared/useSearchInputState.ts": "ad2221df597f07d418d082164d11a3654261cede7c388d2a8bd7e0304075c4f6", + "frontend-modern/src/features/docker/DockerAlertsTable.tsx": "5fde1ec1acac4011a5bfa4a5467558104b536b752bc49dfc24c691a531cc5c59", + "frontend-modern/src/features/docker/DockerConfigsTable.tsx": "05b1c2ad08ee3ae7b18881a932923fe7a66c299020af1d4b323fe733d2240d2e", + "frontend-modern/src/features/docker/DockerContainersTable.tsx": "51593751c5626b671af125b4a9c0e108f66e99bcae3c2eac6d329d592c839b50", + "frontend-modern/src/features/docker/DockerHostsTable.tsx": "ff52844b66e99012e53d83308bf6fd02cf15ee1550cd136a20c2abbd262d72dd", + "frontend-modern/src/features/docker/DockerImagesTable.tsx": "cd091441e2217fd3fa6646bb7e7509dcb708214096c9ab32fb119c09c7a1636c", + "frontend-modern/src/features/docker/DockerNetworksTable.tsx": "1a9ababc04f5d1fdc3a58ec7744894bba284345a7c6ab61da3b4d7689dbdcbbc", + "frontend-modern/src/features/docker/DockerPageSurface.tsx": "01a95750034ef0b6c42deda8de7921a68b54bafcd3988099c5976b4e66ce78fd", + "frontend-modern/src/features/docker/DockerSecretsTable.tsx": "3ef4d3899e6ab7d4e02411aba2100839f6bacd90fb43efdd9084799db0002409", + "frontend-modern/src/features/docker/DockerServicesTable.tsx": "6d727875002d36e9747a43c835c13a89319e385239f686274e58595ab399bc62", + "frontend-modern/src/features/docker/DockerStorageUsageTable.tsx": "9e74a2b5709cc3a20af2242e24c3c74c68ceb341abf38d7104be04e4c5e734b7", + "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx": "597b3a676b235d1d5354db64bbe1697ea5ac686c72a2ec7d6529e2e5dc053f0c", + "frontend-modern/src/features/docker/DockerTasksTable.tsx": "91ace55b7b2b0dabb83fecac2d65b9423242a9d2d9dec7c0121462a890a986b5", + "frontend-modern/src/features/docker/DockerVolumesTable.tsx": "1edfa81bbe2d183bc2c0e8088fa484a648c2299b390fba99767a8d469a9ea30a", + "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx": "d6d6f90dcd4a8e2750249bcc58d4d8b20a0ec90479e4a015a7e2e57e0e118f9b", + "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx": "d96477efd6b3d42ccc18b675ca40c73de32261b0c1a08ac21475fb4b9425e810", + "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx": "bbfc48536109d2758cb6ddd774c34a4c97182f1b1ec78963db192ce53cd23e46", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "302c2d1d472dbd5525c900fec3915b73e8dc03a1172a5bdacad23d4d169b0b6d", + "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "a582c21961c42a3b06ce783e62beac08acb6057a7abdc45d1a7a8de1e9da9813", + "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx": "25d03d803ccd3b22a183992c17dbcb48899e474f8d7416fb51f40d462f149491", + "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx": "26ea9234a477e1e496482c25b6d71c78cf88d4f542267300b1940ddfb0919578", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx": "525288625d6c0ca2a17cf19b1768fa96d9dc7488c9c3bfc879216d3e7005477d", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "1bcd857e3d7f178f3385f19ac6b10fb0f99c8a016c20b855c13beee4bbb9ce56", + "frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx": "9c9866f7fab9276e671cc9b5eaec6b559ba51fa8a76194558f9c6f1ca6779ac3", + "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx": "bd7e43a79321c78b792eac4577d6460f306fdfef883073840c4f18bcec99d880", + "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "8cd19ccb1cbc0aa26c001fc4337be137e6b82dfe28a49e12b8832cf4f6fded3c", + "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "055a440d609bf469c7b5d45bf12c419f133393eb79598902c4059f86bd9979ee", + "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "1b3de34f73440adcdcfa4fb78fd6b7bb6987b0409d96ddbe18ae23c12d863a15", + "frontend-modern/src/features/platformPage/platformSearchSuggestions.ts": "f9b29fa75e0bf001b43c3da6163209cdb61f1a6603c68b1a3d67dd5c747d2ea3", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "9ee68caab9eaf7204b4317ecaba505450e237b90c71d2f77c9c181164277c046", + "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx": "ef78efa893e62a7c9470bea2fae96f41d8204d1514105d5c60abaa45b37625cb", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx": "9843ecae3662aac07c2e2e2004cec333b66c2b4eb53f251b8c04bd9e712d405a", + "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "0d549da64da0d7887a6b118df9b863110fbd078a36960c16868f427c127bc51a", + "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx": "1e0706c4c672ac1e8353fa0b8d052a6a8ccd1285d7b7583ffd6472ff99a8d344", + "frontend-modern/src/features/proxmox/proxmoxPageModel.ts": "be7f59a030f38a935c79a7f1039776bc4d37023fd0ee800ed0a50f161a7b7cde", + "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx": "fa095fc470308986d56171874d027b9fbdffec9cdf373197b714c510f7b13da5", + "frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx": "d3bde05cd00db0fa38f14198804ddbdf811d0dbc3d3e8d5dc080381f9b0f0838", + "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx": "b92b8b1ab41b714c17dacf2f3349f20dc6bac374f72b9d1c342a5896ddf03c40", + "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx": "d9f93ad4786c87b9a6fead883c44a256f268982db1459b65f00ee6473cf131ab", + "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx": "c8d98f26b2fdd057e5c99248d72b077effb2f0e9ec623bf337cd7f12a370297f", + "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx": "b1a43478f05d35ee4cf34c36d0107b6b3c9de7e4f04ef840c436558bd680ba85", + "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx": "cef42e9d146bd3bb3fe7fbcaf4f59db6ec7ff6da01832bd6a677c080610e27d4", + "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "fcd0b97fda7083b2d09276781b246a226fe3290875916923fa000503003408b9", + "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx": "af5d52d7468768d81a3f85cf19d4fe5d5b537ab1e0fa5703d6a2543c7cfd4669", + "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx": "88f0bc8d7e7c3c47932282e68fbf477b12fd060ebff6b5a8ed7d461b5928c1bb", + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx": "5b82730dd147401f9d44fef0125210e863a2f895ac238d1d45d94dc38fae722a", + "frontend-modern/src/features/vmware/VsphereActivityTable.tsx": "570c73ff43fa1478ddabc3708c38379ce526eb0476c4025fd92d152d626c6094", + "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx": "888d0dd01d5b10dcea6a061949b33e80e29e89a5554e2fdda4fc14925b3b0584", + "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx": "d1dc4a1ab8888cf96d7d02d48c478c63c948fe4d618305191859821df333b092", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx": "01d51d3a0b24f42fd1439f9531a932f7c58e2e9603e38260197ac64931ec2f4e", + "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx": "0cdf8b6965d42d13064773d9ffcf71edae78403ffe9e91e2418a5e390ee45339", + "frontend-modern/src/utils/searchQuery.ts": "37a6ec34da3f0ca2812b7c1787dcb4dcc5ebba6216300e79cddaa88d285956f0" }, "routes": [ - "/settings/system-general", - "/settings/infrastructure?add=docker", - "/settings/infrastructure?add=pve" + "/proxmox/overview", + "/docker/overview", + "/kubernetes/overview" ], "viewports": [ { @@ -40,23 +150,30 @@ "height": 720 }, { - "width": 375, - "height": 812 + "width": 390, + "height": 844 } ], "states": [ - "General panel: Docker in Proxmox LXCs card rendered below the Docker/Podman updates card with toggle ON reflecting the persisted server value", - "General panel: toggle OFF state after disabling (aria-pressed=false), backend value false, monitor checker and collector torn down per server log", - "General panel: toggle restored ON, backend value true persisted to system.json and surviving a server restart", - "Infrastructure installer (add=docker): Docker-inside-Proxmox-LXCs callout points at the Settings toggle first with the env variable as the locking override", - "PVE node setup guide (Host Telemetry Agent): opt-in callout names the toggle, the Settings path, and both environment variables", - "Mobile viewport: card renders with no horizontal overflow, env hint wraps with break-all" + "Proxmox overview with no search: three real nodes and the complete workload inventory", + "Proxmox overview with wireguard-edge-01 and reporting-api-01 committed: two removable pills and two matching workloads while all three real nodes remained visible", + "Docker overview with vaultwarden committed: one removable pill and the matching container", + "Kubernetes overview with chec committed: abbreviated pill retained every matching checkout object", + "Kubernetes overview with checkout- committed: both checkout API and checkout web pods remained visible", + "Kubernetes overview with checkout-api-6c746d5bcf-c7z2p committed: only the exact pod remained", + "Kubernetes overview with unmatched phrase: free text remained in the field and no pill was created", + "Narrow Proxmox and Kubernetes layouts at 390x844: search remained usable, filter count stayed visible, tables stayed contained, and the independent node inventory or exact matching pod remained visible" ], "interactions": [ - "Clicked the Discover Docker in LXC guests toggle off: settings update POST returned 200, GET showed false, server logged \"Docker detection disabled\"/\"Docker inventory collection disabled\" and \"opt-in changed via settings\" enabled=false", - "Clicked the toggle back on: GET showed true, server logged \"Docker inventory collection enabled\" and the collector reconfigure line", - "Restarted the backend with the opt-in persisted true: GET after boot returned true (persistence across restart)", - "Selected the Host Telemetry Agent source in the PVE setup guide to render the Docker-in-LXC callout", - "Scrolled the General panel to the new card at desktop and mobile widths" + "Typed wire on Proxmox and verified one dim inline guard-edge-01 suffix with no suggestion listbox", + "Pressed Tab then Enter to commit wireguard-edge-01 as a removable search pill", + "Typed report, pressed Right Arrow then Enter, and committed reporting-api-01 as a second inclusive pill", + "Verified the Proxmox workload terms filtered only the workload table: West Production A, B, and C all remained in the node summary and No Proxmox VE nodes never rendered", + "Typed vault on Docker, pressed Tab then Enter, and verified the canonical completion and pill behavior outside Proxmox", + "Typed chec on Kubernetes and verified the ambiguous suffix stopped at the shared checkout- prefix; pressed Enter directly to commit chec while all matching checkout objects remained", + "Typed chec again, pressed Right Arrow then Enter, and verified checkout- committed without arbitrarily selecting one object", + "Typed checkout-api-6c746d5bcf-c, pressed Right Arrow then Enter, and verified the unique checkout-api-6c746d5bcf-c7z2p pod committed and filtered exactly", + "Typed unmatched phrase and pressed Enter; verified it remained ordinary free text and did not become a pill", + "Expanded the compact mobile Filters control and verified committed pills and Clear filters remained operable" ] } diff --git a/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx b/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx index d1fb3842a..15139874b 100644 --- a/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx @@ -38,6 +38,7 @@ import type { WorkloadsMemoryDisplayBasis, WorkloadsStatusMode, } from './workloadsFilterModel'; +import { buildWorkloadSearchSuggestions } from './workloadSearchSuggestions'; import { DEFAULT_WORKLOADS_SORT_DIRECTION, DEFAULT_WORKLOADS_SORT_KEY, @@ -288,6 +289,10 @@ export const WorkloadsFilter: Component = (props) => { return filters; }; + const searchSuggestions = createMemo(() => + buildWorkloadSearchSuggestions(props.searchSuggestionWorkloads?.() ?? []), + ); + return ( = (props) => { placeholder: props.searchPlaceholder ?? 'Search workloads by name, ID, node, or image', historyKey: STORAGE_KEYS.WORKLOADS_SEARCH_HISTORY, emptyMessage: props.searchEmptyMessage ?? 'Recent workload searches appear here.', + suggestions: searchSuggestions, onBeforeAutoFocus: props.onBeforeAutoFocus, }} searchTrailing={props.searchTrailing} diff --git a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx index c9b84e488..2bcfe250b 100644 --- a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx @@ -134,6 +134,7 @@ export function WorkloadsSurface(props: WorkloadsSurfaceComponentProps) { ariaLabel={props.filterAriaLabel} searchPlaceholder={props.filterSearchPlaceholder} searchEmptyMessage={props.filterSearchEmptyMessage} + searchSuggestionWorkloads={state.allGuests} statusOptions={props.filterStatusOptions} columnVisibility={state.workloadsFilterColumnVisibility()} containerRuntimeFilter={state.containerRuntimeFilterConfig()} diff --git a/frontend-modern/src/components/Workloads/__tests__/workloadSearchSuggestions.test.ts b/frontend-modern/src/components/Workloads/__tests__/workloadSearchSuggestions.test.ts new file mode 100644 index 000000000..fd609de8f --- /dev/null +++ b/frontend-modern/src/components/Workloads/__tests__/workloadSearchSuggestions.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import type { WorkloadGuest } from '@/types/workloads'; +import { buildWorkloadSearchSuggestions } from '../workloadSearchSuggestions'; + +describe('buildWorkloadSearchSuggestions', () => { + it('projects workload names, ids, hosts, types, and status for every platform workload', () => { + const workload = { + id: 'docker:host-a:container:abc123', + name: 'grafana-agent-01', + displayId: 'abc123', + type: 'container', + status: 'running', + dockerHostName: 'host-a', + technology: 'docker', + } as unknown as WorkloadGuest; + + const suggestions = buildWorkloadSearchSuggestions([workload]); + const suggestion = suggestions.find((item) => item.id.startsWith('workload:'))!; + + expect(suggestion.label).toBe('grafana-agent-01'); + expect(suggestion.description).toContain('host-a'); + expect(suggestion.description).toContain('Running'); + expect(suggestion.keywords).toEqual( + expect.arrayContaining(['grafana-agent-01', 'abc123', 'host-a']), + ); + expect(suggestions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'workload-scope:host:host-a', value: 'host-a' }), + ]), + ); + }); +}); diff --git a/frontend-modern/src/components/Workloads/workloadSearchSuggestions.ts b/frontend-modern/src/components/Workloads/workloadSearchSuggestions.ts new file mode 100644 index 000000000..fd5edec45 --- /dev/null +++ b/frontend-modern/src/components/Workloads/workloadSearchSuggestions.ts @@ -0,0 +1,94 @@ +import type { SearchInputSuggestion } from '@/components/shared/SearchInput'; +import type { WorkloadGuest } from '@/types/workloads'; +import { titleCaseDelimitedLabel } from '@/utils/textPresentation'; +import { getWorkloadTypePresentation } from '@/utils/workloadTypePresentation'; +import { getCanonicalWorkloadId, resolveWorkloadType } from '@/utils/workloads'; +import { getWorkloadSearchCandidates } from './workloadSelectors'; + +const clean = (value: unknown): string => + typeof value === 'string' || typeof value === 'number' ? String(value).trim() : ''; + +const workloadDisplayId = (guest: WorkloadGuest): string => + clean(guest.displayId) || + (Number.isFinite(guest.vmid) && guest.vmid > 0 ? String(guest.vmid) : ''); + +const workloadLabel = (guest: WorkloadGuest): string => + clean(guest.name) || workloadDisplayId(guest) || clean(guest.id) || 'Unnamed workload'; + +const workloadDescription = (guest: WorkloadGuest): string => { + const type = getWorkloadTypePresentation(resolveWorkloadType(guest)).label; + const displayId = workloadDisplayId(guest); + const identity = displayId ? `${type} ${displayId}` : type; + const node = clean(guest.node) || clean(guest.dockerHostName) || clean(guest.contextLabel); + const status = titleCaseDelimitedLabel(clean(guest.status)); + return [identity, node, status].filter(Boolean).join(' · '); +}; + +export const buildWorkloadSearchSuggestions = ( + workloads: readonly WorkloadGuest[], +): SearchInputSuggestion[] => { + const nameCounts = new Map(); + for (const workload of workloads) { + const name = workloadLabel(workload).toLowerCase(); + nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1); + } + + const seen = new Set(); + const suggestions: SearchInputSuggestion[] = []; + const scopeFields: Array<{ + key: 'node' | 'host' | 'context' | 'cluster' | 'namespace'; + label: string; + value: (workload: WorkloadGuest) => string; + }> = [ + { key: 'node', label: 'Node', value: (workload) => clean(workload.node) }, + { key: 'host', label: 'Host', value: (workload) => clean(workload.dockerHostName) }, + { key: 'context', label: 'Context', value: (workload) => clean(workload.contextLabel) }, + { key: 'cluster', label: 'Cluster', value: (workload) => clean(workload.clusterName) }, + { key: 'namespace', label: 'Namespace', value: (workload) => clean(workload.namespace) }, + ]; + + for (const field of scopeFields) { + const values = new Set(workloads.map(field.value).filter(Boolean)); + for (const value of values) { + suggestions.push({ + id: `workload-scope:${field.key}:${value}`, + label: value, + value, + description: field.label, + group: 'Infrastructure', + keywords: [field.label, value], + }); + } + } + + for (const workload of workloads) { + const canonicalId = getCanonicalWorkloadId(workload) || workload.id; + if (!canonicalId || seen.has(canonicalId)) continue; + seen.add(canonicalId); + + const label = workloadLabel(workload); + const duplicateName = (nameCounts.get(label.toLowerCase()) ?? 0) > 1; + suggestions.push({ + id: `workload:${canonicalId}`, + label, + value: duplicateName ? canonicalId : label, + description: workloadDescription(workload), + group: 'Infrastructure', + keywords: getWorkloadSearchCandidates(workload).map(clean).filter(Boolean), + completions: [ + workload.name, + workload.displayId, + workload.vmid, + workload.id, + workload.node, + workload.dockerHostName, + workload.contextLabel, + workload.clusterName, + ] + .map(clean) + .filter(Boolean), + }); + } + + return suggestions; +}; diff --git a/frontend-modern/src/components/Workloads/workloadSelectors.ts b/frontend-modern/src/components/Workloads/workloadSelectors.ts index deb5b69da..482dd6976 100644 --- a/frontend-modern/src/components/Workloads/workloadSelectors.ts +++ b/frontend-modern/src/components/Workloads/workloadSelectors.ts @@ -86,7 +86,9 @@ type SortDirection = 'asc' | 'desc'; type SortValue = string | number | boolean | null | undefined; -const workloadSearchCandidates = (guest: WorkloadGuest): Array => [ +export const getWorkloadSearchCandidates = ( + guest: WorkloadGuest, +): Array => [ guest.name, guest.id, guest.displayId, @@ -104,13 +106,14 @@ const workloadSearchCandidates = (guest: WorkloadGuest): Array { const needle = term.trim().toLowerCase(); if (!needle) return true; - return workloadSearchCandidates(guest) + return getWorkloadSearchCandidates(guest) .filter( (value): value is string | number => typeof value === 'string' || typeof value === 'number', ) diff --git a/frontend-modern/src/components/Workloads/workloadsFilterModel.ts b/frontend-modern/src/components/Workloads/workloadsFilterModel.ts index 3d356db8d..aceb1e8f7 100644 --- a/frontend-modern/src/components/Workloads/workloadsFilterModel.ts +++ b/frontend-modern/src/components/Workloads/workloadsFilterModel.ts @@ -55,6 +55,7 @@ export interface WorkloadsFilterProps { ariaLabel?: string; searchPlaceholder?: string; searchEmptyMessage?: string; + searchSuggestionWorkloads?: () => readonly WorkloadGuest[]; statusOptions?: readonly WorkloadsStatusOption[]; inventoryStats?: () => WorkloadsInventoryStats; inventoryCountsVisible?: () => boolean; diff --git a/frontend-modern/src/components/shared/FilterBar/FilterBar.test.tsx b/frontend-modern/src/components/shared/FilterBar/FilterBar.test.tsx index 5ccd2af6b..229f3b54c 100644 --- a/frontend-modern/src/components/shared/FilterBar/FilterBar.test.tsx +++ b/frontend-modern/src/components/shared/FilterBar/FilterBar.test.tsx @@ -2,6 +2,7 @@ import { cleanup, fireEvent, render, screen, within } from '@solidjs/testing-lib import { afterEach, describe, expect, it, vi } from 'vitest'; import { FilterBar } from './FilterBar'; import type { FilterDef } from './filterCatalog'; +import { createSignal } from 'solid-js'; const search = { value: () => '', @@ -302,4 +303,133 @@ describe('FilterBar', () => { expect(label).toHaveTextContent('Filter'); expect(select.parentElement).toHaveClass('p-0.5'); }); + + it('completes a matching filter inline before pinning it as a chip on Enter', () => { + const Harness = () => { + const [query, setQuery] = createSignal(''); + const [node, setNode] = createSignal(''); + const nodeFilter: FilterDef = { + id: 'node', + label: 'Node', + group: 'scope', + value: node, + setValue: setNode, + defaultValue: '', + options: () => [ + { value: '', label: 'All nodes' }, + { value: 'pve1', label: 'pve1' }, + ], + }; + return ( + false} + /> + ); + }; + + const { container } = render(() => ); + const input = screen.getByPlaceholderText('Search infrastructure'); + input.focus(); + fireEvent.input(input, { target: { value: 'pv' } }); + expect(container.querySelector('[data-search-completion-suffix]')).toHaveTextContent('e1'); + + fireEvent.keyDown(input, { key: 'Tab' }); + expect(input).toHaveValue('pve1'); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(screen.getByRole('button', { name: 'Remove Node filter' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Node: pve1' })).toBeInTheDocument(); + expect(input).toHaveValue(''); + }); + + it('commits multiple infrastructure completions as removable inclusive search pills', () => { + const Harness = () => { + const [query, setQuery] = createSignal(''); + return ( + <> + [ + { id: 'node:pve1', label: 'pve1', value: 'pve1' }, + { id: 'host:docker-a', label: 'docker-a', value: 'docker-a' }, + ], + }} + filters={[]} + isMobile={() => false} + /> + {query()} + + ); + }; + + render(() => ); + const input = screen.getByPlaceholderText('Search objects'); + input.focus(); + + fireEvent.input(input, { target: { value: 'pv' } }); + fireEvent.keyDown(input, { key: 'Tab' }); + fireEvent.keyDown(input, { key: 'Enter' }); + expect(screen.getByRole('button', { name: 'Remove search term pve1' })).toBeInTheDocument(); + + fireEvent.input(input, { target: { value: 'doc' } }); + fireEvent.keyDown(input, { key: 'Tab' }); + fireEvent.keyDown(input, { key: 'Enter' }); + expect(screen.getByRole('button', { name: 'Remove search term docker-a' })).toBeInTheDocument(); + expect(screen.getByTestId('effective-search')).toHaveTextContent('pve1, docker-a'); + + fireEvent.click(screen.getByRole('button', { name: 'Remove search term pve1' })); + expect( + screen.queryByRole('button', { name: 'Remove search term pve1' }), + ).not.toBeInTheDocument(); + expect(screen.getByTestId('effective-search')).toHaveTextContent('docker-a'); + }); + + it('commits a recognized abbreviated query without choosing one ambiguous object', () => { + const Harness = () => { + const [query, setQuery] = createSignal(''); + return ( + <> + [ + { id: 'deployment:checkout-api', label: 'checkout-api' }, + { id: 'deployment:checkout-web', label: 'checkout-web' }, + { id: 'pod:checkout-api-a', label: 'checkout-api-6c746d5bcf-c7z2p' }, + ], + }} + filters={[]} + isMobile={() => false} + /> + {query()} + + ); + }; + + const { container } = render(() => ); + const input = screen.getByPlaceholderText('Search Kubernetes objects'); + input.focus(); + fireEvent.input(input, { target: { value: 'chec' } }); + + expect(container.querySelector('[data-search-completion-suffix]')).toHaveTextContent('kout-'); + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(input).toHaveValue(''); + expect(screen.getByRole('button', { name: 'Remove search term chec' })).toBeInTheDocument(); + expect(screen.getByTestId('effective-search')).toHaveTextContent('chec'); + + fireEvent.input(input, { target: { value: 'unmatched prose' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + expect(input).toHaveValue('unmatched prose'); + expect( + screen.queryByRole('button', { name: 'Remove search term unmatched prose' }), + ).not.toBeInTheDocument(); + }); }); diff --git a/frontend-modern/src/components/shared/FilterBar/FilterBar.tsx b/frontend-modern/src/components/shared/FilterBar/FilterBar.tsx index e749dfac0..a1145522c 100644 --- a/frontend-modern/src/components/shared/FilterBar/FilterBar.tsx +++ b/frontend-modern/src/components/shared/FilterBar/FilterBar.tsx @@ -1,4 +1,4 @@ -import { Component, For, JSX, Show, createMemo, createSignal } from 'solid-js'; +import { Component, For, JSX, Show, createEffect, createMemo, createSignal } from 'solid-js'; import RotateCcwIcon from 'lucide-solid/icons/rotate-ccw'; import { Card } from '@/components/shared/Card'; import { FilterButtonGroup } from '@/components/shared/FilterButtonGroup'; @@ -8,11 +8,16 @@ import { AddFilterMenu } from './AddFilterMenu'; import { FilterChip } from './FilterChip'; import { ViewOptionsMenu } from './ViewOptionsMenu'; import { + buildFilterSearchSuggestions, clearFilter, hasAddableFilterOptions, + getSearchSuggestionValues, isFilterSet, + serializeFilterSearch, + toFilterSearchTerm, type FilterBarProps, type FilterDef, + type FilterSearchTerm, } from './filterCatalog'; const FilterBarClearAllButton: Component<{ onClick: () => void }> = (props) => ( @@ -54,8 +59,28 @@ const FilterBarRailDivider: Component = () => (