standardize coloring

This commit is contained in:
mbecker20
2024-03-31 10:29:21 -07:00
parent fc602054ba
commit 516690b260
23 changed files with 410 additions and 901 deletions

View File

@@ -19,7 +19,7 @@ import {
SearchX, SearchX,
} from "lucide-react"; } from "lucide-react";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import { cn, snake_case_to_upper_space_case } from "@lib/utils"; import { cn } from "@lib/utils";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -36,6 +36,7 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
} from "@ui/command"; } from "@ui/command";
import { snake_case_to_upper_space_case } from "@lib/formatting";
export const ConfigItem = ({ export const ConfigItem = ({
label, label,
@@ -52,7 +53,7 @@ export const ConfigItem = ({
className className
)} )}
> >
<div className="capitalize"> {snake_case_to_upper_space_case(label)} </div> <div>{snake_case_to_upper_space_case(label)}</div>
{children} {children}
</div> </div>
); );

View File

@@ -9,13 +9,18 @@ import { PieChart } from "react-minimal-pie-chart";
import { useRead } from "@lib/hooks"; import { useRead } from "@lib/hooks";
import { Rocket } from "lucide-react"; import { Rocket } from "lucide-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { cn } from "@lib/utils";
import {
hex_color_by_intention,
text_color_class_by_intention,
} from "@lib/color";
export const DeploymentsChart = () => { export const DeploymentsChart = () => {
const summary = useRead("GetDeploymentsSummary", {}).data; const summary = useRead("GetDeploymentsSummary", {}).data;
return ( return (
<Link to="/deployments" className="w-full"> <Link to="/deployments" className="w-full">
<Card > <Card>
<CardHeader className="justify-between"> <CardHeader className="justify-between">
<div> <div>
<CardTitle>Deployments</CardTitle> <CardTitle>Deployments</CardTitle>
@@ -26,25 +31,45 @@ export const DeploymentsChart = () => {
<CardContent className="flex h-[200px] items-center justify-between"> <CardContent className="flex h-[200px] items-center justify-between">
<div className="flex flex-col gap-2 text-muted-foreground w-full"> <div className="flex flex-col gap-2 text-muted-foreground w-full">
<CardDescription> <CardDescription>
<span className="text-green-500 font-bold"> <span
className={cn(
text_color_class_by_intention("Good"),
"font-bold"
)}
>
{summary?.running}{" "} {summary?.running}{" "}
</span> </span>
Running Running
</CardDescription> </CardDescription>
<CardDescription> <CardDescription>
<span className="text-red-500 font-bold"> <span
className={cn(
text_color_class_by_intention("Critical"),
"font-bold"
)}
>
{summary?.stopped}{" "} {summary?.stopped}{" "}
</span> </span>
Stopped Stopped
</CardDescription> </CardDescription>
<CardDescription> <CardDescription>
<span className="text-blue-500 font-bold"> <span
className={cn(
text_color_class_by_intention("Neutral"),
"font-bold"
)}
>
{summary?.not_deployed}{" "} {summary?.not_deployed}{" "}
</span> </span>
Not Deployed Not Deployed
</CardDescription> </CardDescription>
<CardDescription> <CardDescription>
<span className="text-purple-500 font-bold"> <span
className={cn(
text_color_class_by_intention("Unknown"),
"font-bold"
)}
>
{summary?.unknown}{" "} {summary?.unknown}{" "}
</span> </span>
Unknown Unknown
@@ -57,25 +82,25 @@ export const DeploymentsChart = () => {
lineWidth={30} lineWidth={30}
data={[ data={[
{ {
color: "#22C55E", color: hex_color_by_intention("Good"),
value: summary?.running ?? 0, value: summary?.running ?? 0,
title: "running", title: "running",
key: "running", key: "running",
}, },
{ {
color: "#EF0044", color: hex_color_by_intention("Critical"),
value: summary?.stopped ?? 0, value: summary?.stopped ?? 0,
title: "stopped", title: "stopped",
key: "stopped", key: "stopped",
}, },
{ {
color: "#3B82F6", color: hex_color_by_intention("Neutral"),
value: summary?.not_deployed ?? 0, value: summary?.not_deployed ?? 0,
title: "not-deployed", title: "not-deployed",
key: "not-deployed", key: "not-deployed",
}, },
{ {
color: "purple", color: hex_color_by_intention("Unknown"),
value: summary?.unknown ?? 0, value: summary?.unknown ?? 0,
title: "unknown", title: "unknown",
key: "unknown", key: "unknown",

View File

@@ -9,6 +9,11 @@ import { PieChart } from "react-minimal-pie-chart";
import { useRead } from "@lib/hooks"; import { useRead } from "@lib/hooks";
import { Server } from "lucide-react"; import { Server } from "lucide-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import {
hex_color_by_intention,
text_color_class_by_intention,
} from "@lib/color";
import { cn } from "@lib/utils";
export const ServersChart = () => { export const ServersChart = () => {
const { data } = useRead("GetServersSummary", {}); const { data } = useRead("GetServersSummary", {});
@@ -25,15 +30,36 @@ export const ServersChart = () => {
<CardContent className="flex h-[200px] items-center justify-between"> <CardContent className="flex h-[200px] items-center justify-between">
<div className="flex flex-col gap-2 text-muted-foreground w-full"> <div className="flex flex-col gap-2 text-muted-foreground w-full">
<CardDescription> <CardDescription>
<span className="text-green-500 font-bold">{data?.healthy} </span> <span
className={cn(
text_color_class_by_intention("Good"),
"font-bold"
)}
>
{data?.healthy}{" "}
</span>
Healthy Healthy
</CardDescription> </CardDescription>
<CardDescription> <CardDescription>
<span className="text-red-500 font-bold">{data?.unhealthy} </span> <span
className={cn(
text_color_class_by_intention("Critical"),
"font-bold"
)}
>
{data?.unhealthy}{" "}
</span>
Unhealthy Unhealthy
</CardDescription> </CardDescription>
<CardDescription> <CardDescription>
<span className="text-blue-500 font-bold">{data?.disabled} </span> <span
className={cn(
text_color_class_by_intention("Neutral"),
"font-bold"
)}
>
{data?.disabled}{" "}
</span>
Disabled Disabled
</CardDescription> </CardDescription>
</div> </div>
@@ -44,19 +70,19 @@ export const ServersChart = () => {
lineWidth={30} lineWidth={30}
data={[ data={[
{ {
color: "#22C55E", color: hex_color_by_intention("Good"),
value: data?.healthy ?? 0, value: data?.healthy ?? 0,
title: "healthy", title: "healthy",
key: "healthy", key: "healthy",
}, },
{ {
color: "#EF0044", color: hex_color_by_intention("Critical"),
value: data?.unhealthy ?? 0, value: data?.unhealthy ?? 0,
title: "unhealthy", title: "unhealthy",
key: "unhealthy", key: "unhealthy",
}, },
{ {
color: "#3B82F6", color: hex_color_by_intention("Neutral"),
value: data?.disabled ?? 0, value: data?.disabled ?? 0,
title: "disabled", title: "disabled",
key: "disabled", key: "disabled",

View File

@@ -1,192 +0,0 @@
// import { Section } from "./layouts";
// import { Loader2, Lock, PlusCircle, User } from "lucide-react";
// import { useRead, useWrite } from "@lib/hooks";
// import { UsableResource } from "@types";
// import { Card, CardHeader, CardTitle } from "@ui/card";
// import {
// Select,
// SelectContent,
// SelectGroup,
// SelectItem,
// SelectTrigger,
// SelectValue,
// } from "@ui/select";
// import { Types } from "@monitor/client";
// import {
// Dialog,
// DialogContent,
// DialogFooter,
// DialogHeader,
// DialogTitle,
// DialogTrigger,
// } from "@ui/dialog";
// import { Button } from "@ui/button";
// import { useState } from "react";
// import { ResourceTarget } from "@monitor/client/dist/types";
// const Username = ({ user_id }: { user_id: string }) => {
// const username = useRead("GetUsername", { user_id }).data?.username;
// return <>{username}</>;
// };
// const NewPermission = ({ id, type }: ResourceTarget) => {
// const [open, set] = useState(false);
// const [user_id, setUserId] = useState<string>();
// const [permission, setPermission] = useState<Types.PermissionLevel>();
// const users = useRead("GetUsers", {}).data?.filter((u) => !u.admin);
// const { mutate, isPending } = useWrite("UpdateUserPermissionsOnTarget");
// return (
// <Dialog open={open} onOpenChange={set}>
// <DialogTrigger asChild>
// <Button className="gap-2">
// Add Permission
// <PlusCircle className="w-4 h-4" />
// </Button>
// </DialogTrigger>
// <DialogContent>
// <DialogHeader>
// <DialogTitle>Add User</DialogTitle>
// </DialogHeader>
// <div className="grid gap-4 my-4">
// <div className="flex items-center justify-between">
// User
// <Select value={user_id} onValueChange={setUserId}>
// <SelectTrigger className="w-48">
// <SelectValue placeholder="Select User" />
// </SelectTrigger>
// <SelectContent className="w-48">
// <SelectGroup>
// {users?.map((user) => (
// <SelectItem key={user._id?.$oid} value={user._id!.$oid}>
// {user.username}
// </SelectItem>
// ))}
// </SelectGroup>
// </SelectContent>
// </Select>
// </div>
// <div className="flex items-center justify-between">
// Permissions Level
// <Select
// value={permission}
// onValueChange={(lv: any) =>
// setPermission(lv as Types.PermissionLevel)
// }
// >
// <SelectTrigger className="w-48">
// <SelectValue placeholder="Select Permission Level" />
// </SelectTrigger>
// <SelectContent className="w-48">
// <SelectGroup>
// <SelectItem value={Types.PermissionLevel.Read}>
// Read
// </SelectItem>
// <SelectItem value={Types.PermissionLevel.Execute}>
// Execute
// </SelectItem>
// <SelectItem value={Types.PermissionLevel.Write}>
// Write
// </SelectItem>
// </SelectGroup>
// </SelectContent>
// </Select>
// </div>
// </div>
// <DialogFooter className="flex justify-end">
// <Button
// className="gap-2"
// disabled={isPending}
// onClick={() =>
// permission &&
// user_id &&
// mutate({ permission, user_id, target: { id, type } })
// }
// >
// Add Permissions
// {isPending ? (
// <Loader2 className="w-4 h-4 animate-spin" />
// ) : (
// <PlusCircle className="w-4 h-4" />
// )}
// </Button>
// </DialogFooter>
// </DialogContent>
// </Dialog>
// );
// };
// // export const ResourcePermissions = ({
// // type,
// // id,
// // }: {
// // type: UsableResource;
// // id: string;
// // }) => {
// // const user = useRead("GetUser", {}).data;
// // const admin = user?.admin;
// // const me = user?._id?.$oid;
// // const permissions = useRead(`Get${type}`, { id }, { enabled: admin }).data
// // ?.permissions;
// // const users = useRead("GetUsers", {}).data?.filter((u) => !u.admin);
// // const { mutate: update, isPending } = useWrite(
// // "UpdateUserPermissionsOnTarget"
// // );
// // const display = Object.keys(permissions ?? {})
// // .filter((id) => id != me)
// // .filter((id) => !users?.find((u) => u._id?.$oid === id)?.admin);
// // if (!admin || !display.length) return null;
// // return (
// // <Section
// // title="Permissions"
// // icon={<Lock className="w-4 h-4" />}
// // actions={<NewPermission id={id} type={type} />}
// // >
// // <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
// // {display.map((user_id) => (
// // <Card key={user_id}>
// // <CardHeader className="justify-between">
// // <CardTitle className="flex items-center gap-2">
// // <User className="w-4 h-4" />
// // <Username user_id={user_id} />
// // </CardTitle>
// // <Select
// // value={permissions?.[user_id]}
// // onValueChange={(p: any) =>
// // update({
// // permission: p as Types.PermissionLevel,
// // user_id,
// // target: { type, id },
// // })
// // }
// // disabled={isPending}
// // >
// // <SelectTrigger className="w-32">
// // <SelectValue placeholder="Set Permissions" />
// // </SelectTrigger>
// // <SelectContent className="w-32">
// // <SelectGroup>
// // <SelectItem value={Types.PermissionLevel.Read}>
// // Read
// // </SelectItem>
// // <SelectItem value={Types.PermissionLevel.Update}>
// // Update
// // </SelectItem>
// // <SelectItem value={Types.PermissionLevel.Execute}>
// // Execute
// // </SelectItem>
// // </SelectGroup>
// // </SelectContent>
// // </Select>
// // </CardHeader>
// // </Card>
// // ))}
// // </div>
// // </Section>
// // );
// // };

View File

@@ -0,0 +1,56 @@
import { Config } from "@components/config";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { useState } from "react";
export const AlerterConfig = ({ id }: { id: string }) => {
const config = useRead("GetAlerter", { alerter: id }).data?.config;
if (config?.type === "Slack") return <SlackAlerterConfig id={id} />;
if (config?.type === "Custom") return <CustomAlerterConfig id={id} />;
};
const SlackAlerterConfig = ({ id }: { id: string }) => {
const config = useRead("GetAlerter", { alerter: id }).data?.config;
const [update, set] = useState<Partial<Types.SlackAlerterConfig>>({});
const { mutate } = useWrite("UpdateAlerter");
if (!config) return null;
return (
<Config
config={config.params}
update={update}
set={set}
onSave={() => mutate({ id, config: { type: "Slack", params: update } })}
components={{
general: {
general: {
url: true,
},
},
}}
/>
);
};
const CustomAlerterConfig = ({ id }: { id: string }) => {
const config = useRead("GetAlerter", { alerter: id }).data?.config;
const [update, set] = useState<Partial<Types.CustomAlerterConfig>>({});
const { mutate } = useWrite("UpdateAlerter");
if (!config) return null;
return (
<Config
config={config.params}
update={update}
set={set}
onSave={() => mutate({ id, config: { type: "Custom", params: update } })}
components={{
general: {
general: {
url: true,
},
},
}}
/>
);
};

View File

@@ -13,11 +13,11 @@ import { RequiredResourceComponents } from "@types";
import { Input } from "@ui/input"; import { Input } from "@ui/input";
import { AlarmClock } from "lucide-react"; import { AlarmClock } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { Config } from "@components/config";
import { DataTable } from "@ui/data-table"; import { DataTable } from "@ui/data-table";
import { ResourceComponents } from ".."; import { ResourceComponents } from "..";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card"; import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { AlerterConfig } from "./config";
const useAlerter = (id?: string) => const useAlerter = (id?: string) =>
useRead("ListAlerters", {}).data?.find((d) => d.id === id); useRead("ListAlerters", {}).data?.find((d) => d.id === id);
@@ -64,52 +64,6 @@ const NewAlerter = () => {
); );
}; };
const SlackAlerterConfig = ({ id }: { id: string }) => {
const config = useRead("GetAlerter", { alerter: id }).data?.config;
const [update, set] = useState<Partial<Types.SlackAlerterConfig>>({});
const { mutate } = useWrite("UpdateAlerter");
if (!config) return null;
return (
<Config
config={config.params}
update={update}
set={set}
onSave={() => mutate({ id, config: { type: "Slack", params: update } })}
components={{
general: {
general: {
url: true,
},
},
}}
/>
);
};
const CustomAlerterConfig = ({ id }: { id: string }) => {
const config = useRead("GetAlerter", { alerter: id }).data?.config;
const [update, set] = useState<Partial<Types.CustomAlerterConfig>>({});
const { mutate } = useWrite("UpdateAlerter");
if (!config) return null;
return (
<Config
config={config.params}
update={update}
set={set}
onSave={() => mutate({ id, config: { type: "Custom", params: update } })}
components={{
general: {
general: {
url: true,
},
},
}}
/>
);
};
const AlerterTable = () => { const AlerterTable = () => {
const alerters = useRead("ListAlerters", {}).data; const alerters = useRead("ListAlerters", {}).data;
return ( return (
@@ -157,12 +111,9 @@ export const AlerterComponents: RequiredResourceComponents = {
Icon: () => <AlarmClock className="w-4 h-4" />, Icon: () => <AlarmClock className="w-4 h-4" />,
Description: ({ id }) => <>{useAlerter(id)?.info.alerter_type} alerter</>, Description: ({ id }) => <>{useAlerter(id)?.info.alerter_type} alerter</>,
Info: ({ id }) => <>{id}</>, Info: ({ id }) => <>{id}</>,
Status: () => <></>,
Page: { Page: {
Config: ({ id }: { id: string }) => { Config: AlerterConfig,
const config = useRead("GetAlerter", { alerter: id }).data?.config;
if (config?.type === "Slack") return <SlackAlerterConfig id={id} />;
if (config?.type === "Custom") return <CustomAlerterConfig id={id} />;
},
}, },
Actions: () => null, Actions: () => null,
Table: AlerterTable, Table: AlerterTable,

View File

@@ -1,7 +1,6 @@
import { NewResource } from "@components/layouts"; import { NewResource } from "@components/layouts";
import { ConfirmButton } from "@components/util"; import { ConfirmButton } from "@components/util";
import { useExecute, useRead, useWrite } from "@lib/hooks"; import { useExecute, useRead, useWrite } from "@lib/hooks";
import { fmt_date_with_minutes, fmt_version } from "@lib/utils";
import { RequiredResourceComponents } from "@types"; import { RequiredResourceComponents } from "@types";
import { DataTable } from "@ui/data-table"; import { DataTable } from "@ui/data-table";
import { Input } from "@ui/input"; import { Input } from "@ui/input";
@@ -13,6 +12,8 @@ import { BuildChart } from "@components/dashboard/builds-chart";
import { TagsWithBadge, useTagsFilter } from "@components/tags"; import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { useToast } from "@ui/use-toast"; import { useToast } from "@ui/use-toast";
import { BuildConfig } from "./config"; import { BuildConfig } from "./config";
import { fmt_date_with_minutes, fmt_version } from "@lib/formatting";
import { fill_color_class_by_intention } from "@lib/color";
const useBuild = (id?: string) => const useBuild = (id?: string) =>
useRead("ListBuilds", {}).data?.find((d) => d.id === id); useRead("ListBuilds", {}).data?.find((d) => d.id === id);
@@ -43,7 +44,7 @@ const Name = ({ id }: { id: string }) => <>{useBuild(id)?.name}</>;
const Icon = ({ id }: { id: string }) => { const Icon = ({ id }: { id: string }) => {
const building = useRead("GetBuildActionState", { build: id }).data?.building; const building = useRead("GetBuildActionState", { build: id }).data?.building;
const className = building const className = building
? "w-4 h-4 animate-spin fill-green-500" ? "w-4 h-4 animate-spin " + fill_color_class_by_intention("Good")
: "w-4 h-4"; : "w-4 h-4";
return <Hammer className={className} />; return <Hammer className={className} />;
}; };
@@ -74,7 +75,7 @@ const BuildTable = () => {
}, },
{ {
header: "Repo", header: "Repo",
accessorKey: "info.repo" accessorKey: "info.repo",
}, },
{ {
header: "Version", header: "Version",

View File

@@ -136,6 +136,7 @@ export const BuilderComponents: RequiredResourceComponents = {
</> </>
), ),
Icon: () => <Factory className="w-4 h-4" />, Icon: () => <Factory className="w-4 h-4" />,
Status: () => <></>,
Page: { Page: {
Config: BuilderConfig, Config: BuilderConfig,
}, },

View File

@@ -25,7 +25,6 @@ export const RedeployContainer = ({ id }: DeploymentId) => {
return ( return (
<ConfirmButton <ConfirmButton
title={deployment?.info.status ? "Redeploy" : "Deploy"} title={deployment?.info.status ? "Redeploy" : "Deploy"}
// intent="success"
icon={<Rocket className="h-4 w-4" />} icon={<Rocket className="h-4 w-4" />}
onClick={() => mutate({ deployment: id })} onClick={() => mutate({ deployment: id })}
disabled={isPending} disabled={isPending}
@@ -120,7 +119,8 @@ export const DeleteDeployment = ({ id }: { id: string }) => {
const { data: d } = useRead("GetDeployment", { deployment: id }); const { data: d } = useRead("GetDeployment", { deployment: id });
const { mutateAsync, isPending } = useWrite("DeleteDeployment"); const { mutateAsync, isPending } = useWrite("DeleteDeployment");
const deleting = useRead("GetDeploymentActionState", { deployment: id }).data?.deleting; const deleting = useRead("GetDeploymentActionState", { deployment: id }).data
?.deleting;
if (!d) return null; if (!d) return null;
return ( return (

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { ConfigItem, ResourceSelector } from "@components/config/util"; import { ConfigItem, ResourceSelector } from "@components/config/util";
import { fmt_version } from "@lib/formatting";
import { useRead } from "@lib/hooks"; import { useRead } from "@lib/hooks";
import { fmt_version } from "@lib/utils";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { Input } from "@ui/input"; import { Input } from "@ui/input";
import { import {

View File

@@ -7,7 +7,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@ui/select"; } from "@ui/select";
import { keys } from "@lib/utils"; import { object_keys } from "@lib/utils";
const format_mode = (m: string) => m.split("-").join(" "); const format_mode = (m: string) => m.split("-").join(" ");
@@ -27,7 +27,7 @@ export const RestartModeSelector = ({
<SelectValue placeholder="Select Type" /> <SelectValue placeholder="Select Type" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{keys(Types.RestartMode).map((mode) => ( {object_keys(Types.RestartMode).map((mode) => (
<SelectItem <SelectItem
key={mode} key={mode}
value={Types.RestartMode[mode]} value={Types.RestartMode[mode]}

View File

@@ -2,7 +2,7 @@ import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types"; import { RequiredResourceComponents } from "@types";
import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react"; import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react";
import { cn, snake_case_to_upper_space_case } from "@lib/utils"; import { cn } from "@lib/utils";
import { useState } from "react"; import { useState } from "react";
import { NewResource, Section } from "@components/layouts"; import { NewResource, Section } from "@components/layouts";
@@ -23,33 +23,27 @@ import { ResourceComponents } from "..";
import { TagsWithBadge, useTagsFilter } from "@components/tags"; import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { DeploymentsChart } from "@components/dashboard/deployments-chart"; import { DeploymentsChart } from "@components/dashboard/deployments-chart";
import { Button } from "@ui/button"; import { Button } from "@ui/button";
import { snake_case_to_upper_space_case } from "@lib/formatting";
import {
deployment_state_intention,
fill_color_class_by_intention,
text_color_class_by_intention,
} from "@lib/color";
export const useDeployment = (id?: string) => export const useDeployment = (id?: string) =>
useRead("ListDeployments", {}, { refetchInterval: 5000 }).data?.find( useRead("ListDeployments", {}, { refetchInterval: 5000 }).data?.find(
(d) => d.id === id (d) => d.id === id
); );
const deployment_state_color = (state: Types.DockerContainerState) => {
if (state === Types.DockerContainerState.Running) return "green-500";
if (state === Types.DockerContainerState.Paused) return "orange-500";
if (state === Types.DockerContainerState.NotDeployed) return "blue-500";
return "red-500";
};
const deployment_state_fill_color = (state: Types.DockerContainerState) => {
return `fill-${deployment_state_color(state)}`;
};
const deployment_state_text_color = (state: Types.DockerContainerState) => {
return `text-${deployment_state_color(state)}`;
};
const Icon = ({ id }: { id?: string }) => { const Icon = ({ id }: { id?: string }) => {
const state = useDeployment(id)?.info.state; const state = useDeployment(id)?.info.state;
return ( return (
<Rocket <Rocket
className={cn("w-4", state && deployment_state_fill_color(state))} className={cn(
"w-4",
fill_color_class_by_intention(deployment_state_intention(state))
)}
/> />
); );
}; };
@@ -161,7 +155,9 @@ export const DeploymentTable = ({
header: "State", header: "State",
cell: ({ row }) => { cell: ({ row }) => {
const state = row.original.info.state; const state = row.original.info.state;
const color = deployment_state_text_color(state); const color = text_color_class_by_intention(
deployment_state_intention(state)
);
return ( return (
<div className={color}> <div className={color}>
{snake_case_to_upper_space_case(state)} {snake_case_to_upper_space_case(state)}
@@ -192,11 +188,10 @@ export const DeploymentComponents: RequiredResourceComponents = {
Status: ({ id }) => { Status: ({ id }) => {
const state = const state =
useDeployment(id)?.info.state ?? Types.DockerContainerState.Unknown; useDeployment(id)?.info.state ?? Types.DockerContainerState.Unknown;
return ( const color = text_color_class_by_intention(
<div className={deployment_state_text_color(state)}> deployment_state_intention(state)
{snake_case_to_upper_space_case(state)}
</div>
); );
return <div className={color}>{snake_case_to_upper_space_case(state)}</div>;
}, },
Actions: ({ id }) => ( Actions: ({ id }) => (
<div className="flex gap-4"> <div className="flex gap-4">

View File

@@ -27,6 +27,7 @@ export const ProcedureComponents: RequiredResourceComponents = {
Description: ({ id }) => <>{useProcedure(id)?.info.procedure_type}</>, Description: ({ id }) => <>{useProcedure(id)?.info.procedure_type}</>,
Info: () => <></>, Info: () => <></>,
Icon: () => <Route className="w-4" />, Icon: () => <Route className="w-4" />,
Status: () => <></>,
Page: { Page: {
Config: ProcedureConfig, Config: ProcedureConfig,
}, },

View File

@@ -1,450 +0,0 @@
// import { ResourceSelector } from "@components/config/util";
// import { NewResource } from "@components/layouts";
// import { ConfirmButton } from "@components/util";
// import { useExecute, useRead, useWrite } from "@lib/hooks";
// import { fmt_date_with_minutes } from "@lib/utils";
// import { Types } from "@monitor/client";
// import { Execution } from "@monitor/client/dist/types";
// import { Icon } from "@radix-ui/react-select";
// import { RequiredResourceComponents, UsableResource } from "@types";
// import { Button } from "@ui/button";
// import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
// import { DataTable } from "@ui/data-table";
// import { Input } from "@ui/input";
// import {
// Select,
// SelectContent,
// SelectGroup,
// SelectItem,
// SelectTrigger,
// SelectValue,
// } from "@ui/select";
// import { Loader2, Route, Save } from "lucide-react";
// import React, { useEffect, useState } from "react";
// import { Link } from "react-router-dom";
// const useProcedure = (id?: string) =>
// useRead("ListProcedures", {}).data?.find((d) => d.id === id);
// const Name = ({ id }: { id: string }) => <>{useProcedure(id)?.name}</>;
// // const NewProcedure = ({ parent }: { parent?: Types.Procedure }) => {
// // const [name, setName] = useState("");
// // const [type, setType] = useState<Types.ProcedureConfig["type"]>("Execution");
// // const update_parent = useWrite("UpdateProcedure").mutate;
// // const { mutateAsync } = useWrite("CreateProcedure", {
// // onSuccess: ({ _id }) => {
// // if (!parent?._id?.$oid || !_id?.$oid) return;
// // if (
// // parent.config.type === "Sequence" ||
// // parent.config.type === "Parallel"
// // ) {
// // update_parent({
// // id: parent._id.$oid,
// // config: {
// // ...parent.config,
// // data: [...parent.config.data, { enabled: true, id: _id!.$oid }],
// // },
// // });
// // }
// // },
// // });
// // return (
// // <NewResource
// // type="Procedure"
// // onSuccess={() =>
// // mutateAsync({
// // name,
// // config: {
// // type,
// // data: get_default_data(type),
// // } as Types.ProcedureConfig,
// // })
// // }
// // enabled={!!name}
// // >
// // <div className="grid md:grid-cols-2">
// // Procedure Name
// // <Input
// // placeholder="procedure-name"
// // value={name}
// // onChange={(e) => setName(e.target.value)}
// // />
// // </div>
// // <div className="grid md:grid-cols-2">
// // Procedure Type
// // <Select
// // value={type}
// // onValueChange={(value) => setType(value as typeof type)}
// // >
// // <SelectTrigger>
// // <SelectValue placeholder="Select Type" />
// // </SelectTrigger>
// // <SelectContent>
// // <SelectGroup>
// // <SelectItem value="Execution">Execution</SelectItem>
// // <SelectItem value="Sequence">Sequence</SelectItem>
// // <SelectItem value="Paralell">Paralell</SelectItem>
// // </SelectGroup>
// // </SelectContent>
// // </Select>
// // </div>
// // </NewResource>
// // );
// // };
// // type ExecutionType = Extract<
// // Types.ProcedureConfig,
// // { type: "Execution" }
// // >["data"]["type"];
// // type ExecutionConfigComponent<
// // T extends ExecutionType,
// // P = Extract<Execution, { type: T }>["params"]
// // > = React.FC<{
// // params: P;
// // setParams: React.Dispatch<React.SetStateAction<P>>;
// // }>;
// // type ExecutionConfigParams<T extends ExecutionType> = Extract<
// // Execution,
// // { type: T }
// // >["params"];
// // type ExecutionConfigs = {
// // [ExType in ExecutionType]: {
// // component: ExecutionConfigComponent<ExType>;
// // params: ExecutionConfigParams<ExType>;
// // };
// // };
// // const TypeSelector = ({
// // type,
// // selected,
// // onSelect,
// // }: {
// // type: UsableResource;
// // selected: string;
// // onSelect: (value: string) => void;
// // }) => (
// // <div className="flex items-center justify-between">
// // {type}
// // <ResourceSelector type={type} selected={selected} onSelect={onSelect} />
// // </div>
// // );
// // const EXEC_TYPES: ExecutionConfigs = {
// // None: {
// // params: {},
// // component: () => <></>,
// // },
// // CloneRepo: {
// // params: { id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Repo"
// // selected={params.id}
// // onSelect={(id) => setParams((p) => ({ ...p, id }))}
// // />
// // ),
// // },
// // Deploy: {
// // params: { deployment_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Deployment"
// // selected={params.deployment_id}
// // onSelect={(id) => setParams((p) => ({ ...p, deployment_id: id }))}
// // />
// // ),
// // },
// // PruneDockerContainers: {
// // params: { server_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Server"
// // selected={params.server_id}
// // onSelect={(server_id) => setParams((p) => ({ ...p, server_id }))}
// // />
// // ),
// // },
// // PruneDockerImages: {
// // params: { server_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Server"
// // selected={params.server_id}
// // onSelect={(id) => setParams((p) => ({ ...p, id }))}
// // />
// // ),
// // },
// // PruneDockerNetworks: {
// // params: { server_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Server"
// // selected={params.server_id}
// // onSelect={(id) => setParams((p) => ({ ...p, id }))}
// // />
// // ),
// // },
// // PullRepo: {
// // params: { id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Repo"
// // selected={params.id}
// // onSelect={(id) => setParams((p) => ({ ...p, id }))}
// // />
// // ),
// // },
// // RemoveContainer: {
// // params: { deployment_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Deployment"
// // selected={params.deployment_id}
// // onSelect={(id) => setParams((p) => ({ ...p, deployment_id: id }))}
// // />
// // ),
// // },
// // RunBuild: {
// // params: { build_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Build"
// // selected={params.build_id}
// // onSelect={(build_id) => setParams((p) => ({ ...p, build_id }))}
// // />
// // ),
// // },
// // RunProcedure: {
// // params: { procedure_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Procedure"
// // selected={params.procedure_id}
// // onSelect={(id) => setParams((p) => ({ ...p, procedure_id: id }))}
// // />
// // ),
// // },
// // StartContainer: {
// // params: { deployment_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Deployment"
// // selected={params.deployment_id}
// // onSelect={(id) => setParams((p) => ({ ...p, deployment_id: id }))}
// // />
// // ),
// // },
// // StopAllContainers: {
// // params: { server_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Server"
// // selected={params.server_id}
// // onSelect={(id) => setParams((p) => ({ ...p, server_id: id }))}
// // />
// // ),
// // },
// // StopContainer: {
// // params: { deployment_id: "" },
// // component: ({ params, setParams }) => (
// // <TypeSelector
// // type="Deployment"
// // selected={params.deployment_id}
// // onSelect={(id) => setParams((p) => ({ ...p, deployment_id: id }))}
// // />
// // ),
// // },
// // };
// const UpdateProcedure = ({
// id,
// procedure,
// }: {
// id: string;
// procedure: Types.ProcedureConfig;
// }) => {
// const { mutate } = useWrite("UpdateProcedure");
// return (
// <Button onClick={() => mutate({ id, config: procedure })}>
// <Save className="w-4" />
// </Button>
// );
// };
// // const ExecutionConfig = ({ id }: { id: string }) => {
// // const procedure = useRead("GetProcedure", { id }).data;
// // if (procedure?.config.type !== "Execution") return null;
// // // eslint-disable-next-line react-hooks/rules-of-hooks
// // const [type, setType] = useState<ExecutionType>(procedure.config.data.type);
// // // eslint-disable-next-line react-hooks/rules-of-hooks
// // const [params, setParams] = useState(procedure.config.data.params);
// // // eslint-disable-next-line react-hooks/rules-of-hooks
// // useEffect(() => {
// // if (procedure?.config.type !== "Execution") return;
// // if (type !== procedure.config.data.type) {
// // setParams(EXEC_TYPES[type].params);
// // }
// // }, [procedure, type]);
// // const Component = EXEC_TYPES[type].component;
// // return (
// // <div className="p-4 border rounded-md flex flex-col gap-4">
// // <div className="flex items-center justify-between">
// // {procedure.name}
// // <UpdateProcedure
// // id={id}
// // procedure={{ type: "Execution", data: { type, params } as Execution }}
// // />
// // </div>
// // <div className="flex items-center justify-between">
// // Execution Type
// // <Select
// // value={type}
// // onValueChange={(value) => setType(value as typeof type)}
// // >
// // <SelectTrigger className="w-72">
// // <SelectValue placeholder="Select Type" />
// // </SelectTrigger>
// // <SelectContent className="w-72">
// // <SelectGroup>
// // {Object.keys(EXEC_TYPES).map((type) => (
// // <SelectItem
// // value={type}
// // className="whitespace-nowrap"
// // key={type}
// // >
// // {type.match(/[A-Z][a-z]+/g)?.join(" ")}
// // </SelectItem>
// // ))}
// // </SelectGroup>
// // </SelectContent>
// // </Select>
// // </div>
// // <div className="pt-2 border-t">
// // {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
// // <Component params={params as any} setParams={setParams as any} />
// // </div>
// // <div className="pt-2 border-t">
// // <pre>{JSON.stringify(procedure?.config, null, 2)}</pre>
// // </div>
// // </div>
// // );
// // };
// // const SequenceConfig = ({ id }: { id: string }) => {
// // const procedure = useRead("GetProcedure", { id }).data;
// // if (procedure?.config.type !== "Sequence") return null;
// // return (
// // <div className="p-4 border rounded-md flex flex-col gap-4">
// // <div className="flex items-center justify-between">
// // {procedure?.name}
// // <NewProcedure parent={procedure} />
// // </div>
// // <pre>{JSON.stringify(procedure?.config, null, 2)}</pre>
// // <div>
// // {procedure.config.data.map((p) => (
// // <ProcedureConfig id={p.id} key={p.id} />
// // ))}
// // </div>
// // </div>
// // );
// // };
// // export const ProcedureConfig = ({ id }: { id: string }) => {
// // const procedure = useRead("GetProcedure", { id }).data;
// // if (procedure?.config.type === "Sequence") return <SequenceConfig id={id} />;
// // if (procedure?.config.type === "Execution")
// // return <ExecutionConfig id={id} />;
// // };
// export const ProcedureDashboard = () => {
// const procedure_count = useRead("ListProcedures", {}).data?.length;
// return (
// <Link to="/procedures/" className="w-full">
// <Card>
// <CardHeader className="justify-between">
// <div>
// <CardTitle>Procedures</CardTitle>
// <CardDescription>{procedure_count} Total</CardDescription>
// </div>
// <Route className="w-4 h-4" />
// </CardHeader>
// </Card>
// </Link>
// );
// };
// export const ProcedureComponents: RequiredResourceComponents = {
// Name: ({ id }) => <>{useProcedure(id)?.name}</>,
// Description: ({ id }) => <>{useProcedure(id)?.info.procedure_type}</>,
// Info: ({ id }) => <>{id}</>,
// Icon: () => <Route className="w-4" />,
// Page: {
// // Config: ({ id }) => <ProcedureConfig id={id} />,
// },
// Actions: ({ id }) => {
// const running = useRead("GetProcedureActionState", { procedure: id }).data?.running;
// const { mutate, isPending } = useExecute("RunProcedure");
// return (
// <ConfirmButton
// title={running ? "Building" : "Run"}
// icon={
// running ? (
// <Loader2 className="w-4 h-4 animate-spin" />
// ) : (
// <Route className="h-4 w-4" />
// )
// }
// onClick={() => mutate({ procedure: id })}
// disabled={running || isPending}
// />
// );
// },
// Table: () => {
// const alerters = useRead("ListAlerters", {}).data;
// return (
// <DataTable
// data={alerters ?? []}
// columns={[
// {
// accessorKey: "id",
// header: "Name",
// cell: ({ row }) => {
// const id = row.original.id;
// return (
// <Link
// to={`/procedures/${id}`}
// className="flex items-center gap-2"
// >
// <Icon id={id} />
// <Name id={id} />
// </Link>
// );
// },
// },
// { header: "Tags", accessorFn: ({ tags }) => tags.join(", ") },
// {
// header: "Created",
// accessorFn: ({ created_at }) =>
// fmt_date_with_minutes(new Date(created_at)),
// },
// ]}
// />
// );
// },
// New: () => <NewProcedure />,
// Dashboard: ProcedureDashboard,
// };

View File

@@ -0,0 +1,46 @@
import { Config } from "@components/config";
import { AccountSelector, ResourceSelector } from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { useState } from "react";
export const RepoConfig = ({ id }: { id: string }) => {
const config = useRead("GetRepo", { repo: id }).data?.config;
const [update, set] = useState<Partial<Types.RepoConfig>>({});
const mutate = useWrite("UpdateRepo");
if (!config) return null;
return (
<Config
config={config}
update={update}
set={set}
onSave={() => mutate}
components={{
general: {
general: {
server_id: (selected, set) => (
<ResourceSelector
type="Server"
selected={selected}
onSelect={(server_id) => set({ server_id })}
/>
),
github_account: (value, set) => (
<AccountSelector
type="Server"
account_type="github"
id={update.server_id ?? config.server_id}
selected={value}
onSelect={(github_account) => set({ github_account })}
/>
),
repo: true,
branch: true,
on_pull: true,
on_clone: true,
},
},
}}
/>
);
};

View File

@@ -1,15 +1,12 @@
import { Config } from "@components/config";
import { AccountSelector, ResourceSelector } from "@components/config/util";
import { TagsWithBadge } from "@components/tags"; import { TagsWithBadge } from "@components/tags";
import { useRead, useWrite } from "@lib/hooks"; import { useRead } from "@lib/hooks";
import { Types } from "@monitor/client";
import { Icon } from "@radix-ui/react-select"; import { Icon } from "@radix-ui/react-select";
import { RequiredResourceComponents } from "@types"; import { RequiredResourceComponents } from "@types";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card"; import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { DataTable } from "@ui/data-table"; import { DataTable } from "@ui/data-table";
import { GitBranch } from "lucide-react"; import { GitBranch } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { RepoConfig } from "./config";
const useRepo = (id?: string) => const useRepo = (id?: string) =>
useRead("ListRepos", {}).data?.find((d) => d.id === id); useRead("ListRepos", {}).data?.find((d) => d.id === id);
@@ -38,47 +35,9 @@ export const RepoComponents: RequiredResourceComponents = {
Description: ({ id }) => <>{id}</>, Description: ({ id }) => <>{id}</>,
Info: ({ id }) => <>{id}</>, Info: ({ id }) => <>{id}</>,
Icon: () => <GitBranch className="w-4 h-4" />, Icon: () => <GitBranch className="w-4 h-4" />,
Status: () => <></>,
Page: { Page: {
Config: ({ id }) => { Config: RepoConfig,
const config = useRead("GetRepo", { repo: id }).data?.config;
const [update, set] = useState<Partial<Types.RepoConfig>>({});
const mutate = useWrite("UpdateRepo");
if (!config) return null;
return (
<Config
config={config}
update={update}
set={set}
onSave={() => mutate}
components={{
general: {
general: {
server_id: (selected, set) => (
<ResourceSelector
type="Server"
selected={selected}
onSelect={(server_id) => set({ server_id })}
/>
),
github_account: (value, set) => (
<AccountSelector
type="Server"
account_type="github"
id={update.server_id ?? config.server_id}
selected={value}
onSelect={(github_account) => set({ github_account })}
/>
),
repo: true,
branch: true,
on_pull: true,
on_clone: true,
},
},
}}
/>
);
},
}, },
Table: () => { Table: () => {
const alerters = useRead("ListAlerters", {}).data; const alerters = useRead("ListAlerters", {}).data;

View File

@@ -0,0 +1,50 @@
import { Config } from "@components/config";
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { useState } from "react";
export const ServerConfig = ({ id }: { id: string }) => {
const invalidate = useInvalidate();
const config = useRead("GetServer", { server: id }).data?.config;
const [update, set] = useState<Partial<Types.ServerConfig>>({});
const { mutate } = useWrite("UpdateServer", {
onSuccess: () => {
// In case of disabling to resolve unreachable alert
invalidate(["ListAlerts"]);
},
});
if (!config) return null;
return (
<Config
config={config}
update={update}
set={set}
onSave={() => mutate({ id, config: update })}
components={{
general: {
general: {
address: true,
region: true,
enabled: true,
auto_prune: true,
},
},
warnings: {
cpu: {
cpu_warning: true,
cpu_critical: true,
},
memory: {
mem_warning: true,
mem_critical: true,
},
disk: {
disk_warning: true,
disk_critical: true,
},
},
}}
/>
);
};

View File

@@ -1,4 +1,4 @@
import { useInvalidate, useRead, useWrite } from "@lib/hooks"; import { useRead, useWrite } from "@lib/hooks";
import { cn } from "@lib/utils"; import { cn } from "@lib/utils";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types"; import { RequiredResourceComponents } from "@types";
@@ -12,7 +12,6 @@ import {
Rocket, Rocket,
} from "lucide-react"; } from "lucide-react";
import { ServerStats } from "./stats"; import { ServerStats } from "./stats";
import { Config } from "@components/config";
import { useState } from "react"; import { useState } from "react";
import { NewResource, Section } from "@components/layouts"; import { NewResource, Section } from "@components/layouts";
import { Input } from "@ui/input"; import { Input } from "@ui/input";
@@ -23,6 +22,12 @@ import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { DeleteServer, RenameServer } from "./actions"; import { DeleteServer, RenameServer } from "./actions";
import { ServersChart } from "@components/dashboard/servers-chart"; import { ServersChart } from "@components/dashboard/servers-chart";
import { DeploymentTable } from "../deployment"; import { DeploymentTable } from "../deployment";
import {
fill_color_class_by_intention,
server_status_intention,
text_color_class_by_intention,
} from "@lib/color";
import { ServerConfig } from "./config";
export const useServer = (id?: string) => export const useServer = (id?: string) =>
useRead("ListServers", {}).data?.find((d) => d.id === id); useRead("ListServers", {}).data?.find((d) => d.id === id);
@@ -76,57 +81,12 @@ export const ServerInfo = ({
export const ServerIconComponent = ({ id }: { id?: string }) => { export const ServerIconComponent = ({ id }: { id?: string }) => {
const status = useServer(id)?.info.status; const status = useServer(id)?.info.status;
const color = () => {
if (status === Types.ServerStatus.Ok) return "fill-green-500";
if (status === Types.ServerStatus.NotOk) return "fill-red-500";
if (status === Types.ServerStatus.Disabled) return "fill-blue-500";
};
return <ServerIcon className={cn("w-4 h-4", id && color())} />;
};
const ServerConfig = ({ id }: { id: string }) => {
const invalidate = useInvalidate();
const config = useRead("GetServer", { server: id }).data?.config;
const [update, set] = useState<Partial<Types.ServerConfig>>({});
const { mutate } = useWrite("UpdateServer", {
onSuccess: () => {
// In case of disabling to resolve unreachable alert
invalidate(["ListAlerts"]);
},
});
if (!config) return null;
return ( return (
<Config <ServerIcon
config={config} className={cn(
update={update} "w-4 h-4",
set={set} id && fill_color_class_by_intention(server_status_intention(status))
onSave={() => mutate({ id, config: update })} )}
components={{
general: {
general: {
address: true,
region: true,
enabled: true,
auto_prune: true,
},
},
warnings: {
cpu: {
cpu_warning: true,
cpu_critical: true,
},
memory: {
mem_warning: true,
mem_critical: true,
},
disk: {
disk_warning: true,
disk_critical: true,
},
},
}}
/> />
); );
}; };
@@ -229,12 +189,9 @@ export const ServerComponents: RequiredResourceComponents = {
Icon: ServerIconComponent, Icon: ServerIconComponent,
Status: ({ id }) => { Status: ({ id }) => {
const status = useServer(id)?.info.status; const status = useServer(id)?.info.status;
const stateClass = const stateClass = text_color_class_by_intention(
status === Types.ServerStatus.Ok server_status_intention(status)
? "text-green-500" );
: status === Types.ServerStatus.NotOk
? "text-red-500"
: "text-blue-500";
return ( return (
<div className={stateClass}> <div className={stateClass}>
{status === Types.ServerStatus.NotOk ? "Not Ok" : status} {status === Types.ServerStatus.NotOk ? "Not Ok" : status}
@@ -253,7 +210,7 @@ export const ServerComponents: RequiredResourceComponents = {
</Section> </Section>
); );
}, },
Config: ({ id }) => <ServerConfig id={id} />, Config: ServerConfig,
Danger: ({ id }) => ( Danger: ({ id }) => (
<Section title="Danger Zone" icon={<AlertTriangle className="w-4 h-4" />}> <Section title="Danger Zone" icon={<AlertTriangle className="w-4 h-4" />}>
<RenameServer id={id} /> <RenameServer id={id} />

80
frontend/src/lib/color.ts Normal file
View File

@@ -0,0 +1,80 @@
import { Types } from "@monitor/client";
export type ColorIntention =
| "Good"
| "Neutral"
| "Warning"
| "Critical"
| "Unknown";
export const hex_color_by_intention = (intention: ColorIntention) => {
switch (intention) {
case "Good":
return "#22C55E";
case "Neutral":
return "#3B82F6";
case "Warning":
return "#F97316";
case "Critical":
return "#EF0044";
case "Unknown":
return "#A855F7";
}
};
export const color_class_by_intention = (intention: ColorIntention) => {
switch (intention) {
case "Good":
return "green-500";
case "Neutral":
return "blue-500";
case "Warning":
return "orange-500";
case "Critical":
return "red-500";
case "Unknown":
return "purple-500";
}
};
export const text_color_class_by_intention = (intention: ColorIntention) => {
return `text-${color_class_by_intention(intention)}`;
};
export const fill_color_class_by_intention = (intention: ColorIntention) => {
return `fill-${color_class_by_intention(intention)}`;
};
export const stroke_color_class_by_intention = (intention: ColorIntention) => {
return `stroke-${color_class_by_intention(intention)}`;
};
export const server_status_intention: (
status?: Types.ServerStatus
) => ColorIntention = (status) => {
switch (status) {
case Types.ServerStatus.Ok:
return "Good";
case Types.ServerStatus.NotOk:
return "Critical";
case Types.ServerStatus.Disabled:
return "Neutral";
default:
return "Unknown";
}
};
export const deployment_state_intention: (
state?: Types.DockerContainerState
) => ColorIntention = (state) => {
switch (state) {
case Types.DockerContainerState.Running:
return "Good";
case Types.DockerContainerState.NotDeployed:
return "Neutral";
case Types.DockerContainerState.Unknown || undefined:
return "Unknown";
default:
return "Critical";
}
};

View File

@@ -0,0 +1,39 @@
import { Types } from "@monitor/client";
export const fmt_date = (d: Date) => {
return `${d.getDate()}/${
d.getMonth() + 1
} @ ${d.getHours()}:${d.getMinutes()}`;
};
export const fmt_date_with_minutes = (d: Date) => {
// return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`;
return d.toLocaleString();
};
export const fmt_version = (version: Types.Version | undefined) => {
if (!version) return "...";
const { major, minor, patch } = version;
if (major === 0 && minor === 0 && patch === 0) return "latest";
return `v${major}.${minor}.${patch}`;
};
export const fmt_duration = (start_ts: number, end_ts: number) => {
const start = new Date(start_ts);
const end = new Date(end_ts);
const durr = end.getTime() - start.getTime();
const seconds = durr / 1000;
const minutes = Math.floor(seconds / 60);
const remaining_seconds = seconds % 60;
return `${
minutes > 0 ? `${minutes} minute${minutes > 1 ? "s" : ""} ` : ""
}${remaining_seconds.toFixed(minutes > 0 ? 0 : 1)} seconds`;
};
/// list_all_items => List All Items
export function snake_case_to_upper_space_case(snake: string) {
return snake
.split("_")
.map((item) => item[0].toUpperCase() + item.slice(1))
.join(" ");
}

View File

@@ -5,11 +5,11 @@ import { toast } from "@ui/use-toast";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { Circle } from "lucide-react"; import { Circle } from "lucide-react";
import { ReactNode, useCallback, useEffect } from "react"; import { ReactNode, useCallback, useEffect } from "react";
import rws from "reconnecting-websocket"; import Rws from "reconnecting-websocket";
import { cn } from "@lib/utils"; import { cn } from "@lib/utils";
import { AUTH_TOKEN_STORAGE_KEY } from "@main"; import { AUTH_TOKEN_STORAGE_KEY } from "@main";
const rws_atom = atom<rws | null>(null); const rws_atom = atom<Rws | null>(null);
const useWebsocket = () => useAtom(rws_atom); const useWebsocket = () => useAtom(rws_atom);
const on_message = ( const on_message = (
@@ -92,7 +92,7 @@ const on_message = (
} }
}; };
const on_open = (ws: rws | null) => { const on_open = (ws: Rws | null) => {
const jwt = localStorage.getItem(AUTH_TOKEN_STORAGE_KEY); const jwt = localStorage.getItem(AUTH_TOKEN_STORAGE_KEY);
if (!ws || !jwt) return; if (!ws || !jwt) return;
const msg: Types.WsLoginMessage = { type: "Jwt", params: { jwt } }; const msg: Types.WsLoginMessage = { type: "Jwt", params: { jwt } };
@@ -116,7 +116,7 @@ export const WebsocketProvider = ({
); );
useEffect(() => { useEffect(() => {
if (!ws) set(new rws(url)); if (!ws) set(new Rws(url));
return () => { return () => {
ws?.close(); ws?.close();
}; };

View File

@@ -7,7 +7,7 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export const keys = <T extends object>(o: T): (keyof T)[] => export const object_keys = <T extends object>(o: T): (keyof T)[] =>
Object.keys(o) as (keyof T)[]; Object.keys(o) as (keyof T)[];
export const RESOURCE_TARGETS: UsableResource[] = [ export const RESOURCE_TARGETS: UsableResource[] = [
@@ -20,36 +20,6 @@ export const RESOURCE_TARGETS: UsableResource[] = [
"Alerter", "Alerter",
]; ];
export const fmt_date = (d: Date) => {
return `${d.getDate()}/${
d.getMonth() + 1
} @ ${d.getHours()}:${d.getMinutes()}`;
};
export const fmt_date_with_minutes = (d: Date) => {
// return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`;
return d.toLocaleString();
};
export const fmt_version = (version: Types.Version | undefined) => {
if (!version) return "...";
const { major, minor, patch } = version;
if (major === 0 && minor === 0 && patch === 0) return "latest";
return `v${major}.${minor}.${patch}`;
};
export const fmt_duration = (start_ts: number, end_ts: number) => {
const start = new Date(start_ts);
const end = new Date(end_ts);
const durr = end.getTime() - start.getTime();
const seconds = durr / 1000;
const minutes = Math.floor(seconds / 60);
const remaining_seconds = seconds % 60;
return `${
minutes > 0 ? `${minutes} minute${minutes > 1 ? "s" : ""} ` : ""
}${remaining_seconds.toFixed(minutes > 0 ? 0 : 1)} seconds`;
};
export function env_to_text(envVars: Types.EnvironmentVar[] | undefined) { export function env_to_text(envVars: Types.EnvironmentVar[] | undefined) {
return envVars?.reduce( return envVars?.reduce(
(prev, { variable, value }) => (prev, { variable, value }) =>
@@ -58,6 +28,17 @@ export function env_to_text(envVars: Types.EnvironmentVar[] | undefined) {
); );
} }
export function text_to_env(env: string): Types.EnvironmentVar[] {
return env
.split("\n")
.filter((line) => keep_line(line))
.map((entry) => {
const [first, ...rest] = entry.replaceAll('"', "").split("=");
return [first, rest.join("=")];
})
.map(([variable, value]) => ({ variable, value }));
}
function keep_line(line: string) { function keep_line(line: string) {
if (line.length === 0) return false; if (line.length === 0) return false;
let firstIndex = -1; let firstIndex = -1;
@@ -71,22 +52,3 @@ function keep_line(line: string) {
if (line[firstIndex] === "#") return false; if (line[firstIndex] === "#") return false;
return true; return true;
} }
export function text_to_env(env: string): Types.EnvironmentVar[] {
return env
.split("\n")
.filter((line) => keep_line(line))
.map((entry) => {
const [first, ...rest] = entry.replaceAll('"', "").split("=");
return [first, rest.join("=")];
})
.map(([variable, value]) => ({ variable, value }));
}
/// list_all_items => List All Items
export function snake_case_to_upper_space_case(snake: string) {
return snake
.split("_")
.map((item) => item[0].toUpperCase() + item.slice(1))
.join(" ");
}

View File

@@ -1,5 +1,6 @@
import { Page, Section } from "@components/layouts"; import { Page, Section } from "@components/layouts";
import { ConfirmButton, ResourceLink } from "@components/util"; import { ConfirmButton, ResourceLink } from "@components/util";
import { text_color_class_by_intention } from "@lib/color";
import { useInvalidate, useRead, useWrite } from "@lib/hooks"; import { useInvalidate, useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { UsableResource } from "@types"; import { UsableResource } from "@types";
@@ -37,8 +38,8 @@ export const UsersPage = () => {
header: "Enabled", header: "Enabled",
cell: ({ row }) => { cell: ({ row }) => {
const enabledClass = row.original.enabled const enabledClass = row.original.enabled
? "text-green-500" ? text_color_class_by_intention("Good")
: "text-red-500"; : text_color_class_by_intention("Critical");
return ( return (
<div className={enabledClass}> <div className={enabledClass}>
{row.original.enabled ? "Enabled" : "Disabled"} {row.original.enabled ? "Enabled" : "Disabled"}