prog on alert pages

This commit is contained in:
mbecker20
2024-04-15 03:29:24 -07:00
parent a5537a0758
commit 0f2b23bb6c
16 changed files with 350 additions and 192 deletions
+19 -2
View File
@@ -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<ListAlerts, User> for State {
@@ -60,3 +63,17 @@ impl Resolve<ListAlerts, User> for State {
Ok(res)
}
}
#[async_trait]
impl Resolve<GetAlert, User> for State {
async fn resolve(
&self,
GetAlert { id }: GetAlert,
_: User,
) -> anyhow::Result<GetAlertResponse> {
find_one_by_id(&db_client().await.alerts, &id)
.await
.context("failed to query db for alert")?
.context("no alert found with given id")
}
}
+1
View File
@@ -115,6 +115,7 @@ enum ReadRequest {
// ==== ALERT ====
ListAlerts(ListAlerts),
GetAlert(GetAlert),
// ==== SERVER STATS ====
#[to_string_resolver]
+16 -1
View File
@@ -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<Alert>,
pub next_page: Option<I64>,
}
//
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(MonitorReadRequest)]
#[response(GetAlertResponse)]
pub struct GetAlert {
pub id: String,
}
#[typeshare]
pub type GetAlertResponse = Alert;
+1
View File
@@ -89,6 +89,7 @@ export type ReadResponses = {
// ==== ALERT ====
ListAlerts: Types.ListAlertsResponse;
GetAlert: Types.GetAlertResponse;
// ==== SERVER STATS ====
GetSystemStats: Types.GetSystemStatsResponse;
+67 -60
View File
@@ -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<Config, Info> {
_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 };
+47
View File
@@ -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 (
<Dialog open={open} onOpenChange={set}>
<DialogTrigger asChild>
<Button variant="secondary" className="items-center gap-2">
Details
</Button>
</DialogTrigger>
<DialogContent className="w-fit min-w-[30vw] max-w-[90vw]">
{alert && (
<>
<DialogHeader className="flex-row justify-between w-full">
{alert && (
<>
<div className="flex gap-4 items-center">
<ResourceLink
type={alert.target.type as UsableResource}
id={alert.target.id}
/>
<AlertLevel level={alert.level} />
</div>
<div className="text-muted-foreground">
{fmt_date_with_minutes(new Date(alert.ts))}
</div>
</>
)}
</DialogHeader>
<pre>{JSON.stringify(alert.data, undefined, 2)}</pre>
</>
)}
</DialogContent>
</Dialog>
);
};
+3 -88
View File
@@ -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 = () => {
</Button>
}
>
{open && (
<DataTable
data={alerts ?? []}
columns={[
{
header: "Details",
cell: ({ row }) =>
row.original._id?.$oid && (
<AlertDetailsDialog id={row.original._id?.$oid} />
),
},
{
header: "Target",
cell: ({ row }) => {
switch (row.original.target.type) {
case "Server":
return (
<ResourceComponents.Server.Link
id={row.original.target.id}
/>
);
default:
return "Unknown";
}
},
},
{
header: "Level",
cell: ({ row }) => <AlertLevel level={row.original.level} />,
},
{
header: "Alert",
accessorKey: "variant",
},
{
header: "Open Since",
accessorFn: ({ ts }) => fmt_date_with_minutes(new Date(ts)),
},
]}
/>
)}
{open && <AlertsTable alerts={alerts ?? []} />}
</Section>
);
};
const AlertLevel = ({ level }: { level: Types.SeverityLevel }) => {
export const AlertLevel = ({ level }: { level: Types.SeverityLevel }) => {
return (
<div
className={text_color_class_by_intention(alert_level_intention(level))}
@@ -88,42 +42,3 @@ const AlertLevel = ({ level }: { level: Types.SeverityLevel }) => {
</div>
);
};
const AlertDetailsDialog = ({ id }: { id: string }) => {
const [open, set] = useState(false);
const alert = useRead("ListAlerts", {}).data?.alerts.find(
(alert) => alert._id?.$oid === id
);
return (
<Dialog open={open} onOpenChange={set}>
<DialogTrigger asChild>
<Button variant="secondary" className="items-center gap-2">
Details
</Button>
</DialogTrigger>
<DialogContent className="w-fit min-w-[30vw] max-w-[90vw]">
{alert && (
<>
<DialogHeader className="flex-row justify-between w-full">
{alert && (
<>
<div className="flex gap-4 items-center">
<ResourceLink
type={alert.target.type as UsableResource}
id={alert.target.id}
/>
<AlertLevel level={alert.level} />
</div>
<div className="text-muted-foreground">
{fmt_date_with_minutes(new Date(alert.ts))}
</div>
</>
)}
</DialogHeader>
<pre>{JSON.stringify(alert.data, undefined, 2)}</pre>
</>
)}
</DialogContent>
</Dialog>
);
};
+48
View File
@@ -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 (
<DataTable
data={alerts ?? []}
columns={[
{
header: "Details",
cell: ({ row }) =>
row.original._id?.$oid && (
<AlertDetailsDialog id={row.original._id?.$oid} />
),
},
{
header: "Target",
cell: ({ row }) => {
const Components =
ResourceComponents[row.original.target.type as UsableResource];
return Components ? (
<Components.Link id={row.original.target.id} />
) : (
"Unknown"
);
},
},
{
header: "Level",
cell: ({ row }) => <AlertLevel level={row.original.level} />,
},
{
header: "Alert Type",
accessorKey: "variant",
},
{
header: "Opened",
accessorFn: ({ ts }) => fmt_date_with_minutes(new Date(ts)),
},
]}
/>
);
};
+1 -1
View File
@@ -43,7 +43,7 @@ export const Page = ({
<div className="flex flex-col gap-12 container py-8">
{(title || subtitle || actions) && (
<div className="flex flex-col gap-6 lg:flex-row lg:gap-0 lg:items-start justify-between">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-4">
<div className="flex gap-4 items-center">
<h1 className="text-4xl">{title}</h1>
{titleRight}
@@ -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" && <ServerStats server_id={id} />;
// },
Alerts: ({ id }) => {
const alerts = useRead("ListAlerts", {
query: { "target.type": "Server", "target.id": id },
}).data?.alerts.slice(0, 3);
return (
(alerts?.length || 0) > 0 && (
<Section
title="Alerts"
icon={<AlertTriangle className="w-4 h-4" />}
actions={
<Link to={`/servers/${id}/alerts`}>
<Button variant="secondary" size="icon">
<ExternalLink className="w-4 h-4" />
</Button>
</Link>
}
>
<AlertsTable alerts={alerts ?? []} />
</Section>
)
);
},
Deployments: ({ id }) => {
const deployments = useRead("ListDeployments", {}).data?.filter(
(deployment) => deployment.info.server_id === id
);
return (
<Section title="Deployments" icon={<Rocket className="w-4 h-4" />}>
<DeploymentTable deployments={deployments} />
</Section>
(deployments?.length || 0) > 0 && (
<Section title="Deployments" icon={<Rocket className="w-4 h-4" />}>
<DeploymentTable deployments={deployments} />
</Section>
)
);
},
Config: ServerConfig,
+10
View File
@@ -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 = () => {
? [<Key className="w-4 h-4" />, "Api Keys"]
: location.pathname === "/tags"
? [<Tag className="w-4 h-4" />, "Tags"]
: location.pathname === "/alerts"
? [<AlertTriangle className="w-4 h-4" />, "Alerts"]
: location.pathname === "/users"
? [<UserCircle2 className="w-4 h-4" />, "Users"]
: [<FileQuestion className="w-4 h-4" />, "Unknown"];
@@ -121,6 +124,13 @@ const PrimaryDropdown = () => {
<DropdownMenuSeparator />
<Link to="/alerts">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<AlertTriangle className="w-4 h-4" />
Alerts
</DropdownMenuItem>
</Link>
<Link to="/tags">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Tag className="w-4 h-4" />
+1 -1
View File
@@ -71,7 +71,7 @@ export const ResourceUpdates = ({ type, id }: Types.ResourceTarget) => {
icon={<Bell className="w-4 h-4" />}
actions={
<Link to={`/${type.toLowerCase()}s/${id}/updates`}>
<Button variant="secondary">
<Button variant="secondary" size="icon">
<ExternalLink className="w-4 h-4" />
</Button>
</Link>
+102
View File
@@ -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<Types.AlertData["type"] | "All">(
"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 (
<Page title="Alerts">
<div className="flex flex-col gap-4">
<div className="flex gap-4 items-center justify-end">
<div
className="flex gap-3 items-center cursor-pointer"
onClick={() => setOnlyOpen(!onlyOpen)}
>
<Label htmlFor="only-open" className="text-nowrap cursor-pointer">
Only Open
</Label>
<Switch id="only-open" checked={onlyOpen} />
</div>
<Select
value={variant}
onValueChange={(variant) => {
setVariant(variant as Types.AlertData["type"] | "All");
}}
>
<SelectTrigger className="w-[200px] overflow-ellipsis">
<SelectValue placeholder="Alert Type" />
</SelectTrigger>
<SelectContent align="end">
{["All", ...alert_types].map((variant) => (
<SelectItem value={variant}>{variant}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<AlertsTable alerts={alerts?.alerts ?? []} />
<div className="flex gap-4 justify-center items-center text-muted-foreground">
<Button
variant="outline"
onClick={() => setPage(page - 1)}
disabled={page === 0}
>
Prev Page
</Button>
Page: {page + 1}
<Button
variant="outline"
onClick={() => alerts?.next_page && setPage(alerts.next_page)}
disabled={!alerts?.next_page}
>
Next Page
</Button>
</div>
</div>
</Page>
);
};
-30
View File
@@ -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 (
<Page title="Api Keys" actions={<CreateKey />}>
{/* <ApiKeysList /> */}
<DataTable
data={keys}
columns={[
@@ -69,33 +66,6 @@ export const Keys = () => {
);
};
export const ApiKeysList = () => {
const keys = useRead("ListApiKeys", {}).data;
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{keys?.map((key) => (
<Card
id={key.key}
className="h-full hover:bg-accent/50 group-focus:bg-accent/50 transition-colors"
>
<CardHeader className="flex-row justify-between items-center">
<CardTitle>{key.name}</CardTitle>
<DeleteKey api_key={key.key} />
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
<div>created at: {fmt_date(new Date(key.created_at))}</div>
<div>
expires:{" "}
{key.expires === 0 ? "never" : fmt_date(new Date(key.expires))}
</div>
<div>{key.key}</div>
</CardContent>
</Card>
))}
</div>
);
};
const ONE_DAY_MS = 1000 * 60 * 60 * 24;
type ExpiresOptions = "90 days" | "180 days" | "1 year" | "never";
+1 -2
View File
@@ -27,7 +27,7 @@ export const Resource = () => {
</div>
}
subtitle={
<div className="text-sm text-muted-foreground flex flex-col gap-2">
<div className="text-sm text-muted-foreground flex flex-col gap-4">
<div className="flex gap-4 items-center">
<div className="flex gap-2 items-center">
<Components.Icon id={id} />
@@ -51,7 +51,6 @@ export const Resource = () => {
}
>
<ResourceUpdates type={type} id={id} />
{/* <ResourcePermissions type={type} id={id} /> */}
{Object.entries(Components.Page).map(([section, Component]) => (
<Component key={section} id={id} />
))}
+3
View File
@@ -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: <Tags /> },
{ path: "tree", element: <Tree /> },
{ path: "resources", element: <AllResources /> },
{ path: "alerts", element: <Alerts /> },
{
path: ":type",
children: [
@@ -31,6 +33,7 @@ const ROUTER = createBrowserRouter([
{ path: ":id", element: <Resource /> },
{ path: ":id/stats", element: <ResourceStats /> },
{ path: ":id/updates", element: <ResourceUpdates /> },
{ path: ":id/alerts", element: <Alerts /> },
],
},
{