feat(search): tenant-scoped search across desktop and mobile#1249
feat(search): tenant-scoped search across desktop and mobile#1249yahyafakhroji wants to merge 7 commits into
Conversation
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
Adds the data layer for cloud-portal's tenant-scoped search feature (milo-os/search#92). Follows the standard cloud-portal resource module pattern at app/resources/search/: - schema.ts — Zod domain types (SearchHit, SearchResult, SearchTarget) - adapter.ts — K8s wire ⇄ domain (the only file in the module that imports the generated SDK; central translation seam) - service.ts — service factory with explicit Content-Type: application/json header (the search apiserver's OpenAPI spec doesn't declare a request-body content type, so the SDK falls back to '*/*' which K8s rejects with 415; the explicit header bypasses that) - queries.ts — TanStack useSearchInProject hook - constants.ts — PROJECT_KINDS (verified against deployed ResourceIndexPolicy: Domain, DNSZone, HTTPProxy, ExportPolicy) with documented disabled list for kinds awaiting policy - recents.ts — localStorage helpers under the datum-cloud-search-* namespace (dash-separated to match cookie-naming convention) - active-project.ts — last-visited project storage (powers the GCP-style cross-route footer when useApp().project clears off- project routes) - telemetry.ts — typed emitter for search.{opened,queried,selected, dismissed,partial_permission,error}; payloads carry queryLength only — raw query text never leaves the client Brings the regenerated search SDK gen files (schemas, sdk, types) onto the branch since the adapter imports types from them. All public exports surfaced via index.ts barrel; internals (SDK types, service factory) deliberately omitted.
Single source of truth for ALL search behaviour. Surfaces (cmd-K
palette, project bar, mobile sheet) become thin presentational
shells that consume this hook.
useSearchEngine owns:
- query state + 250ms debounce
- project-scoped dispatch via useSearchInProject (no global path —
v3 collapsed the scope union to projectId: string | null per team
feedback)
- result grouping by kind, capped at 5 per group
- client-side tenant filter as defense-in-depth (drops cross-tenant
hits while the server-side parent-context middleware is still
being wired — when it lands this becomes a no-op)
- reactive recents (state-backed useState + useEffect — clearing
recents updates the UI immediately, no useMemo staleness)
- auto-save recent queries on every successful search with hits
(not only on selectHit), so users can re-run queries they tried
but didn't click through on
- selectHit clears setQuery('') before onOpenChange(false) so React
batches the state changes and the popover/sheet closes cleanly
- single emission site for all 6 telemetry events; no surface ever
emits directly
- ARIA combobox/listbox wiring + live-region status text
useActiveProject resolves the active project from useApp().project
(route-driven) first, falls back to localStorage if the user
navigated to /account or /org/* where useApp clears project to
undefined. Side-effect mirrors any route project back to storage so
the last-visited fallback stays fresh.
Visual building blocks consumed by every search surface (CmdKPalette,
ProjectSearchBar, MobileSearchSheet).
- KindIcon — JSX-returning component (not a function returning a
component) so call sites stay createElement-free. Icons match the
project sidebar nav: GaugeIcon for HTTPProxy, ChartSplineIcon for
ExportPolicy, LayersIcon for Domain, SignpostIcon for DNSZone.
All lucide icons are wrapped via @datum-cloud/datum-ui/icons Icon
for consistent stroke weight with the rest of the chrome.
- kindDisplayName — kind → human label map. Labels mirror the
sidebar nav strings exactly: 'AI Edge' for HTTPProxy, 'Metrics'
for ExportPolicy, 'DNS' for DNSZone (matches the nav, shorter than
'DNS Zones').
- kindToHref — kind → detail-page URL via paths.config; returns null
for non-navigable kinds (filtered out by SearchResultItem).
- SearchResultItem — single hit row. Renders displayName (e.g.
'hiyahya.dev' from spec.domainName) as primary text and the K8s
resource name as a secondary line when it differs. Optional
showIcon prop (default true) lets typed-results lists hide the
per-row icon when the group header already carries it; mixed-kind
lists like Recently Opened keep the row icons since the group
has no implicit kind context.
- SearchResultList — typed-results renderer; CommandGroup heading
carries the KindIcon + label, rows below are text-only.
- SearchEmptyState — recents queries (re-runnable strings) plus
recently-opened hits (mixed-kind, full SearchResultItem rows)
with a Clear All action.
- SearchPartialPermissionNote — role='status' (not alert) banner
for per-kind SAR denial reporting.
- SearchScopeFooter — sticky 'Searching in {project}' / 'Showing
resource results for {project} only.' depending on hasResults.
Reinforces the scope guarantee the moment results appear.
Three project-scoped surface components consuming useSearchEngine, plus the responsive picker and layout integration. - CmdKPalette — desktop palette. Opens via ⌘/ + Ctrl+/ hotkey (the hotkey always sets open=true rather than toggling, to avoid desync between React state and Radix's internal dialog state). Disabled state when no active project shows 'Open a project to search.' instead of silently doing nothing. - ProjectSearchBar — always-visible inline input in the project layout header. Uses PopoverAnchor (not PopoverTrigger) to keep the input's onFocus as the sole open-driver, avoiding the click-to-toggle race that caused the original flicker. The trailing clear X button is anchored inside PopoverAnchor so Radix's onInteractOutside guard recognises clicks on it. - MobileSearchSheet — full-screen Sheet on mobile + tablet. The surface intercepts selection (handleSelect) to call onOpenChange (false) BEFORE delegating to engine.selectHit, committing the close synchronously with the tap and avoiding the cross-event flicker between cmdk's onSelect and react-router's navigation. - SearchEntry — responsive picker. Renders CmdKPalette on desktop (≥1024px); mobile + tablet (<1024px) get a search-icon button in the header that opens the MobileSearchSheet. The previous tablet-shows-inline approach stacked badly against the project switcher in the 768–1023px range — moving tablet to the mobile pattern eliminated the cramp. - Header / layout integration: header.tsx mounts SearchEntry; dashboard.layout.tsx threads a headerContent prop through to the project detail layout, which slots in ProjectSearchBar. - ProjectSearchBar uses 'hidden lg:flex' (desktop-only inline affordance); below lg, users get the MobileSearchSheet via the global SearchEntry icon-button. - Input style is h-9 + white background + text-icon-quaternary search icon (matches the table-search convention) so it feels visually consistent with the rest of the chrome. - Dead app/components/header/search-bar.tsx removed; its re-export from header/index.ts dropped.
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
Desktop search is now exclusively the always-visible ProjectSearchBar on project routes. The hotkey changes: - ⌘/ + Ctrl+/ → ⌘K + Ctrl+K (modern convention; Linear, GitHub, Notion, Cursor all use ⌘K) - Behavior: focus the existing inline input instead of opening a separate dialog - Scope: desktop only — useEffect guards on breakpoint === 'desktop'. Mobile + tablet retain the icon-button + MobileSearchSheet flow via SearchEntry (unchanged) Why this shape: - The previous CmdKPalette dialog duplicated all of ProjectSearchBar's UI (popover, results, recents, partial-permission banner). Routing the hotkey at the inline input removes ~135 lines of UI duplication while preserving every feature. - Browser conflict: Chrome + Firefox use Ctrl+K (Cmd+K on Mac) for the URL bar's search-bar focus. preventDefault overrides it. - The listener skips if the event target IS the input — pressing Cmd+K while typing in our input shouldn't double-fire. Removed: - app/features/search/surfaces/CmdKPalette.tsx (entire file) - 'cmd-k' variant from SearchSurface telemetry union (test fixtures rebased to 'project-bar') Tradeoff: the hotkey only works on /project/:id/* routes on desktop (where ProjectSearchBar is mounted). Off-project pages have no hotkey — acceptable, matches the v3 'search is project-scoped' model.
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
Adds a <kbd> inside ProjectSearchBar (desktop only) showing the focus
shortcut. macOS renders the ⌘ glyph + K; Windows/Linux render 'Ctrl K'.
Detection is driven by useOs, which is enhanced to prefer
navigator.userAgentData.platform (the future-safe Chromium signal as
the UA string is progressively frozen) before falling back to the
userAgent regex.
ConnectorDownloadCard's local detectBrowserOs helper is dropped in
favor of the same useOs hook — the mobile-exclusion (no connector
binary for iOS/Android) stays at the call site since it's a domain
rule, not an OS-detection rule.
UI notes:
- kbd render is gated on os !== 'undetermined' so SSR + first paint
don't flash the wrong glyph.
- kbd shows only when the input is empty. When there's a query the
clear-X button takes that slot (mutually exclusive).
- Input right padding swaps pr-7 ↔ pr-14 to keep the icon and kbd
from overlapping the placeholder.
- aria-keyshortcuts ('Meta+K' on Mac, 'Control+K' elsewhere) is set
on the input so VoiceOver / Narrator announce the shortcut, while
the visual kbd stays aria-hidden.
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
|
@yahyafakhroji could we change it so it's integrated like in this design please? The dropdown is ok, just the actual bar. https://www.figma.com/design/bBEQ8YeTP4SngNl5EkkQdH/Datum---Master-Design-File?node-id=9515-10643&t=ghDDZQLE87D6E8LN-4 |
…tWithAddons Search is project-scoped, so it no longer belongs in the global header chrome. Both surfaces now mount in the project-detail layout's headerContent, with the breakpoint decision lifted up from the components to the layout: - Desktop → ProjectSearchBar (inline input) - Mobile + tablet → SearchEntry (icon button → MobileSearchSheet) SearchEntry and the global header no longer self-guard on breakpoint; the header drops its SearchEntry mount entirely. ProjectSearchBar is rebuilt on InputWithAddons — the search icon moves to the leading slot and the clear button to the trailing slot, instead of absolutely positioning both over a bare Input. Placeholder shortened to 'Search'.
|
@mattdjenkinson Updated! |
🧪 Test Summary
📎 ArtifactsNo artifacts (all tests passed). |
Summary
Wires cloud-portal to the tenant-scoped search API from milo-os/search#92. All search is project-scoped (GCP-style). Two surfaces share one behavior engine:
⌘K/Ctrl Kfocuses the input.Every surface shows a footer "Searching in [project name]" so the user always knows what they're searching inside. When there's no active project context, the search opens in a disabled state with "Open a project to search."
Supported project resources
The search currently covers these project-scoped kinds (defined in
PROJECT_KINDSatapp/resources/search/search.constants.ts):Domainnetworking.datumapis.comHTTPProxynetworking.datumapis.comDNSZonedns.networking.miloapis.comExportPolicytelemetry.miloapis.comEach kind requires a matching
ResourceIndexPolicydeployed in the cluster to be indexable.Scope resolution
The active project comes from one of:
What changed
Preview
Screen.Recording.2026-05-21.at.17.02.57.mov
References