From 59293587547c039f17e98c4270571cd08111a32f Mon Sep 17 00:00:00 2001 From: Syahdan Date: Mon, 29 Dec 2025 07:33:33 +0700 Subject: [PATCH] refactor: move monitor page to index route and configure Tauri window - Move monitor page content from /monitor to / (index route) - Create placeholder monitor_.tsx route - Remove Header component from root layout - Configure Tauri window with transparent titlebar and dark background - Enable macOS private API for window styling --- apps/web/src-tauri/Cargo.toml | 2 +- apps/web/src-tauri/tauri.conf.json | 5 +- apps/web/src/routeTree.gen.ts | 12 +- apps/web/src/routes/__root.tsx | 5 +- apps/web/src/routes/index.tsx | 332 +++++++++++++++++++++++++++-- apps/web/src/routes/monitor.tsx | 322 ---------------------------- apps/web/src/routes/monitor_.tsx | 34 +++ 7 files changed, 357 insertions(+), 355 deletions(-) delete mode 100644 apps/web/src/routes/monitor.tsx create mode 100644 apps/web/src/routes/monitor_.tsx diff --git a/apps/web/src-tauri/Cargo.toml b/apps/web/src-tauri/Cargo.toml index 944dcf6..b01e963 100644 --- a/apps/web/src-tauri/Cargo.toml +++ b/apps/web/src-tauri/Cargo.toml @@ -21,7 +21,7 @@ tauri-build = { version = "2.5.3", features = [] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" -tauri = { version = "2.9.5", features = [] } +tauri = { version = "2.9.5", features = ["macos-private-api"] } tauri-plugin-log = "2" tauri-plugin-fs = "2" tauri-plugin-dialog = "2" diff --git a/apps/web/src-tauri/tauri.conf.json b/apps/web/src-tauri/tauri.conf.json index 973fc22..3506011 100644 --- a/apps/web/src-tauri/tauri.conf.json +++ b/apps/web/src-tauri/tauri.conf.json @@ -16,9 +16,12 @@ "width": 800, "height": 600, "resizable": true, - "fullscreen": false + "fullscreen": false, + "titleBarStyle": "Transparent", + "backgroundColor": "#0a0a0a" } ], + "macOSPrivateApi": true, "security": { "csp": null } diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 194f6ce..552cc8c 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -9,11 +9,11 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' -import { Route as MonitorRouteImport } from './routes/monitor' +import { Route as MonitorRouteImport } from './routes/monitor_' import { Route as IndexRouteImport } from './routes/index' const MonitorRoute = MonitorRouteImport.update({ - id: '/monitor', + id: '/monitor_', path: '/monitor', getParentRoute: () => rootRouteImport, } as any) @@ -34,14 +34,14 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute - '/monitor': typeof MonitorRoute + '/monitor_': typeof MonitorRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: '/' | '/monitor' fileRoutesByTo: FileRoutesByTo to: '/' | '/monitor' - id: '__root__' | '/' | '/monitor' + id: '__root__' | '/' | '/monitor_' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -51,8 +51,8 @@ export interface RootRouteChildren { declare module '@tanstack/react-router' { interface FileRoutesByPath { - '/monitor': { - id: '/monitor' + '/monitor_': { + id: '/monitor_' path: '/monitor' fullPath: '/monitor' preLoaderRoute: typeof MonitorRouteImport diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 5fbbc8f..9e078c2 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -1,13 +1,12 @@ import { HeadContent, Outlet, createRootRouteWithContext } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; -import Header from "@/components/header"; import { ThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import "../index.css"; -export interface RouterAppContext {} +export interface RouterAppContext { } export const Route = createRootRouteWithContext()({ component: RootComponent, @@ -41,7 +40,7 @@ function RootComponent() { storageKey="vite-ui-theme" >
-
+ {/*
*/}
diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 4449f24..a3234f9 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -1,33 +1,321 @@ import { createFileRoute } from "@tanstack/react-router"; +import { invoke } from "@tauri-apps/api/core"; +import { open } from "@tauri-apps/plugin-dialog"; +import { readTextFile } from "@tauri-apps/plugin-fs"; +import { FileUp, Filter, Search } from "lucide-react"; +import * as React from "react"; + +import { AlertList } from "@/components/monitor/alert-list"; +import { IncidentManager } from "@/components/monitor/incident-manager"; +import { KpiDashboard } from "@/components/monitor/kpi-dashboard"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import type { Incident, KpiMetrics, ProcessedAlert } from "@/lib/types/alerts"; + +type StatusFilter = "all" | "resolved" | "unresolved"; export const Route = createFileRoute("/")({ - component: HomeComponent, + component: MonitorPage, }); -const TITLE_TEXT = ` - ██████╗ ███████╗████████╗████████╗███████╗██████╗ - ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ - ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ - ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ - ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ - ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +function MonitorPage() { + const [alerts, setAlerts] = React.useState([]); + const [incidents, setIncidents] = React.useState([]); + const [kpis, setKpis] = React.useState(null); + const [selectedAlert, setSelectedAlert] = + React.useState(null); + const [isLoading, setIsLoading] = React.useState(false); + const [error, setError] = React.useState(null); + const [searchQuery, setSearchQuery] = React.useState(""); + const [statusFilter, setStatusFilter] = React.useState("all"); - ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ - ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ - ██║ ███████╗ ██║ ███████║██║ █████╔╝ - ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ - ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗ - ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ - `; + const filteredAlerts = React.useMemo(() => { + return alerts.filter((alert) => { + if (statusFilter === "resolved" && !alert.isResolved) return false; + if (statusFilter === "unresolved" && alert.isResolved) return false; + + if (!searchQuery.trim()) return true; + + const query = searchQuery.toLowerCase(); + if (alert.alertName?.toLowerCase().includes(query)) return true; + + for (const value of Object.values(alert.labels)) { + if (value.toLowerCase().includes(query)) return true; + } + + return false; + }); + }, [alerts, searchQuery, statusFilter]); + + const selectAllState = React.useMemo(() => { + if (filteredAlerts.length === 0) return "none" as const; + const invalidCount = filteredAlerts.filter((a) => a.isInvalid).length; + if (invalidCount === 0) return "none" as const; + if (invalidCount === filteredAlerts.length) return "all" as const; + return "some" as const; + }, [filteredAlerts]); + + const recalculateKpis = React.useCallback( + async (currentAlerts: ProcessedAlert[], currentIncidents: Incident[]) => { + try { + const newKpis = await invoke("calculate_kpis_command", { + alerts: currentAlerts, + incidents: currentIncidents, + }); + setKpis(newKpis); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } + }, + [], + ); + + const handleLoadFile = async () => { + try { + setIsLoading(true); + setError(null); + + const filePath = await open({ + multiple: false, + filters: [{ name: "JSON", extensions: ["json"] }], + }); + + if (!filePath) { + setIsLoading(false); + return; + } + + const content = await readTextFile(filePath); + + const processedAlerts = await invoke( + "process_alerts_json", + { jsonContent: content }, + ); + + setAlerts(processedAlerts); + setSelectedAlert(null); + await recalculateKpis(processedAlerts, incidents); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setIsLoading(false); + } + }; + + const handleToggleInvalid = (alertId: number, isInvalid: boolean) => { + setAlerts((prev) => { + const updated = prev.map((a) => + a.id === alertId ? { ...a, isInvalid } : a, + ); + recalculateKpis(updated, incidents); + return updated; + }); + + if (selectedAlert?.id === alertId) { + setSelectedAlert((prev) => (prev ? { ...prev, isInvalid } : null)); + } + }; + + const handleToggleAllInvalid = (isInvalid: boolean) => { + const filteredIds = new Set(filteredAlerts.map((a) => a.id)); + setAlerts((prev) => { + const updated = prev.map((a) => + filteredIds.has(a.id) ? { ...a, isInvalid } : a, + ); + recalculateKpis(updated, incidents); + return updated; + }); + + if (selectedAlert && filteredIds.has(selectedAlert.id)) { + setSelectedAlert((prev) => (prev ? { ...prev, isInvalid } : null)); + } + }; + + const handleSelectAlert = (alert: ProcessedAlert) => { + setSelectedAlert(alert); + }; + + const handleAddIncident = (incident: Omit) => { + const newIncident: Incident = { + ...incident, + attachedAlertIds: [], + }; + const newIncidents = [...incidents, newIncident]; + setIncidents(newIncidents); + recalculateKpis(alerts, newIncidents); + }; + + const handleAttachAlert = (alertId: number, incidentId: string) => { + const updatedAlerts = alerts.map((a) => + a.id === alertId ? { ...a, attachedIncidentId: incidentId } : a, + ); + const updatedIncidents = incidents.map((i) => + i.id === incidentId && !i.attachedAlertIds.includes(alertId) + ? { ...i, attachedAlertIds: [...i.attachedAlertIds, alertId] } + : i, + ); + + setAlerts(updatedAlerts); + setIncidents(updatedIncidents); + recalculateKpis(updatedAlerts, updatedIncidents); + + if (selectedAlert?.id === alertId) { + setSelectedAlert((prev) => + prev ? { ...prev, attachedIncidentId: incidentId } : null, + ); + } + }; + + const handleDetachAlert = (alertId: number) => { + const alert = alerts.find((a) => a.id === alertId); + const incidentId = alert?.attachedIncidentId; + + const updatedAlerts = alerts.map((a) => + a.id === alertId ? { ...a, attachedIncidentId: null } : a, + ); + const updatedIncidents = incidentId + ? incidents.map((i) => + i.id === incidentId + ? { + ...i, + attachedAlertIds: i.attachedAlertIds.filter( + (aid) => aid !== alertId, + ), + } + : i, + ) + : incidents; + + setAlerts(updatedAlerts); + setIncidents(updatedIncidents); + recalculateKpis(updatedAlerts, updatedIncidents); + + if (selectedAlert?.id === alertId) { + setSelectedAlert((prev) => + prev ? { ...prev, attachedIncidentId: null } : null, + ); + } + }; -function HomeComponent() { return ( -
-
{TITLE_TEXT}
-
-
-

API Status

-
+
+
+

Alert Monitor

+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ +
+ +
+
+
+ + handleToggleAllInvalid(checked === true) + } + disabled={filteredAlerts.length === 0} + aria-label="Mark all filtered as invalid" + /> + + {filteredAlerts.length} + {(searchQuery || statusFilter !== "all") && ` / ${alerts.length}`} + + + + + + {statusFilter === "all" + ? "All" + : statusFilter === "resolved" + ? "Resolved" + : "Unresolved"} + + + } + /> + + setStatusFilter(v as StatusFilter)} + > + All + + Resolved + + + Unresolved + + + + +
+ + setSearchQuery(e.target.value)} + className="h-7 pl-7 text-xs" + /> +
+
+ {alerts.length === 0 ? ( +
+ + + No Alerts Loaded + + + Click "Load Alerts JSON" to import alert data from a Grafana + Alerts API export. + + +
+ ) : ( + + )} +
+ +
+ +
); diff --git a/apps/web/src/routes/monitor.tsx b/apps/web/src/routes/monitor.tsx deleted file mode 100644 index 7cb7a20..0000000 --- a/apps/web/src/routes/monitor.tsx +++ /dev/null @@ -1,322 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { invoke } from "@tauri-apps/api/core"; -import { open } from "@tauri-apps/plugin-dialog"; -import { readTextFile } from "@tauri-apps/plugin-fs"; -import { FileUp, Filter, Search } from "lucide-react"; -import * as React from "react"; - -import { AlertList } from "@/components/monitor/alert-list"; -import { IncidentManager } from "@/components/monitor/incident-manager"; -import { KpiDashboard } from "@/components/monitor/kpi-dashboard"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import type { Incident, KpiMetrics, ProcessedAlert } from "@/lib/types/alerts"; - -type StatusFilter = "all" | "resolved" | "unresolved"; - -export const Route = createFileRoute("/monitor")({ - component: MonitorPage, -}); - -function MonitorPage() { - const [alerts, setAlerts] = React.useState([]); - const [incidents, setIncidents] = React.useState([]); - const [kpis, setKpis] = React.useState(null); - const [selectedAlert, setSelectedAlert] = - React.useState(null); - const [isLoading, setIsLoading] = React.useState(false); - const [error, setError] = React.useState(null); - const [searchQuery, setSearchQuery] = React.useState(""); - const [statusFilter, setStatusFilter] = React.useState("all"); - - const filteredAlerts = React.useMemo(() => { - return alerts.filter((alert) => { - if (statusFilter === "resolved" && !alert.isResolved) return false; - if (statusFilter === "unresolved" && alert.isResolved) return false; - - if (!searchQuery.trim()) return true; - - const query = searchQuery.toLowerCase(); - if (alert.alertName?.toLowerCase().includes(query)) return true; - - for (const value of Object.values(alert.labels)) { - if (value.toLowerCase().includes(query)) return true; - } - - return false; - }); - }, [alerts, searchQuery, statusFilter]); - - const selectAllState = React.useMemo(() => { - if (filteredAlerts.length === 0) return "none" as const; - const invalidCount = filteredAlerts.filter((a) => a.isInvalid).length; - if (invalidCount === 0) return "none" as const; - if (invalidCount === filteredAlerts.length) return "all" as const; - return "some" as const; - }, [filteredAlerts]); - - const recalculateKpis = React.useCallback( - async (currentAlerts: ProcessedAlert[], currentIncidents: Incident[]) => { - try { - const newKpis = await invoke("calculate_kpis_command", { - alerts: currentAlerts, - incidents: currentIncidents, - }); - setKpis(newKpis); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } - }, - [], - ); - - const handleLoadFile = async () => { - try { - setIsLoading(true); - setError(null); - - const filePath = await open({ - multiple: false, - filters: [{ name: "JSON", extensions: ["json"] }], - }); - - if (!filePath) { - setIsLoading(false); - return; - } - - const content = await readTextFile(filePath); - - const processedAlerts = await invoke( - "process_alerts_json", - { jsonContent: content }, - ); - - setAlerts(processedAlerts); - setSelectedAlert(null); - await recalculateKpis(processedAlerts, incidents); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setIsLoading(false); - } - }; - - const handleToggleInvalid = (alertId: number, isInvalid: boolean) => { - setAlerts((prev) => { - const updated = prev.map((a) => - a.id === alertId ? { ...a, isInvalid } : a, - ); - recalculateKpis(updated, incidents); - return updated; - }); - - if (selectedAlert?.id === alertId) { - setSelectedAlert((prev) => (prev ? { ...prev, isInvalid } : null)); - } - }; - - const handleToggleAllInvalid = (isInvalid: boolean) => { - const filteredIds = new Set(filteredAlerts.map((a) => a.id)); - setAlerts((prev) => { - const updated = prev.map((a) => - filteredIds.has(a.id) ? { ...a, isInvalid } : a, - ); - recalculateKpis(updated, incidents); - return updated; - }); - - if (selectedAlert && filteredIds.has(selectedAlert.id)) { - setSelectedAlert((prev) => (prev ? { ...prev, isInvalid } : null)); - } - }; - - const handleSelectAlert = (alert: ProcessedAlert) => { - setSelectedAlert(alert); - }; - - const handleAddIncident = (incident: Omit) => { - const newIncident: Incident = { - ...incident, - attachedAlertIds: [], - }; - const newIncidents = [...incidents, newIncident]; - setIncidents(newIncidents); - recalculateKpis(alerts, newIncidents); - }; - - const handleAttachAlert = (alertId: number, incidentId: string) => { - const updatedAlerts = alerts.map((a) => - a.id === alertId ? { ...a, attachedIncidentId: incidentId } : a, - ); - const updatedIncidents = incidents.map((i) => - i.id === incidentId && !i.attachedAlertIds.includes(alertId) - ? { ...i, attachedAlertIds: [...i.attachedAlertIds, alertId] } - : i, - ); - - setAlerts(updatedAlerts); - setIncidents(updatedIncidents); - recalculateKpis(updatedAlerts, updatedIncidents); - - if (selectedAlert?.id === alertId) { - setSelectedAlert((prev) => - prev ? { ...prev, attachedIncidentId: incidentId } : null, - ); - } - }; - - const handleDetachAlert = (alertId: number) => { - const alert = alerts.find((a) => a.id === alertId); - const incidentId = alert?.attachedIncidentId; - - const updatedAlerts = alerts.map((a) => - a.id === alertId ? { ...a, attachedIncidentId: null } : a, - ); - const updatedIncidents = incidentId - ? incidents.map((i) => - i.id === incidentId - ? { - ...i, - attachedAlertIds: i.attachedAlertIds.filter( - (aid) => aid !== alertId, - ), - } - : i, - ) - : incidents; - - setAlerts(updatedAlerts); - setIncidents(updatedIncidents); - recalculateKpis(updatedAlerts, updatedIncidents); - - if (selectedAlert?.id === alertId) { - setSelectedAlert((prev) => - prev ? { ...prev, attachedIncidentId: null } : null, - ); - } - }; - - return ( -
-
-

Alert Monitor

- -
- - {error && ( -
- {error} -
- )} - -
- -
- -
-
-
- - handleToggleAllInvalid(checked === true) - } - disabled={filteredAlerts.length === 0} - aria-label="Mark all filtered as invalid" - /> - - {filteredAlerts.length} - {(searchQuery || statusFilter !== "all") && ` / ${alerts.length}`} - - - - - - {statusFilter === "all" - ? "All" - : statusFilter === "resolved" - ? "Resolved" - : "Unresolved"} - - - } - /> - - setStatusFilter(v as StatusFilter)} - > - All - - Resolved - - - Unresolved - - - - -
- - setSearchQuery(e.target.value)} - className="h-7 pl-7 text-xs" - /> -
-
- {alerts.length === 0 ? ( -
- - - No Alerts Loaded - - - Click "Load Alerts JSON" to import alert data from a Grafana - Alerts API export. - - -
- ) : ( - - )} -
- -
- -
-
-
- ); -} diff --git a/apps/web/src/routes/monitor_.tsx b/apps/web/src/routes/monitor_.tsx new file mode 100644 index 0000000..d18c05b --- /dev/null +++ b/apps/web/src/routes/monitor_.tsx @@ -0,0 +1,34 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/monitor_")({ + component: HomeComponent, +}); + +const TITLE_TEXT = ` + ██████╗ ███████╗████████╗████████╗███████╗██████╗ + ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ + ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ + ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ + ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ + ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ + + ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ + ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ + ██║ ███████╗ ██║ ███████║██║ █████╔╝ + ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ + ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗ + ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ + `; + +function HomeComponent() { + return ( +
+
{TITLE_TEXT}
+
+
+

API Status

+
+
+
+ ); +}