better ui
This commit is contained in:
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
|
||||
@@ -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<HTMLInputElement>(null)
|
||||
const [theme, setThemeState] = useState<ThemeMode>(() => 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 (
|
||||
<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">
|
||||
<span className="font-[family-name:var(--font-display)] text-base font-bold tracking-[-0.01em] text-ds-text">
|
||||
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 = () => {
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
<GlobalSearchDialog isOpen={searchDialogOpen} onClose={() => setSearchDialogOpen(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user