From cb3266bf33458a4d307c5473bc8b10b2bb52a2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20T=C3=B6rnberg?= Date: Tue, 22 Sep 2026 08:45:42 +0200 Subject: [PATCH] better ui --- .../components/layout/GlobalSearchDialog.tsx | 179 ++++++++++++++++++ otel-ui/src/components/layout/TopBar.tsx | 13 +- .../contexts/ObservabilityContextProvider.tsx | 1 + .../src/contexts/ObservabilityContextValue.ts | 2 + 4 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 otel-ui/src/components/layout/GlobalSearchDialog.tsx diff --git a/otel-ui/src/components/layout/GlobalSearchDialog.tsx b/otel-ui/src/components/layout/GlobalSearchDialog.tsx new file mode 100644 index 0000000..07eb303 --- /dev/null +++ b/otel-ui/src/components/layout/GlobalSearchDialog.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useMemo, useRef } from "react" +import { AlertTriangle, ArrowRight, Search, Server, X } from "lucide-react" +import { useObservabilityData } from "../../hooks/useObservabilityData" + +interface GlobalSearchDialogProps { + isOpen: boolean + onClose: () => void +} + +/** + * Command-palette style dialog opened from the top bar search box. Leads + * with matching OTel services (the common "something's wrong with X, + * let me debug it" entry point) and falls back to a plain telemetry + * search over the current signals. + */ +export const GlobalSearchDialog: React.FC = ({ isOpen, onClose }) => { + const { + searchQuery, + setSearchQuery, + uniqueServices, + serviceHealth, + setSelectedServices, + setActiveTab, + unfocusedTraces, + unfocusedLogs, + } = useObservabilityData() + + const inputRef = useRef(null) + + useEffect(() => { + if (isOpen) inputRef.current?.focus() + }, [isOpen]) + + useEffect(() => { + if (!isOpen) return + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose() + } + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [isOpen, onClose]) + + const query = searchQuery.trim().toLowerCase() + + const matchingServices = useMemo(() => { + const services = query ? uniqueServices.filter((s) => s.toLowerCase().includes(query)) : uniqueServices + return [...services] + .sort((a, b) => (serviceHealth[b]?.totalSignals ?? 0) - (serviceHealth[a]?.totalSignals ?? 0)) + .slice(0, 8) + }, [uniqueServices, serviceHealth, query]) + + const matchCounts = useMemo(() => { + if (!query) return { traces: 0, logs: 0 } + let traces = 0 + unfocusedTraces.forEach((t) => { + t.resourceSpans?.forEach((rs) => rs.scopeSpans?.forEach((ss) => (traces += ss.spans?.length ?? 0))) + }) + let logs = 0 + unfocusedLogs.forEach((l) => { + l.scopeLogs?.forEach((sl) => (logs += sl.logRecords?.length ?? 0)) + }) + return { traces, logs } + }, [query, unfocusedTraces, unfocusedLogs]) + + const debugService = (service: string) => { + setSelectedServices(new Set([service])) + setActiveTab("tab0") + onClose() + } + + const viewAllResults = () => { + setActiveTab("tab0") + onClose() + } + + if (!isOpen) return null + + return ( +
{ + if (e.target === e.currentTarget) onClose() + }} + > +
+
+
+ +
+
+ OTel Services{query ? ` matching "${searchQuery.trim()}"` : ""} +
+ + {matchingServices.length === 0 ? ( +
No services found.
+ ) : ( +
    + {matchingServices.map((service) => { + const health = serviceHealth[service] + return ( +
  • + +
  • + ) + })} +
+ )} + + {query && ( + <> +
+ Telemetry +
+ + + )} +
+
+
+ ) +} + +export default GlobalSearchDialog diff --git a/otel-ui/src/components/layout/TopBar.tsx b/otel-ui/src/components/layout/TopBar.tsx index a606662..a19ff23 100644 --- a/otel-ui/src/components/layout/TopBar.tsx +++ b/otel-ui/src/components/layout/TopBar.tsx @@ -4,6 +4,7 @@ import { useObservabilityData } from "../../hooks/useObservabilityData" import { currentUser, login, logout } from "../../lib/auth" import { getTheme, setTheme, type ThemeMode } from "../../lib/theme" import type { TimeWindow } from "../../contexts/ObservabilityContextValue" +import { GlobalSearchDialog } from "./GlobalSearchDialog" const TIME_WINDOWS: { value: TimeWindow; label: string }[] = [ { value: "15m", label: "15m" }, @@ -23,6 +24,7 @@ export const TopBar: React.FC = () => { useObservabilityData() const searchInputRef = useRef(null) const [theme, setThemeState] = useState(() => getTheme()) + const [searchDialogOpen, setSearchDialogOpen] = useState(false) const toggleTheme = () => { const next: ThemeMode = theme === "dark" ? "light" : "dark" @@ -34,7 +36,7 @@ export const TopBar: React.FC = () => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault() - searchInputRef.current?.focus() + setSearchDialogOpen(true) } } window.addEventListener("keydown", handleKeyDown) @@ -42,7 +44,8 @@ export const TopBar: React.FC = () => { }, []) return ( -
+ <> +
OhhTel @@ -76,6 +79,10 @@ export const TopBar: React.FC = () => { type="search" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} + onFocus={(e) => { + e.target.blur() + setSearchDialogOpen(true) + }} placeholder="Search trace id, service, route, user…" className="h-9 w-full rounded-[9px] border border-ds-line bg-ds-surface-2 pl-9 pr-14 font-[family-name:var(--font-data)] text-[13px] text-ds-text placeholder:text-ds-text-3 focus:outline-none focus:ring-1 focus:ring-ds-accent" /> @@ -157,6 +164,8 @@ export const TopBar: React.FC = () => { )}
+ setSearchDialogOpen(false)} /> + ) } diff --git a/otel-ui/src/contexts/ObservabilityContextProvider.tsx b/otel-ui/src/contexts/ObservabilityContextProvider.tsx index 6e6d0de..61f08f0 100644 --- a/otel-ui/src/contexts/ObservabilityContextProvider.tsx +++ b/otel-ui/src/contexts/ObservabilityContextProvider.tsx @@ -790,6 +790,7 @@ export function ObservabilityProvider({ selectedServices, toggleService, + setSelectedServices, excludedAttributes, toggleExcludedValue, removeExcludedValue, diff --git a/otel-ui/src/contexts/ObservabilityContextValue.ts b/otel-ui/src/contexts/ObservabilityContextValue.ts index b112878..f540752 100644 --- a/otel-ui/src/contexts/ObservabilityContextValue.ts +++ b/otel-ui/src/contexts/ObservabilityContextValue.ts @@ -62,6 +62,8 @@ interface ObservabilityContextType { // Global filters selectedServices: Set; toggleService: (service: string) => void; + /** Replaces the whole selection, e.g. "focus on exactly this one service". */ + setSelectedServices: (services: Set) => void; excludedAttributes: Record>; toggleExcludedValue: (key: string, value: string) => void; removeExcludedValue: (key: string, value: string) => void;