From 0f2b23bb6cb6ec112149c840713a41f056fb57c8 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Mon, 15 Apr 2024 03:29:24 -0700 Subject: [PATCH] prog on alert pages --- bin/core/src/api/read/alert.rs | 21 ++- bin/core/src/api/read/mod.rs | 1 + client/core/rs/src/api/read/alert.rs | 17 ++- client/core/ts/src/responses.ts | 1 + client/core/ts/src/types.ts | 127 +++++++++--------- frontend/src/components/alert/details.tsx | 47 +++++++ frontend/src/components/alert/index.tsx | 91 +------------ frontend/src/components/alert/table.tsx | 48 +++++++ frontend/src/components/layouts.tsx | 2 +- .../src/components/resources/server/index.tsx | 37 ++++- frontend/src/components/topbar.tsx | 10 ++ frontend/src/components/updates/resource.tsx | 2 +- frontend/src/pages/alerts.tsx | 102 ++++++++++++++ frontend/src/pages/keys.tsx | 30 ----- frontend/src/pages/resource.tsx | 3 +- frontend/src/router.tsx | 3 + 16 files changed, 350 insertions(+), 192 deletions(-) create mode 100644 frontend/src/components/alert/details.tsx create mode 100644 frontend/src/components/alert/table.tsx create mode 100644 frontend/src/pages/alerts.tsx diff --git a/bin/core/src/api/read/alert.rs b/bin/core/src/api/read/alert.rs index e7d053943..5f86407eb 100644 --- a/bin/core/src/api/read/alert.rs +++ b/bin/core/src/api/read/alert.rs @@ -1,10 +1,13 @@ use anyhow::Context; use async_trait::async_trait; use monitor_client::{ - api::read::{ListAlerts, ListAlertsResponse}, + api::read::{ + GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse, + }, entities::{deployment::Deployment, server::Server, user::User}, }; use mungos::{ + by_id::find_one_by_id, find::find_collect, mongodb::{bson::doc, options::FindOptions}, }; @@ -14,7 +17,7 @@ use crate::{ db::db_client, helpers::resource::StateResource, state::State, }; -const NUM_ALERTS_PER_PAGE: u64 = 10; +const NUM_ALERTS_PER_PAGE: u64 = 20; #[async_trait] impl Resolve for State { @@ -60,3 +63,17 @@ impl Resolve for State { Ok(res) } } + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetAlert { id }: GetAlert, + _: User, + ) -> anyhow::Result { + find_one_by_id(&db_client().await.alerts, &id) + .await + .context("failed to query db for alert")? + .context("no alert found with given id") + } +} diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index 4742a195c..a30393222 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -115,6 +115,7 @@ enum ReadRequest { // ==== ALERT ==== ListAlerts(ListAlerts), + GetAlert(GetAlert), // ==== SERVER STATS ==== #[to_string_resolver] diff --git a/client/core/rs/src/api/read/alert.rs b/client/core/rs/src/api/read/alert.rs index 289a7aec9..8dfeaacb6 100644 --- a/client/core/rs/src/api/read/alert.rs +++ b/client/core/rs/src/api/read/alert.rs @@ -9,7 +9,7 @@ use super::MonitorReadRequest; #[typeshare] #[derive( - Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, + Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits, )] #[empty_traits(MonitorReadRequest)] #[response(ListAlertsResponse)] @@ -25,3 +25,18 @@ pub struct ListAlertsResponse { pub alerts: Vec, pub next_page: Option, } + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetAlertResponse)] +pub struct GetAlert { + pub id: String, +} + +#[typeshare] +pub type GetAlertResponse = Alert; diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index 7c49a8f29..baa664cdd 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -89,6 +89,7 @@ export type ReadResponses = { // ==== ALERT ==== ListAlerts: Types.ListAlertsResponse; + GetAlert: Types.GetAlertResponse; // ==== SERVER STATS ==== GetSystemStats: Types.GetSystemStatsResponse; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 398a92896..f4801add2 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -55,6 +55,67 @@ export interface User { export type GetUserResponse = User; +export enum SeverityLevel { + Ok = "OK", + Warning = "WARNING", + Critical = "CRITICAL", +} + +export type AlertData = + | { type: "ServerUnreachable", data: { + id: string; + name: string; + region?: string; + err?: _Serror; +}} + | { type: "ServerCpu", data: { + id: string; + name: string; + region?: string; + percentage: number; +}} + | { type: "ServerMem", data: { + id: string; + name: string; + region?: string; + used_gb: number; + total_gb: number; +}} + | { type: "ServerDisk", data: { + id: string; + name: string; + region?: string; + path: string; + used_gb: number; + total_gb: number; +}} + | { type: "ContainerStateChange", data: { + id: string; + name: string; + server_id: string; + server_name: string; + from: DockerContainerState; + to: DockerContainerState; +}} + | { type: "AwsBuilderTerminationFailed", data: { + instance_id: string; +}} + | { type: "None", data: { +}}; + +export interface Alert { + _id?: MongoId; + ts: I64; + resolved: boolean; + level: SeverityLevel; + target: ResourceTarget; + variant: AlertData["type"]; + data: AlertData; + resolved_ts?: I64; +} + +export type GetAlertResponse = Alert; + export interface Resource { _id?: MongoId; name: string; @@ -938,70 +999,15 @@ export interface ListAlerts { page?: U64; } -export enum SeverityLevel { - Ok = "OK", - Warning = "WARNING", - Critical = "CRITICAL", -} - -export type AlertData = - | { type: "ServerUnreachable", data: { - id: string; - name: string; - region?: string; - err?: _Serror; -}} - | { type: "ServerCpu", data: { - id: string; - name: string; - region?: string; - percentage: number; -}} - | { type: "ServerMem", data: { - id: string; - name: string; - region?: string; - used_gb: number; - total_gb: number; -}} - | { type: "ServerDisk", data: { - id: string; - name: string; - region?: string; - path: string; - used_gb: number; - total_gb: number; -}} - | { type: "ContainerStateChange", data: { - id: string; - name: string; - server_id: string; - server_name: string; - from: DockerContainerState; - to: DockerContainerState; -}} - | { type: "AwsBuilderTerminationFailed", data: { - instance_id: string; -}} - | { type: "None", data: { -}}; - -export interface Alert { - _id?: MongoId; - ts: I64; - resolved: boolean; - level: SeverityLevel; - target: ResourceTarget; - variant: AlertData["type"]; - data: AlertData; - resolved_ts?: I64; -} - export interface ListAlertsResponse { alerts: Alert[]; next_page?: I64; } +export interface GetAlert { + id: string; +} + export interface GetAlerter { /** Id or name */ alerter: string; @@ -1785,7 +1791,6 @@ export type ReadRequest = | { type: "ListServers", params: ListServers } | { type: "GetServerStatus", params: GetServerStatus } | { type: "GetPeripheryVersion", params: GetPeripheryVersion } - | { type: "GetSystemInformation", params: GetSystemInformation } | { type: "GetDockerContainers", params: GetDockerContainers } | { type: "GetDockerImages", params: GetDockerImages } | { type: "GetDockerNetworks", params: GetDockerNetworks } @@ -1825,6 +1830,8 @@ export type ReadRequest = | { type: "GetUpdate", params: GetUpdate } | { type: "ListUpdates", params: ListUpdates } | { type: "ListAlerts", params: ListAlerts } + | { type: "GetAlert", params: GetAlert } + | { type: "GetSystemInformation", params: GetSystemInformation } | { type: "GetSystemStats", params: GetSystemStats } | { type: "GetSystemProcesses", params: GetSystemProcesses }; diff --git a/frontend/src/components/alert/details.tsx b/frontend/src/components/alert/details.tsx new file mode 100644 index 000000000..6287528b9 --- /dev/null +++ b/frontend/src/components/alert/details.tsx @@ -0,0 +1,47 @@ +import { ResourceLink } from "@components/resources/common"; +import { useRead } from "@lib/hooks"; +import { UsableResource } from "@types"; +import { Button } from "@ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "@ui/dialog"; +import { useState } from "react"; +import { AlertLevel } from "."; +import { fmt_date_with_minutes } from "@lib/formatting"; + +export const AlertDetailsDialog = ({ id }: { id: string }) => { + const [open, set] = useState(false); + const alert = useRead("ListAlerts", {}).data?.alerts.find( + (alert) => alert._id?.$oid === id + ); + return ( + + + + + + {alert && ( + <> + + {alert && ( + <> +
+ + +
+
+ {fmt_date_with_minutes(new Date(alert.ts))} +
+ + )} +
+
{JSON.stringify(alert.data, undefined, 2)}
+ + )} +
+
+ ); +}; diff --git a/frontend/src/components/alert/index.tsx b/frontend/src/components/alert/index.tsx index 94ff49c4b..b0915a141 100644 --- a/frontend/src/components/alert/index.tsx +++ b/frontend/src/components/alert/index.tsx @@ -1,21 +1,15 @@ import { Section } from "@components/layouts"; -import { ResourceComponents } from "@components/resources"; -import { ResourceLink } from "@components/resources/common"; import { alert_level_intention, text_color_class_by_intention, } from "@lib/color"; -import { fmt_date_with_minutes } from "@lib/formatting"; import { useRead } from "@lib/hooks"; import { Types } from "@monitor/client"; -import { UsableResource } from "@types"; import { Button } from "@ui/button"; -import { DataTable } from "@ui/data-table"; -import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "@ui/dialog"; import { useAtom } from "jotai"; import { atomWithStorage } from "jotai/utils"; import { AlertTriangle } from "lucide-react"; -import { useState } from "react"; +import { AlertsTable } from "./table"; const openAtom = atomWithStorage("show-alerts-v0", true); @@ -34,52 +28,12 @@ export const OpenAlerts = () => { } > - {open && ( - - row.original._id?.$oid && ( - - ), - }, - { - header: "Target", - cell: ({ row }) => { - switch (row.original.target.type) { - case "Server": - return ( - - ); - default: - return "Unknown"; - } - }, - }, - { - header: "Level", - cell: ({ row }) => , - }, - { - header: "Alert", - accessorKey: "variant", - }, - { - header: "Open Since", - accessorFn: ({ ts }) => fmt_date_with_minutes(new Date(ts)), - }, - ]} - /> - )} + {open && } ); }; -const AlertLevel = ({ level }: { level: Types.SeverityLevel }) => { +export const AlertLevel = ({ level }: { level: Types.SeverityLevel }) => { return (
{
); }; - -const AlertDetailsDialog = ({ id }: { id: string }) => { - const [open, set] = useState(false); - const alert = useRead("ListAlerts", {}).data?.alerts.find( - (alert) => alert._id?.$oid === id - ); - return ( - - - - - - {alert && ( - <> - - {alert && ( - <> -
- - -
-
- {fmt_date_with_minutes(new Date(alert.ts))} -
- - )} -
-
{JSON.stringify(alert.data, undefined, 2)}
- - )} -
-
- ); -}; diff --git a/frontend/src/components/alert/table.tsx b/frontend/src/components/alert/table.tsx new file mode 100644 index 000000000..956bfdabe --- /dev/null +++ b/frontend/src/components/alert/table.tsx @@ -0,0 +1,48 @@ +import { ResourceComponents } from "@components/resources"; +import { fmt_date_with_minutes } from "@lib/formatting"; +import { Types } from "@monitor/client"; +import { DataTable } from "@ui/data-table"; +import { AlertLevel } from "."; +import { AlertDetailsDialog } from "./details"; +import { UsableResource } from "@types"; + +export const AlertsTable = ({ alerts }: { alerts: Types.Alert[] }) => { + return ( + + row.original._id?.$oid && ( + + ), + }, + { + header: "Target", + cell: ({ row }) => { + const Components = + ResourceComponents[row.original.target.type as UsableResource]; + return Components ? ( + + ) : ( + "Unknown" + ); + }, + }, + { + header: "Level", + cell: ({ row }) => , + }, + { + header: "Alert Type", + accessorKey: "variant", + }, + { + header: "Opened", + accessorFn: ({ ts }) => fmt_date_with_minutes(new Date(ts)), + }, + ]} + /> + ); +}; diff --git a/frontend/src/components/layouts.tsx b/frontend/src/components/layouts.tsx index 14490059f..cdf4dbde4 100644 --- a/frontend/src/components/layouts.tsx +++ b/frontend/src/components/layouts.tsx @@ -43,7 +43,7 @@ export const Page = ({
{(title || subtitle || actions) && (
-
+

{title}

{titleRight} diff --git a/frontend/src/components/resources/server/index.tsx b/frontend/src/components/resources/server/index.tsx index 0af7dcf70..cd2870422 100644 --- a/frontend/src/components/resources/server/index.tsx +++ b/frontend/src/components/resources/server/index.tsx @@ -9,6 +9,7 @@ import { Cpu, MemoryStick, Database, + ExternalLink, } from "lucide-react"; import { Section } from "@components/layouts"; import { RenameServer, SERVER_ACTIONS } from "./actions"; @@ -23,6 +24,8 @@ import { ServerTable } from "./table"; import { ServersChart } from "./dashboard"; import { Link } from "react-router-dom"; import { DeleteResource, NewResource, ResourceLink } from "../common"; +import { AlertsTable } from "@components/alert/table"; +import { Button } from "@ui/button"; export const useServer = (id?: string) => useRead("ListServers", {}).data?.find((d) => d.id === id); @@ -103,18 +106,38 @@ export const ServerComponents: RequiredResourceComponents = { }, Actions: SERVER_ACTIONS, Page: { - // Stats: ({ id }) => { - // const status = useServer(id)?.info.status; - // return status === "Ok" && ; - // }, + Alerts: ({ id }) => { + const alerts = useRead("ListAlerts", { + query: { "target.type": "Server", "target.id": id }, + }).data?.alerts.slice(0, 3); + return ( + (alerts?.length || 0) > 0 && ( +
} + actions={ + + + + } + > + +
+ ) + ); + }, Deployments: ({ id }) => { const deployments = useRead("ListDeployments", {}).data?.filter( (deployment) => deployment.info.server_id === id ); return ( -
}> - -
+ (deployments?.length || 0) > 0 && ( +
}> + +
+ ) ); }, Config: ServerConfig, diff --git a/frontend/src/components/topbar.tsx b/frontend/src/components/topbar.tsx index b84d9f25b..d24b34c15 100644 --- a/frontend/src/components/topbar.tsx +++ b/frontend/src/components/topbar.tsx @@ -1,6 +1,7 @@ import { useRead, useResourceParamType } from "@lib/hooks"; import { ResourceComponents } from "./resources"; import { + AlertTriangle, Box, Boxes, FileQuestion, @@ -81,6 +82,8 @@ const PrimaryDropdown = () => { ? [, "Api Keys"] : location.pathname === "/tags" ? [, "Tags"] + : location.pathname === "/alerts" + ? [, "Alerts"] : location.pathname === "/users" ? [, "Users"] : [, "Unknown"]; @@ -121,6 +124,13 @@ const PrimaryDropdown = () => { + + + + Alerts + + + diff --git a/frontend/src/components/updates/resource.tsx b/frontend/src/components/updates/resource.tsx index ea00279f2..544497f2e 100644 --- a/frontend/src/components/updates/resource.tsx +++ b/frontend/src/components/updates/resource.tsx @@ -71,7 +71,7 @@ export const ResourceUpdates = ({ type, id }: Types.ResourceTarget) => { icon={} actions={ - diff --git a/frontend/src/pages/alerts.tsx b/frontend/src/pages/alerts.tsx new file mode 100644 index 000000000..7caafd98e --- /dev/null +++ b/frontend/src/pages/alerts.tsx @@ -0,0 +1,102 @@ +import { AlertsTable } from "@components/alert/table"; +import { Page } from "@components/layouts"; +import { useRead, useResourceParamType } from "@lib/hooks"; +import { Types } from "@monitor/client"; +import { Button } from "@ui/button"; +import { Label } from "@ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ui/select"; +import { Switch } from "@ui/switch"; +import { useState } from "react"; +import { useParams } from "react-router"; + +const ALERT_TYPES = { + Server: ["ServerUnreachable", "ServerCpu", "ServerMem", "ServerDisk"], + Deployment: ["ContainerStateChange"], +}; + +const FALLBACK_ALERT_TYPES = [ + ...ALERT_TYPES.Server, + ...ALERT_TYPES.Deployment, + "AwsBuilderTerminationFailed", +]; + +export const Alerts = () => { + const type = useResourceParamType(); + const id = useParams().id as string | undefined; + const alert_types: string[] = type + ? ALERT_TYPES[type] ?? FALLBACK_ALERT_TYPES + : FALLBACK_ALERT_TYPES; + + const [page, setPage] = useState(0); + const [variant, setVariant] = useState( + "All" + ); + const [onlyOpen, setOnlyOpen] = useState(false); + const alerts = useRead("ListAlerts", { + query: { + "target.type": type, + "target.id": id, + variant: variant === "All" ? undefined : variant, + resolved: onlyOpen ? false : undefined, + }, + page, + }).data; + return ( + +
+
+
setOnlyOpen(!onlyOpen)} + > + + +
+ +
+ + + +
+ + Page: {page + 1} + +
+
+
+ ); +}; diff --git a/frontend/src/pages/keys.tsx b/frontend/src/pages/keys.tsx index e9ad05e4b..5427b6141 100644 --- a/frontend/src/pages/keys.tsx +++ b/frontend/src/pages/keys.tsx @@ -10,7 +10,6 @@ import { DialogTrigger, } from "@ui/dialog"; import { Button } from "@ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; import { useToast } from "@ui/use-toast"; import { Trash, PlusCircle, Loader2, Check } from "lucide-react"; import { useState } from "react"; @@ -23,14 +22,12 @@ import { DropdownMenuTrigger, } from "@ui/dropdown-menu"; import { DataTable } from "@ui/data-table"; -import { fmt_date } from "@lib/formatting"; export const Keys = () => { useSetTitle("Api Keys"); const keys = useRead("ListApiKeys", {}).data ?? []; return ( }> - {/* */} { ); }; -export const ApiKeysList = () => { - const keys = useRead("ListApiKeys", {}).data; - return ( -
- {keys?.map((key) => ( - - - {key.name} - - - -
created at: {fmt_date(new Date(key.created_at))}
-
- expires:{" "} - {key.expires === 0 ? "never" : fmt_date(new Date(key.expires))} -
-
{key.key}
-
-
- ))} -
- ); -}; - const ONE_DAY_MS = 1000 * 60 * 60 * 24; type ExpiresOptions = "90 days" | "180 days" | "1 year" | "never"; diff --git a/frontend/src/pages/resource.tsx b/frontend/src/pages/resource.tsx index b9bfadb98..da3e9b73a 100644 --- a/frontend/src/pages/resource.tsx +++ b/frontend/src/pages/resource.tsx @@ -27,7 +27,7 @@ export const Resource = () => {
} subtitle={ -
+
@@ -51,7 +51,6 @@ export const Resource = () => { } > - {/* */} {Object.entries(Components.Page).map(([section, Component]) => ( ))} diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index b647aef93..90b6d1245 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -13,6 +13,7 @@ import { AllResources } from "@pages/home/all_resources"; import { UserDisabled } from "@pages/user_disabled"; import { Home } from "@pages/home"; import { ResourceStats } from "@pages/resource_stats"; +import { Alerts } from "@pages/alerts"; const ROUTER = createBrowserRouter([ { @@ -24,6 +25,7 @@ const ROUTER = createBrowserRouter([ { path: "tags", element: }, { path: "tree", element: }, { path: "resources", element: }, + { path: "alerts", element: }, { path: ":type", children: [ @@ -31,6 +33,7 @@ const ROUTER = createBrowserRouter([ { path: ":id", element: }, { path: ":id/stats", element: }, { path: ":id/updates", element: }, + { path: ":id/alerts", element: }, ], }, {