better ui
Build and Publish / Metadata (push) Successful in 11s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 1m48s

This commit is contained in:
mats committed 2026-09-22 08:45:42 +02:00
1 parent a0f558b43c
commit cb3266bf33
4 files changed
+193 -2

No files matched your search

@@ -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<GlobalSearchDialogProps> = ({ isOpen, onClose }) => {
const {
searchQuery,
setSearchQuery,
uniqueServices,
serviceHealth,
setSelectedServices,
setActiveTab,
unfocusedTraces,
unfocusedLogs,
} = useObservabilityData()
const inputRef = useRef<HTMLInputElement>(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 (
<div
className="fixed inset-0 z-[100] flex items-start justify-center bg-black/50 pt-[12vh]"
onMouseDown={(e) => {
if (e.target === e.currentTarget) onClose()
}}
>
<div className="w-full max-w-xl rounded-[14px] border border-ds-line bg-ds-surface shadow-2xl">
<div className="relative flex items-center border-b border-ds-line px-4">
<Search className="h-4 w-4 shrink-0 text-ds-text-3" aria-hidden="true" />
<input
ref={inputRef}
type="search"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") viewAllResults()
}}
placeholder="Search trace id, service, route, user…"
className="h-12 w-full bg-transparent px-3 font-[family-name:var(--font-data)] text-sm text-ds-text placeholder:text-ds-text-3 focus:outline-none"
/>
<button
type="button"
onClick={onClose}
aria-label="Close search"
className="shrink-0 rounded-[6px] p-1 text-ds-text-3 hover:bg-ds-surface-2 hover:text-ds-text"
>
<X className="h-4 w-4" aria-hidden="true" />
</button>
</div>
<div className="max-h-[60vh] overflow-y-auto p-2">
<div className="px-2 pb-1 pt-2 font-[family-name:var(--font-data)] text-[10px] font-semibold uppercase tracking-wider text-ds-text-3">
OTel Services{query ? ` matching "${searchQuery.trim()}"` : ""}
</div>
{matchingServices.length === 0 ? (
<div className="px-3 py-4 text-sm text-ds-text-3">No services found.</div>
) : (
<ul>
{matchingServices.map((service) => {
const health = serviceHealth[service]
return (
<li key={service}>
<button
type="button"
onClick={() => debugService(service)}
className="flex w-full items-center gap-3 rounded-[9px] px-3 py-2 text-left hover:bg-ds-surface-2"
>
<span
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${
health?.hasErrors ? "bg-ds-critical-bg text-ds-critical" : "bg-ds-accent-bg text-ds-accent"
}`}
>
<Server className="h-4 w-4" aria-hidden="true" />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-ds-text">{service}</span>
<span className="block truncate font-[family-name:var(--font-data)] text-[11px] text-ds-text-3">
{health ? `${health.totalSignals} signals` : "no recent signals"}
{health?.hasErrors ? ` · ${health.errorCount} errors` : ""}
</span>
</span>
{health?.hasErrors && (
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-ds-critical" aria-hidden="true" />
)}
<span className="shrink-0 font-[family-name:var(--font-data)] text-[11px] text-ds-accent">
Debug <ArrowRight className="ml-0.5 inline h-3 w-3" aria-hidden="true" />
</span>
</button>
</li>
)
})}
</ul>
)}
{query && (
<>
<div className="px-2 pb-1 pt-3 font-[family-name:var(--font-data)] text-[10px] font-semibold uppercase tracking-wider text-ds-text-3">
Telemetry
</div>
<button
type="button"
onClick={viewAllResults}
className="flex w-full items-center justify-between rounded-[9px] px-3 py-2 text-left hover:bg-ds-surface-2"
>
<span className="text-sm text-ds-text">
Search all telemetry for <span className="font-semibold">"{searchQuery.trim()}"</span>
</span>
<span className="shrink-0 font-[family-name:var(--font-data)] text-[11px] text-ds-text-3">
{matchCounts.traces} spans · {matchCounts.logs} logs
</span>
</button>
</>
)}
</div>
</div>
</div>
)
}
export default GlobalSearchDialog
+11 -2
View File
@@ -4,6 +4,7 @@ import { useObservabilityData } from "../../hooks/useObservabilityData"
import { currentUser, login, logout } from "../../lib/auth" import { currentUser, login, logout } from "../../lib/auth"
import { getTheme, setTheme, type ThemeMode } from "../../lib/theme" import { getTheme, setTheme, type ThemeMode } from "../../lib/theme"
import type { TimeWindow } from "../../contexts/ObservabilityContextValue" import type { TimeWindow } from "../../contexts/ObservabilityContextValue"
import { GlobalSearchDialog } from "./GlobalSearchDialog"
const TIME_WINDOWS: { value: TimeWindow; label: string }[] = [ const TIME_WINDOWS: { value: TimeWindow; label: string }[] = [
{ value: "15m", label: "15m" }, { value: "15m", label: "15m" },
@@ -23,6 +24,7 @@ export const TopBar: React.FC = () => {
useObservabilityData() useObservabilityData()
const searchInputRef = useRef<HTMLInputElement>(null) const searchInputRef = useRef<HTMLInputElement>(null)
const [theme, setThemeState] = useState<ThemeMode>(() => getTheme()) const [theme, setThemeState] = useState<ThemeMode>(() => getTheme())
const [searchDialogOpen, setSearchDialogOpen] = useState(false)
const toggleTheme = () => { const toggleTheme = () => {
const next: ThemeMode = theme === "dark" ? "light" : "dark" const next: ThemeMode = theme === "dark" ? "light" : "dark"
@@ -34,7 +36,7 @@ export const TopBar: React.FC = () => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") { if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault() e.preventDefault()
searchInputRef.current?.focus() setSearchDialogOpen(true)
} }
} }
window.addEventListener("keydown", handleKeyDown) window.addEventListener("keydown", handleKeyDown)
@@ -42,7 +44,8 @@ export const TopBar: React.FC = () => {
}, []) }, [])
return ( return (
<header className="fixed left-[72px] right-0 top-0 z-40 flex h-[58px] items-center gap-4 border-b border-ds-line bg-ds-chrome px-6 font-[family-name:var(--font-ui)]"> <>
<header className="fixed left-[72px] right-0 top-0 z-40 flex h-[58px] items-center gap-4 border-b border-ds-line bg-ds-chrome px-6 font-[family-name:var(--font-ui)]">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<span className="font-[family-name:var(--font-display)] text-base font-bold tracking-[-0.01em] text-ds-text"> <span className="font-[family-name:var(--font-display)] text-base font-bold tracking-[-0.01em] text-ds-text">
OhhTel OhhTel
@@ -76,6 +79,10 @@ export const TopBar: React.FC = () => {
type="search" type="search"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
onFocus={(e) => {
e.target.blur()
setSearchDialogOpen(true)
}}
placeholder="Search trace id, service, route, user…" 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" 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 = () => {
</button> </button>
)} )}
</header> </header>
<GlobalSearchDialog isOpen={searchDialogOpen} onClose={() => setSearchDialogOpen(false)} />
</>
) )
} }
@@ -790,6 +790,7 @@ export function ObservabilityProvider({
selectedServices, selectedServices,
toggleService, toggleService,
setSelectedServices,
excludedAttributes, excludedAttributes,
toggleExcludedValue, toggleExcludedValue,
removeExcludedValue, removeExcludedValue,
@@ -62,6 +62,8 @@ interface ObservabilityContextType {
// Global filters // Global filters
selectedServices: Set<string>; selectedServices: Set<string>;
toggleService: (service: string) => void; toggleService: (service: string) => void;
/** Replaces the whole selection, e.g. "focus on exactly this one service". */
setSelectedServices: (services: Set<string>) => void;
excludedAttributes: Record<string, Set<string>>; excludedAttributes: Record<string, Set<string>>;
toggleExcludedValue: (key: string, value: string) => void; toggleExcludedValue: (key: string, value: string) => void;
removeExcludedValue: (key: string, value: string) => void; removeExcludedValue: (key: string, value: string) => void;