131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
import React from "react"
|
|
import {
|
|
Activity,
|
|
AlertOctagon,
|
|
CloudCog,
|
|
Fingerprint,
|
|
GitBranch,
|
|
Globe,
|
|
LineChart,
|
|
Radar,
|
|
ScrollText,
|
|
TrendingUp,
|
|
Waypoints,
|
|
Wrench,
|
|
} from "lucide-react"
|
|
import type { LucideIcon } from "lucide-react"
|
|
import { useObservabilityData } from "../../hooks/useObservabilityData"
|
|
import { useIssues } from "../../hooks/useIssues"
|
|
import { useStatsAvailability } from "../../hooks/useStats"
|
|
|
|
interface RailItem {
|
|
tab: string
|
|
label: string
|
|
Icon: LucideIcon
|
|
dotTone?: "critical" | "warn"
|
|
dotVisible?: boolean
|
|
}
|
|
|
|
/**
|
|
* The 72px fixed icon rail: primary navigation for every screen. Icon-only
|
|
* with a real aria-label per control (spec: "72px icon rail, fixed. 46x46
|
|
* targets, aria-label on every one"). A small status dot only appears when
|
|
* that view has something real to flag (open issues, error logs) - it is
|
|
* never decorative.
|
|
*/
|
|
export const IconRail: React.FC = () => {
|
|
const { activeTab, setActiveTab, logs } = useObservabilityData()
|
|
const { issues } = useIssues()
|
|
const statsAvailable = useStatsAvailability()
|
|
|
|
const openIssuesCount = React.useMemo(
|
|
() => issues.filter((i) => !i.status || i.status === "open").length,
|
|
[issues],
|
|
)
|
|
|
|
const errorLogCount = React.useMemo(() => {
|
|
let errs = 0
|
|
logs.forEach((log) => {
|
|
;(log.scopeLogs ?? []).forEach((sl) => {
|
|
;(sl.logRecords ?? []).forEach((r) => {
|
|
if ((r.severityNumber ?? 0) >= 17) errs += 1
|
|
})
|
|
})
|
|
})
|
|
return errs
|
|
}, [logs])
|
|
|
|
const items: RailItem[] = [
|
|
{ tab: "tab0", label: "Triage overview", Icon: AlertOctagon, dotTone: "critical", dotVisible: openIssuesCount > 0 },
|
|
{ tab: "tab8", label: "Similarity patterns", Icon: GitBranch },
|
|
{ tab: "tab9", label: "Cluster rollouts", Icon: CloudCog },
|
|
{ tab: "tab1", label: "Traces & spans", Icon: Activity },
|
|
{ tab: "tab3", label: "Live log stream", Icon: ScrollText, dotTone: "warn", dotVisible: errorLogCount > 0 },
|
|
{ tab: "tab2", label: "Metrics & APM", Icon: LineChart },
|
|
{ tab: "tab7", label: "RUM sessions", Icon: Globe },
|
|
{ tab: "tab10", label: "Source IPs", Icon: Fingerprint },
|
|
...(statsAvailable ? [{ tab: "tab5", label: "Historical trends", Icon: TrendingUp }] : []),
|
|
{ tab: "tab6", label: "Archive & trap", Icon: Radar },
|
|
]
|
|
|
|
return (
|
|
<nav
|
|
aria-label="Primary"
|
|
className="fixed left-0 top-0 z-50 flex h-full w-[72px] flex-col items-center gap-1.5 border-r border-ds-line bg-ds-chrome py-4"
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab("tab0")}
|
|
aria-label="Ohhtel — go to triage overview"
|
|
className="mb-2 flex h-[34px] w-[34px] items-center justify-center rounded-[10px] bg-ds-accent-bg text-ds-accent"
|
|
>
|
|
<Waypoints className="h-[19px] w-[19px]" aria-hidden="true" />
|
|
</button>
|
|
|
|
{items.map((item) => {
|
|
const isActive = activeTab === item.tab
|
|
return (
|
|
<button
|
|
key={item.tab}
|
|
type="button"
|
|
onClick={() => setActiveTab(item.tab)}
|
|
aria-label={item.label}
|
|
aria-current={isActive ? "page" : undefined}
|
|
title={item.label}
|
|
className={`relative flex h-[46px] w-[46px] items-center justify-center rounded-xl transition-colors ${
|
|
isActive ? "bg-ds-surface-2 text-ds-text" : "text-ds-text-3 hover:bg-ds-surface-2 hover:text-ds-text-2"
|
|
}`}
|
|
>
|
|
<item.Icon className="h-5 w-5" aria-hidden="true" />
|
|
{item.dotVisible && (
|
|
<span
|
|
aria-hidden="true"
|
|
className={`absolute right-2 top-2 h-[7px] w-[7px] rounded-full ${
|
|
item.dotTone === "critical" ? "bg-ds-critical" : "bg-ds-warn"
|
|
}`}
|
|
/>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
|
|
<span className="flex-grow" />
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab("tab4")}
|
|
aria-label="Setup & agent API"
|
|
aria-current={activeTab === "tab4" ? "page" : undefined}
|
|
title="Setup & agent API"
|
|
className={`flex h-[46px] w-[46px] items-center justify-center rounded-xl transition-colors ${
|
|
activeTab === "tab4" ? "bg-ds-surface-2 text-ds-text" : "text-ds-text-3 hover:bg-ds-surface-2 hover:text-ds-text-2"
|
|
}`}
|
|
>
|
|
<Wrench className="h-5 w-5" aria-hidden="true" />
|
|
</button>
|
|
</nav>
|
|
)
|
|
}
|
|
|
|
export default IconRail
|