mirror of
https://github.com/moghtech/komodo.git
synced 2026-09-11 08:01:13 +00:00
prog on alert pages
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ enum ReadRequest {
|
||||
|
||||
// ==== ALERT ====
|
||||
ListAlerts(ListAlerts),
|
||||
GetAlert(GetAlert),
|
||||
|
||||
// ==== SERVER STATS ====
|
||||
#[to_string_resolver]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -89,6 +89,7 @@ export type ReadResponses = {
|
||||
|
||||
// ==== ALERT ====
|
||||
ListAlerts: Types.ListAlertsResponse;
|
||||
GetAlert: Types.GetAlertResponse;
|
||||
|
||||
// ==== SERVER STATS ====
|
||||
GetSystemStats: Types.GetSystemStatsResponse;
|
||||
|
||||
+67
-60
@@ -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 };
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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 /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user