improve info

This commit is contained in:
mbecker20
2024-03-31 12:04:36 -07:00
parent 8c04cf3db2
commit cf054395bb
22 changed files with 736 additions and 730 deletions

View File

@@ -102,10 +102,7 @@ export const NewResource = ({
return (
<Dialog open={open} onOpenChange={set}>
<DialogTrigger asChild>
<Button
variant="secondary"
className="items-center gap-2"
>
<Button variant="secondary" className="items-center gap-2">
New {entityType} <PlusCircle className="w-4 h-4" />
</Button>
</DialogTrigger>
@@ -158,7 +155,9 @@ export const ResourceCard = ({
<Components.Icon id={id} />
</CardHeader>
<CardContent className="text-sm text-muted-foreground">
<Components.Info id={id} />
{Components.Info.map((Info) => (
<Info id={id} />
))}
</CardContent>
<CardFooter className="flex items-center gap-2">
<ResourceTags target={{ type, id }} />
@@ -185,7 +184,9 @@ export const ResourceRow = ({
<CardTitle>
<Components.Name id={id} />
</CardTitle>
<Components.Info id={id} />
{Components.Info.map((Info) => (
<Info id={id} />
))}
<div className="flex items-center gap-2">
<Components.Icon id={id} />
<CardDescription>

View File

@@ -22,101 +22,98 @@ import { AlerterConfig } from "./config";
const useAlerter = (id?: string) =>
useRead("ListAlerters", {}).data?.find((d) => d.id === id);
const NewAlerter = () => {
const { mutateAsync } = useWrite("CreateAlerter");
const [name, setName] = useState("");
const [type, setType] = useState<Types.AlerterConfig["type"]>();
return (
<NewResource
entityType="Alerter"
onSuccess={async () =>
!!type && mutateAsync({ name, config: { type, params: {} } })
}
enabled={!!name && !!type}
>
<div className="grid md:grid-cols-2">
Name
<Input
placeholder="alerter-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid md:grid-cols-2">
Alerter Type
<Select
value={type}
onValueChange={(value) => setType(value as typeof type)}
>
<SelectTrigger>
<SelectValue placeholder="Select Type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Slack">Slack</SelectItem>
<SelectItem value="Custom">Custom</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</NewResource>
);
};
const AlerterTable = () => {
const alerters = useRead("ListAlerters", {}).data;
return (
<DataTable
data={alerters ?? []}
columns={[
{
accessorKey: "id",
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
return (
<Link to={`/alerters/${id}`} className="flex items-center gap-2">
<ResourceComponents.Alerter.Icon id={id} />
<ResourceComponents.Alerter.Name id={id} />
</Link>
);
},
},
{ header: "Tags", accessorFn: ({ tags }) => tags.join(", ") },
]}
/>
);
};
export const AlerterDashboard = () => {
const alerters_count = useRead("ListAlerters", {}).data?.length;
return (
<Link to="/alerters/" className="w-full">
<Card>
<CardHeader className="justify-between">
<div>
<CardTitle>Alerters</CardTitle>
<CardDescription>{alerters_count} Total</CardDescription>
</div>
<AlarmClock className="w-4 h-4" />
</CardHeader>
</Card>
</Link>
);
};
export const AlerterComponents: RequiredResourceComponents = {
Name: ({ id }: { id: string }) => <>{useAlerter(id)?.name}</>,
Icon: () => <AlarmClock className="w-4 h-4" />,
Description: ({ id }) => <>{useAlerter(id)?.info.alerter_type} alerter</>,
Info: ({ id }) => <>{id}</>,
Info: [],
Status: () => <></>,
Page: {
Config: AlerterConfig,
},
Actions: () => null,
Table: AlerterTable,
New: () => <NewAlerter />,
Dashboard: AlerterDashboard,
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={`/alerters/${id}`}
className="flex items-center gap-2"
>
<ResourceComponents.Alerter.Icon id={id} />
<ResourceComponents.Alerter.Name id={id} />
</Link>
);
},
},
{ header: "Tags", accessorFn: ({ tags }) => tags.join(", ") },
]}
/>
);
},
Dashboard: () => {
const alerters_count = useRead("ListAlerters", {}).data?.length;
return (
<Link to="/alerters/" className="w-full">
<Card>
<CardHeader className="justify-between">
<div>
<CardTitle>Alerters</CardTitle>
<CardDescription>{alerters_count} Total</CardDescription>
</div>
<AlarmClock className="w-4 h-4" />
</CardHeader>
</Card>
</Link>
);
},
New: () => {
const { mutateAsync } = useWrite("CreateAlerter");
const [name, setName] = useState("");
const [type, setType] = useState<Types.AlerterConfig["type"]>();
return (
<NewResource
entityType="Alerter"
onSuccess={async () =>
!!type && mutateAsync({ name, config: { type, params: {} } })
}
enabled={!!name && !!type}
>
<div className="grid md:grid-cols-2">
Name
<Input
placeholder="alerter-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid md:grid-cols-2">
Alerter Type
<Select
value={type}
onValueChange={(value) => setType(value as typeof type)}
>
<SelectTrigger>
<SelectValue placeholder="Select Type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Slack">Slack</SelectItem>
<SelectItem value="Custom">Custom</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</NewResource>
);
},
};

View File

@@ -2,45 +2,19 @@ import { NewResource } from "@components/layouts";
import { ConfirmButton } from "@components/util";
import { useExecute, useRead, useWrite } from "@lib/hooks";
import { RequiredResourceComponents } from "@types";
import { DataTable } from "@ui/data-table";
import { Input } from "@ui/input";
import { Ban, Hammer, History, Loader2 } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { ResourceComponents } from "..";
import { BuildChart } from "@components/dashboard/builds-chart";
import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { useToast } from "@ui/use-toast";
import { BuildConfig } from "./config";
import { fmt_date_with_minutes, fmt_version } from "@lib/formatting";
import { fill_color_class_by_intention } from "@lib/color";
import { BuildChart } from "./dashboard";
import { BuildTable } from "./table";
import { fmt_version } from "@lib/formatting";
const useBuild = (id?: string) =>
useRead("ListBuilds", {}).data?.find((d) => d.id === id);
const NewBuild = () => {
const { mutateAsync } = useWrite("CreateBuild");
const [name, setName] = useState("");
return (
<NewResource
entityType="Build"
onSuccess={() => mutateAsync({ name, config: {} })}
enabled={!!name}
>
<div className="grid md:grid-cols-2">
Build Name
<Input
placeholder="build-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</NewResource>
);
};
const Name = ({ id }: { id: string }) => <>{useBuild(id)?.name}</>;
const Icon = ({ id }: { id: string }) => {
const building = useRead("GetBuildActionState", { build: id }).data?.building;
const className = building
@@ -49,75 +23,22 @@ const Icon = ({ id }: { id: string }) => {
return <Hammer className={className} />;
};
const BuildTable = () => {
const builds = useRead("ListBuilds", {}).data;
const tags = useTagsFilter();
return (
<DataTable
data={
builds?.filter((build) =>
tags.every((tag) => build.tags.includes(tag))
) ?? []
}
columns={[
{
accessorKey: "id",
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
return (
<Link to={`/builds/${id}`} className="flex items-center gap-2">
<ResourceComponents.Build.Icon id={id} />
<ResourceComponents.Build.Name id={id} />
</Link>
);
},
},
{
header: "Repo",
accessorKey: "info.repo",
},
{
header: "Version",
accessorFn: ({ info }) => fmt_version(info.version),
},
{
header: "Last Built",
accessorFn: ({ info: { last_built_at } }) => {
if (last_built_at > 0) {
return fmt_date_with_minutes(new Date(last_built_at));
} else {
return "never";
}
},
},
{
header: "Tags",
cell: ({ row }) => {
return (
<div className="flex gap-1">
<TagsWithBadge tag_ids={row.original.tags} />
</div>
);
},
},
]}
/>
);
};
export const BuildComponents: RequiredResourceComponents = {
Name,
Table: BuildTable,
Dashboard: BuildChart,
Name: ({ id }: { id: string }) => <>{useBuild(id)?.name}</>,
Description: ({ id }) => <>{fmt_version(useBuild(id)?.info.version)}</>,
Info: ({ id }) => {
const ts = useBuild(id)?.info.last_built_at;
return (
<div className="flex items-center gap-2">
<History className="w-4 h-4" />
{ts ? new Date(ts).toLocaleString() : "Never Built"}
</div>
);
},
Info: [
({ id }) => {
const ts = useBuild(id)?.info.last_built_at;
return (
<div className="flex items-center gap-2">
<History className="w-4 h-4" />
{ts ? new Date(ts).toLocaleString() : "Never Built"}
</div>
);
},
],
Status: () => <>Build</>,
Page: {
Config: ({ id }) => <BuildConfig id={id} />,
@@ -176,7 +97,24 @@ export const BuildComponents: RequiredResourceComponents = {
);
}
},
Table: BuildTable,
New: NewBuild,
Dashboard: BuildChart,
New: () => {
const { mutateAsync } = useWrite("CreateBuild");
const [name, setName] = useState("");
return (
<NewResource
entityType="Build"
onSuccess={() => mutateAsync({ name, config: {} })}
enabled={!!name}
>
<div className="grid md:grid-cols-2">
Build Name
<Input
placeholder="build-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</NewResource>
);
},
};

View File

@@ -0,0 +1,63 @@
import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { useRead } from "@lib/hooks";
import { DataTable } from "@ui/data-table";
import { Link } from "react-router-dom";
import { ResourceComponents } from "..";
import { fmt_date_with_minutes, fmt_version } from "@lib/formatting";
export const BuildTable = () => {
const builds = useRead("ListBuilds", {}).data;
const tags = useTagsFilter();
return (
<DataTable
data={
builds?.filter((build) =>
tags.every((tag) => build.tags.includes(tag))
) ?? []
}
columns={[
{
accessorKey: "id",
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
return (
<Link to={`/builds/${id}`} className="flex items-center gap-2">
<ResourceComponents.Build.Icon id={id} />
<ResourceComponents.Build.Name id={id} />
</Link>
);
},
},
{
header: "Repo",
accessorKey: "info.repo",
},
{
header: "Version",
accessorFn: ({ info }) => fmt_version(info.version),
},
{
header: "Last Built",
accessorFn: ({ info: { last_built_at } }) => {
if (last_built_at > 0) {
return fmt_date_with_minutes(new Date(last_built_at));
} else {
return "never";
}
},
},
{
header: "Tags",
cell: ({ row }) => {
return (
<div className="flex gap-1">
<TagsWithBadge tag_ids={row.original.tags} />
</div>
);
},
},
]}
/>
);
};

View File

@@ -22,126 +22,124 @@ import { BuilderConfig } from "./config";
const useBuilder = (id?: string) =>
useRead("ListBuilders", {}).data?.find((d) => d.id === id);
const NewBuilder = () => {
const { mutateAsync } = useWrite("CreateBuilder");
const [name, setName] = useState("");
const [type, setType] = useState<Types.BuilderConfig["type"]>();
return (
<NewResource
entityType="Builder"
onSuccess={async () =>
!!type && mutateAsync({ name, config: { type, params: {} } })
}
enabled={!!name && !!type}
>
<div className="grid md:grid-cols-2">
Name
<Input
placeholder="builder-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid md:grid-cols-2">
Builder Type
<Select
value={type}
onValueChange={(value) => setType(value as typeof type)}
>
<SelectTrigger>
<SelectValue placeholder="Select Type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Aws">Aws</SelectItem>
<SelectItem value="Server">Server</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</NewResource>
);
};
const Name = ({ id }: { id: string }) => <>{useBuilder(id)?.name}</>;
const BuilderDashboard = () => {
const builders_count = useRead("ListBuilders", {}).data?.length;
return (
<Link to="/builders/" className="w-full">
<Card>
<CardHeader className="justify-between">
<div>
<CardTitle>Builders</CardTitle>
<CardDescription>{builders_count} Total</CardDescription>
</div>
<Factory className="w-4 h-4" />
</CardHeader>
</Card>
</Link>
);
};
const BuilderTable = () => {
const tags = useTagsFilter();
const builders = useRead("ListBuilders", {}).data;
return (
<DataTable
data={
builders?.filter((builder) =>
tags.every((tag) => builder.tags.includes(tag))
) ?? []
}
columns={[
{
accessorKey: "id",
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
return (
<Link to={`/builders/${id}`} className="flex items-center gap-2">
<Factory className="w-4 h-4" />
<Name id={id} />
</Link>
);
},
},
{
header: "Provider",
accessorKey: "info.provider",
},
{
header: "Instance Type",
accessorKey: "info.instance_type",
},
{ header: "Tags", accessorFn: ({ tags }) => tags.join(", ") },
]}
/>
);
};
export const BuilderComponents: RequiredResourceComponents = {
Name,
Description: ({ id }) => <>{id}</>,
Info: ({ id }) => (
<>
Name: ({ id }: { id: string }) => <>{useBuilder(id)?.name}</>,
Description: () => <></>,
Info: [
({ id }) => (
<div className="flex items-center gap-2">
<Cloud className="w-4 h-4" />
{useBuilder(id)?.info.provider}
</div>
),
({ id }) => (
<div className="flex items-center gap-2">
<Bot className="w-4 h-4" />
{useBuilder(id)?.info.instance_type ?? "N/A"}
</div>
</>
),
),
],
Icon: () => <Factory className="w-4 h-4" />,
Status: () => <></>,
Status: () => <>Builder</>,
Actions: () => <></>,
Page: {
Config: BuilderConfig,
},
Table: BuilderTable,
Actions: () => null,
New: () => <NewBuilder />,
Dashboard: BuilderDashboard,
Table: () => {
const tags = useTagsFilter();
const builders = useRead("ListBuilders", {}).data;
return (
<DataTable
data={
builders?.filter((builder) =>
tags.every((tag) => builder.tags.includes(tag))
) ?? []
}
columns={[
{
accessorKey: "id",
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
return (
<Link
to={`/builders/${id}`}
className="flex items-center gap-2"
>
<Factory className="w-4 h-4" />
<BuilderComponents.Name id={id} />
</Link>
);
},
},
{
header: "Provider",
accessorKey: "info.provider",
},
{
header: "Instance Type",
accessorKey: "info.instance_type",
},
{ header: "Tags", accessorFn: ({ tags }) => tags.join(", ") },
]}
/>
);
},
New: () => {
const { mutateAsync } = useWrite("CreateBuilder");
const [name, setName] = useState("");
const [type, setType] = useState<Types.BuilderConfig["type"]>();
return (
<NewResource
entityType="Builder"
onSuccess={async () =>
!!type && mutateAsync({ name, config: { type, params: {} } })
}
enabled={!!name && !!type}
>
<div className="grid md:grid-cols-2">
Name
<Input
placeholder="builder-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid md:grid-cols-2">
Builder Type
<Select
value={type}
onValueChange={(value) => setType(value as typeof type)}
>
<SelectTrigger>
<SelectValue placeholder="Select Type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="Aws">Aws</SelectItem>
<SelectItem value="Server">Server</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</NewResource>
);
},
Dashboard: () => {
const builders_count = useRead("ListBuilders", {}).data?.length;
return (
<Link to="/builders/" className="w-full">
<Card>
<CardHeader className="justify-between">
<div>
<CardTitle>Builders</CardTitle>
<CardDescription>{builders_count} Total</CardDescription>
</div>
<Factory className="w-4 h-4" />
</CardHeader>
</Card>
</Link>
);
},
};

View File

@@ -1,7 +1,7 @@
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types";
import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react";
import { AlertTriangle, HardDrive, Rocket } from "lucide-react";
import { cn } from "@lib/utils";
import { useState } from "react";
import { NewResource, Section } from "@components/layouts";
@@ -17,174 +17,53 @@ import {
} from "./actions";
import { Input } from "@ui/input";
import { DeploymentLogs } from "./logs";
import { Link } from "react-router-dom";
import { DataTable } from "@ui/data-table";
import { ResourceComponents } from "..";
import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { DeploymentsChart } from "@components/dashboard/deployments-chart";
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";
import { DeploymentTable } from "./table";
import { ResourceLink } from "@components/util";
import { DeploymentsChart } from "./dashboard";
export const useDeployment = (id?: string) =>
useRead("ListDeployments", {}, { refetchInterval: 5000 }).data?.find(
(d) => d.id === id
);
const Icon = ({ id }: { id?: string }) => {
const state = useDeployment(id)?.info.state;
return (
<Rocket
className={cn(
"w-4",
fill_color_class_by_intention(deployment_state_intention(state))
)}
/>
);
};
const Name = ({ id }: { id: string }) => <>{useDeployment(id)?.name}</>;
const Description = ({ id }: { id: string }) => (
<>{useDeployment(id)?.info.status}</>
);
const Info = ({ id }: { id: string }) => {
const info = useDeployment(id)?.info;
const server = useServer(info?.server_id);
return (
<>
<Link
to={info?.build_id ? `/builds/${info.build_id}` : "."}
className="flex items-center gap-2"
>
<HardDrive className="w-4 h-4" />
{useDeployment(id)?.info.image || "N/A"}
</Link>
<Link to={`/servers/${server?.id}`}>
<Button variant="link" className="flex items-center gap-2">
<Server className="w-4 h-4" />
{server?.name ?? "N/A"}
</Button>
</Link>
</>
);
};
export const DeploymentTable = ({
deployments,
}: {
deployments: Types.DeploymentListItem[] | undefined;
}) => {
const tags = useTagsFilter();
return (
<DataTable
data={
deployments?.filter((deployment) =>
tags.every((tag) => deployment.tags.includes(tag))
) ?? []
}
columns={[
{
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
return (
<Link to={`/deployments/${id}`}>
<Button variant="link" className="flex gap-2 items-center p-0">
<ResourceComponents.Deployment.Icon id={id} />
<ResourceComponents.Deployment.Name id={id} />
</Button>
</Link>
);
},
},
{
header: "Image",
cell: ({
row: {
original: {
info: { build_id, image },
},
},
}) => {
const builds = useRead("ListBuilds", {}).data;
if (build_id) {
const build = builds?.find((build) => build.id === build_id);
if (build) {
return (
<Link to={`/builds/${build_id}`}>
<Button
variant="link"
className="flex gap-2 items-center p-0"
>
<ResourceComponents.Build.Icon id={build_id} />
<ResourceComponents.Build.Name id={build_id} />
</Button>
</Link>
);
} else {
return undefined;
}
} else {
const [img, _] = image.split(":");
return img;
}
},
},
{
header: "Server",
cell: ({ row }) => {
const id = row.original.info.server_id;
return (
<Link to={`/servers/${id}`}>
<Button variant="link" className="flex items-center gap-2 p-0">
<ResourceComponents.Server.Icon id={id} />
<ResourceComponents.Server.Name id={id} />
</Button>
</Link>
);
},
},
{
header: "State",
cell: ({ row }) => {
const state = row.original.info.state;
const color = text_color_class_by_intention(
deployment_state_intention(state)
);
return (
<div className={color}>
{snake_case_to_upper_space_case(state)}
</div>
);
},
},
{
header: "Tags",
cell: ({ row }) => {
return (
<div className="flex gap-1">
<TagsWithBadge tag_ids={row.original.tags} />
</div>
);
},
},
]}
/>
);
};
export const DeploymentComponents: RequiredResourceComponents = {
Name,
Description,
Info,
Icon,
Name: ({ id }) => <>{useDeployment(id)?.name}</>,
Description: ({ id }) => <>{useDeployment(id)?.info.status}</>,
Info: [
({ id }) => {
const info = useDeployment(id)?.info;
return info?.build_id ? (
<ResourceLink type="Build" id={info.build_id} />
) : (
<div className="flex gap-2 items-center">
<HardDrive className="w-4 h-4" />
{info?.image || "N/A"}
</div>
);
},
({ id }) => {
const info = useDeployment(id)?.info;
const server = useServer(info?.server_id);
return server?.id ? (
<ResourceLink type="Server" id={server?.id} />
) : (
"Unknown Server"
);
},
],
Icon: ({ id }) => {
const state = useDeployment(id)?.info.state;
const color = fill_color_class_by_intention(
deployment_state_intention(state)
);
return <Rocket className={cn("w-4", color)} />;
},
Status: ({ id }) => {
const state =
useDeployment(id)?.info.state ?? Types.DockerContainerState.Unknown;

View File

@@ -0,0 +1,116 @@
import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { Types } from "@monitor/client";
import { Button } from "@ui/button";
import { DataTable } from "@ui/data-table";
import { Link } from "react-router-dom";
import { useRead } from "@lib/hooks";
import { ResourceComponents } from "..";
import {
deployment_state_intention,
text_color_class_by_intention,
} from "@lib/color";
import { snake_case_to_upper_space_case } from "@lib/formatting";
export const DeploymentTable = ({
deployments,
}: {
deployments: Types.DeploymentListItem[] | undefined;
}) => {
const tags = useTagsFilter();
return (
<DataTable
data={
deployments?.filter((deployment) =>
tags.every((tag) => deployment.tags.includes(tag))
) ?? []
}
columns={[
{
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
return (
<Link to={`/deployments/${id}`}>
<Button variant="link" className="flex gap-2 items-center p-0">
<ResourceComponents.Deployment.Icon id={id} />
<ResourceComponents.Deployment.Name id={id} />
</Button>
</Link>
);
},
},
{
header: "Image",
cell: ({
row: {
original: {
info: { build_id, image },
},
},
}) => {
const builds = useRead("ListBuilds", {}).data;
if (build_id) {
const build = builds?.find((build) => build.id === build_id);
if (build) {
return (
<Link to={`/builds/${build_id}`}>
<Button
variant="link"
className="flex gap-2 items-center p-0"
>
<ResourceComponents.Build.Icon id={build_id} />
<ResourceComponents.Build.Name id={build_id} />
</Button>
</Link>
);
} else {
return undefined;
}
} else {
const [img, _] = image.split(":");
return img;
}
},
},
{
header: "Server",
cell: ({ row }) => {
const id = row.original.info.server_id;
return (
<Link to={`/servers/${id}`}>
<Button variant="link" className="flex items-center gap-2 p-0">
<ResourceComponents.Server.Icon id={id} />
<ResourceComponents.Server.Name id={id} />
</Button>
</Link>
);
},
},
{
header: "State",
cell: ({ row }) => {
const state = row.original.info.state;
const color = text_color_class_by_intention(
deployment_state_intention(state)
);
return (
<div className={color}>
{snake_case_to_upper_space_case(state)}
</div>
);
},
},
{
header: "Tags",
cell: ({ row }) => {
return (
<div className="flex gap-1">
<TagsWithBadge tag_ids={row.original.tags} />
</div>
);
},
},
]}
/>
);
};

View File

@@ -99,10 +99,6 @@ const ProcedureConfigInner = ({
</Button>
}
columns={[
{
header: "Stage",
cell: ({ row: { index } }) => <>{index + 1}</>,
},
{
header: "Enabled",
cell: ({

View File

@@ -1,11 +1,9 @@
import { NewResource } from "@components/layouts";
import { TagsWithBadge } from "@components/tags";
import { ConfirmButton } from "@components/util";
import { useExecute, useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { DataTable } from "@ui/data-table";
import { Input } from "@ui/input";
import {
Select,
@@ -18,6 +16,7 @@ import { Loader2, Route } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { ProcedureConfig } from "./config";
import { ProcedureTable } from "./table";
const useProcedure = (id?: string) =>
useRead("ListProcedures", {}).data?.find((d) => d.id === id);
@@ -25,9 +24,9 @@ const useProcedure = (id?: string) =>
export const ProcedureComponents: RequiredResourceComponents = {
Name: ({ id }) => <>{useProcedure(id)?.name}</>,
Description: ({ id }) => <>{useProcedure(id)?.info.procedure_type}</>,
Info: () => <></>,
Info: [({ id }) => <>{useProcedure(id)?.info.procedure_type}</>],
Icon: () => <Route className="w-4" />,
Status: () => <></>,
Status: () => <>Procedure</>,
Page: {
Config: ProcedureConfig,
},
@@ -50,46 +49,7 @@ export const ProcedureComponents: RequiredResourceComponents = {
/>
);
},
Table: () => {
const procedures = useRead("ListProcedures", {}).data;
return (
<DataTable
data={procedures ?? []}
columns={[
{
accessorKey: "id",
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
return (
<Link
to={`/procedures/${id}`}
className="flex items-center gap-2"
>
<ProcedureComponents.Icon id={id} />
<ProcedureComponents.Name id={id} />
</Link>
);
},
},
{
header: "Type",
accessorKey: "info.procedure_type",
},
{
header: "Tags",
cell: ({ row }) => {
return (
<div className="flex gap-1">
<TagsWithBadge tag_ids={row.original.tags} />
</div>
);
},
},
]}
/>
);
},
Table: ProcedureTable,
New: () => {
const { mutateAsync } = useWrite("CreateProcedure");
const [name, setName] = useState("");

View File

@@ -0,0 +1,46 @@
import { useRead } from "@lib/hooks";
import { DataTable } from "@ui/data-table";
import { Link } from "react-router-dom";
import { ProcedureComponents } from ".";
import { TagsWithBadge } from "@components/tags";
export const ProcedureTable = () => {
const procedures = useRead("ListProcedures", {}).data;
return (
<DataTable
data={procedures ?? []}
columns={[
{
accessorKey: "id",
header: "Name",
cell: ({ row }) => {
const id = row.original.id;
return (
<Link
to={`/procedures/${id}`}
className="flex items-center gap-2"
>
<ProcedureComponents.Icon id={id} />
<ProcedureComponents.Name id={id} />
</Link>
);
},
},
{
header: "Type",
accessorKey: "info.procedure_type",
},
{
header: "Tags",
cell: ({ row }) => {
return (
<div className="flex gap-1">
<TagsWithBadge tag_ids={row.original.tags} />
</div>
);
},
},
]}
/>
);
};

View File

@@ -1,6 +1,5 @@
import { TagsWithBadge } from "@components/tags";
import { useRead } from "@lib/hooks";
import { Icon } from "@radix-ui/react-select";
import { RequiredResourceComponents } from "@types";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { DataTable } from "@ui/data-table";
@@ -11,31 +10,14 @@ import { RepoConfig } from "./config";
const useRepo = (id?: string) =>
useRead("ListRepos", {}).data?.find((d) => d.id === id);
const Name = ({ id }: { id: string }) => <>{useRepo(id)?.name}</>;
export const RepoDashboard = () => {
const repo_count = useRead("ListRepos", {}).data?.length;
return (
<Link to="/repos/" className="w-full">
<Card>
<CardHeader className="justify-between">
<div>
<CardTitle>Repos</CardTitle>
<CardDescription>{repo_count} Total</CardDescription>
</div>
<GitBranch className="w-4 h-4" />
</CardHeader>
</Card>
</Link>
);
};
export const RepoComponents: RequiredResourceComponents = {
Name,
Name: ({ id }: { id: string }) => <>{useRepo(id)?.name}</>,
Description: ({ id }) => <>{id}</>,
Info: ({ id }) => <>{id}</>,
Icon: () => <GitBranch className="w-4 h-4" />,
Info: [],
Status: () => <></>,
Actions: () => <></>,
New: () => <></>,
Page: {
Config: RepoConfig,
},
@@ -52,8 +34,8 @@ export const RepoComponents: RequiredResourceComponents = {
const id = row.original.id;
return (
<Link to={`/repos/${id}`} className="flex items-center gap-2">
<Icon id={id} />
<Name id={id} />
<RepoComponents.Icon id={id} />
<RepoComponents.Name id={id} />
</Link>
);
},
@@ -72,7 +54,20 @@ export const RepoComponents: RequiredResourceComponents = {
/>
);
},
Actions: () => null,
New: () => null,
Dashboard: RepoDashboard,
Dashboard: () => {
const repo_count = useRead("ListRepos", {}).data?.length;
return (
<Link to="/repos/" className="w-full">
<Card>
<CardHeader className="justify-between">
<div>
<CardTitle>Repos</CardTitle>
<CardDescription>{repo_count} Total</CardDescription>
</div>
<GitBranch className="w-4 h-4" />
</CardHeader>
</Card>
</Link>
);
},
};

View File

@@ -2,191 +2,41 @@ import { useRead, useWrite } from "@lib/hooks";
import { cn } from "@lib/utils";
import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types";
import {
MapPin,
Cpu,
MemoryStick,
Database,
ServerIcon,
AlertTriangle,
Rocket,
} from "lucide-react";
import { ServerIcon, AlertTriangle, Rocket } from "lucide-react";
import { ServerStats } from "./stats";
import { useState } from "react";
import { NewResource, Section } from "@components/layouts";
import { Input } from "@ui/input";
import { DataTable } from "@ui/data-table";
import { Link } from "react-router-dom";
import { ResourceComponents } from "..";
import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { DeleteServer, RenameServer } from "./actions";
import { ServersChart } from "@components/dashboard/servers-chart";
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";
import { DeploymentTable } from "../deployment/table";
import { ServerTable } from "./table";
import { ServerInfo } from "./info";
import { ServersChart } from "./dashboard";
export const useServer = (id?: string) =>
useRead("ListServers", {}).data?.find((d) => d.id === id);
export const ServerInfo = ({
id,
showRegion = true,
}: {
id: string;
showRegion?: boolean;
}) => {
const server = useServer(id);
const stats = useRead(
"GetBasicSystemStats",
{ server: id },
{ enabled: server ? server.info.status !== "Disabled" : false }
).data;
const info = useRead(
"GetSystemInformation",
{ server: id },
{ enabled: server ? server.info.status !== "Disabled" : false }
).data;
return (
<>
{showRegion && (
<>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
{useServer(id)?.info.region}
</div>
|
</>
)}
<div className="flex gap-2 items-center">
<Cpu className="w-4 h-4" />
{info?.core_count ?? "N/A"} Core(s)
</div>
|
<div className="flex gap-2 items-center">
<MemoryStick className="w-4 h-4" />
{stats?.mem_total_gb.toFixed(2) ?? "N/A"} GB
</div>
|
<div className="flex gap-2 items-center">
<Database className="w-4 h-4" />
{stats?.disk_total_gb.toFixed(2) ?? "N/A"} GB
</div>
</>
);
};
export const ServerIconComponent = ({ id }: { id?: string }) => {
const status = useServer(id)?.info.status;
return (
<ServerIcon
className={cn(
"w-4 h-4",
id && fill_color_class_by_intention(server_status_intention(status))
)}
/>
);
};
const NewServer = () => {
const { mutateAsync } = useWrite("CreateServer");
const [name, setName] = useState("");
return (
<NewResource
entityType="Server"
onSuccess={() => mutateAsync({ name, config: {} })}
enabled={!!name}
>
<div className="grid md:grid-cols-2">
Server Name
<Input
placeholder="server-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</NewResource>
);
};
const DeploymentCountOnServer = ({ id }: { id: string }) => {
const { data } = useRead("ListDeployments", {
query: { specific: { server_ids: [id] } },
});
return <>{data?.length ?? 0}</>;
};
const ServerTable = () => {
const servers = useRead("ListServers", {}).data;
const tags = useTagsFilter();
return (
<DataTable
// onRowClick={({ id }) => nav(`/servers/${id}`)}
data={
servers?.filter((server) =>
tags.every((tag) => server.tags.includes(tag))
) ?? []
}
columns={[
{
header: "Name",
accessorKey: "id",
cell: ({
row: {
original: { id },
},
}) => {
return (
<Link to={`/servers/${id}`} className="flex gap-2">
<ResourceComponents.Server.Icon id={id} />
<ResourceComponents.Server.Name id={id} />
</Link>
);
},
},
// {
// header: "Description",
// accessorKey: "description",
// },
{
header: "Deployments",
cell: ({ row }) => <DeploymentCountOnServer id={row.original.id} />,
},
{ header: "Region", accessorKey: "info.region" },
{
header: "State",
cell: ({
row: {
original: { id },
},
}) => <ServerComponents.Status id={id} />,
},
{
header: "Tags",
cell: ({ row }) => {
return (
<div className="flex gap-1">
<TagsWithBadge tag_ids={row.original.tags} />
</div>
);
},
},
]}
/>
);
};
export const ServerComponents: RequiredResourceComponents = {
Name: ({ id }: { id: string }) => <>{useServer(id)?.name}</>,
Description: ({ id }) => <>{useServer(id)?.info.status}</>,
Info: ({ id }) => <ServerInfo id={id} />,
Actions: () => null,
Icon: ServerIconComponent,
Info: [({ id }) => <ServerInfo id={id} />],
Icon: ({ id }) => {
const status = useServer(id)?.info.status;
return (
<ServerIcon
className={cn(
"w-4 h-4",
id && fill_color_class_by_intention(server_status_intention(status))
)}
/>
);
},
Status: ({ id }) => {
const status = useServer(id)?.info.status;
const stateClass = text_color_class_by_intention(
@@ -198,6 +48,7 @@ export const ServerComponents: RequiredResourceComponents = {
</div>
);
},
Actions: () => null,
Page: {
Stats: ({ id }) => <ServerStats server_id={id} />,
Deployments: ({ id }) => {
@@ -218,7 +69,26 @@ export const ServerComponents: RequiredResourceComponents = {
</Section>
),
},
New: () => <NewServer />,
New: () => {
const { mutateAsync } = useWrite("CreateServer");
const [name, setName] = useState("");
return (
<NewResource
entityType="Server"
onSuccess={() => mutateAsync({ name, config: {} })}
enabled={!!name}
>
<div className="grid md:grid-cols-2">
Server Name
<Input
placeholder="server-name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</NewResource>
);
},
Table: ServerTable,
Dashboard: ServersChart,
};

View File

@@ -0,0 +1,50 @@
import { useRead } from "@lib/hooks";
import { useServer } from ".";
import { Cpu, Database, MapPin, MemoryStick } from "lucide-react";
export const ServerInfo = ({
id,
showRegion = true,
}: {
id: string;
showRegion?: boolean;
}) => {
const server = useServer(id);
const stats = useRead(
"GetBasicSystemStats",
{ server: id },
{ enabled: server ? server.info.status !== "Disabled" : false }
).data;
const info = useRead(
"GetSystemInformation",
{ server: id },
{ enabled: server ? server.info.status !== "Disabled" : false }
).data;
return (
<>
{showRegion && (
<>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
{useServer(id)?.info.region}
</div>
|
</>
)}
<div className="flex gap-2 items-center">
<Cpu className="w-4 h-4" />
{info?.core_count ?? "N/A"} Core(s)
</div>
|
<div className="flex gap-2 items-center">
<MemoryStick className="w-4 h-4" />
{stats?.mem_total_gb.toFixed(2) ?? "N/A"} GB
</div>
|
<div className="flex gap-2 items-center">
<Database className="w-4 h-4" />
{stats?.disk_total_gb.toFixed(2) ?? "N/A"} GB
</div>
</>
);
};

View File

@@ -0,0 +1,74 @@
import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { useRead } from "@lib/hooks";
import { DataTable } from "@ui/data-table";
import { Link } from "react-router-dom";
import { ServerComponents } from ".";
export const ServerTable = () => {
const servers = useRead("ListServers", {}).data;
const tags = useTagsFilter();
return (
<DataTable
// onRowClick={({ id }) => nav(`/servers/${id}`)}
data={
servers?.filter((server) =>
tags.every((tag) => server.tags.includes(tag))
) ?? []
}
columns={[
{
header: "Name",
accessorKey: "id",
cell: ({
row: {
original: { id },
},
}) => {
return (
<Link to={`/servers/${id}`} className="flex gap-2">
<ServerComponents.Icon id={id} />
<ServerComponents.Name id={id} />
</Link>
);
},
},
// {
// header: "Description",
// accessorKey: "description",
// },
{
header: "Deployments",
cell: ({ row }) => <DeploymentCountOnServer id={row.original.id} />,
},
{ header: "Region", accessorKey: "info.region" },
{
header: "State",
cell: ({
row: {
original: { id },
},
}) => <ServerComponents.Status id={id} />,
},
{
header: "Tags",
cell: ({ row }) => {
return (
<div className="flex gap-1">
<TagsWithBadge tag_ids={row.original.tags} />
</div>
);
},
},
]}
/>
);
};
const DeploymentCountOnServer = ({ id }: { id: string }) => {
const { data } = useRead("ListDeployments", {
query: { specific: { server_ids: [id] } },
});
return <>{data?.length ?? 0}</>;
};

View File

@@ -5,7 +5,8 @@ export type ColorIntention =
| "Neutral"
| "Warning"
| "Critical"
| "Unknown";
| "Unknown"
| "None";
export const hex_color_by_intention = (intention: ColorIntention) => {
switch (intention) {
@@ -19,6 +20,8 @@ export const hex_color_by_intention = (intention: ColorIntention) => {
return "#EF0044";
case "Unknown":
return "#A855F7";
case "None":
return "";
}
};
@@ -34,21 +37,38 @@ export const color_class_by_intention = (intention: ColorIntention) => {
return "red-500";
case "Unknown":
return "purple-500";
case "None":
return "";
}
};
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) => {
if (intention === "None") return "";
return `fill-${color_class_by_intention(intention)}`;
};
export const stroke_color_class_by_intention = (intention: ColorIntention) => {
if (intention === "None") return "";
return `stroke-${color_class_by_intention(intention)}`;
};
export const text_color_class_by_intention = (intention: ColorIntention) => {
switch (intention) {
case "Good":
return "text-green-500";
case "Neutral":
return "text-blue-500";
case "Warning":
return "text-orange-500";
case "Critical":
return "text-red-500";
case "Unknown":
return "text-purple-500";
case "None":
return "";
}
};
export const server_status_intention: (
status?: Types.ServerStatus
) => ColorIntention = (status) => {
@@ -59,8 +79,8 @@ export const server_status_intention: (
return "Critical";
case Types.ServerStatus.Disabled:
return "Neutral";
default:
return "Unknown";
case undefined:
return "None";
}
};
@@ -68,11 +88,13 @@ export const deployment_state_intention: (
state?: Types.DockerContainerState
) => ColorIntention = (state) => {
switch (state) {
case undefined:
return "None";
case Types.DockerContainerState.Running:
return "Good";
case Types.DockerContainerState.NotDeployed:
return "Neutral";
case Types.DockerContainerState.Unknown || undefined:
case Types.DockerContainerState.Unknown:
return "Unknown";
default:
return "Critical";

View File

@@ -3,21 +3,15 @@ import { Page, Section } from "@components/layouts";
import { AlertTriangle, Box, FolderTree } from "lucide-react";
import { DataTable } from "@ui/data-table";
import { Link } from "react-router-dom";
import { ServerComponents } from "@components/resources/server";
import { AlertLevel } from "@components/util";
import { Button } from "@ui/button";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { DeploymentComponents } from "@components/resources/deployment";
import { BuildComponents } from "@components/resources/build";
import { RepoComponents } from "@components/resources/repo";
import { BuilderComponents } from "@components/resources/builder";
import { AlerterComponents } from "@components/resources/alerter";
import { ProcedureComponents } from "@components/resources/procedure/index";
import { TagsSummary } from "@components/dashboard/tags";
import { ApiKeysSummary } from "@components/dashboard/api-keys";
import { atomWithStorage } from "jotai/utils";
import { useAtom } from "jotai";
import { fmt_date_with_minutes } from "@lib/formatting";
import { ResourceComponents } from "@components/resources";
export const Dashboard = () => {
return (
@@ -57,7 +51,9 @@ const OpenAlerts = () => {
case "Server":
return (
<Link to={`/servers/${row.original.target.id}`}>
<ServerComponents.Name id={row.original.target.id} />
<ResourceComponents.Server.Name
id={row.original.target.id}
/>
</Link>
);
default:
@@ -88,11 +84,11 @@ const Resources = () => (
<Section title="Resources" icon={<Box className="w-4 h-4" />} actions="">
<div className="flex flex-col lg:flex-row gap-4 w-full">
<div className="flex flex-col md:flex-row gap-4 w-full">
<ServerComponents.Dashboard />
<DeploymentComponents.Dashboard />
<ResourceComponents.Server.Dashboard />
<ResourceComponents.Deployment.Dashboard />
</div>
<div className="w-full lg:max-w-[50%]">
<BuildComponents.Dashboard />
<ResourceComponents.Build.Dashboard />
</div>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full">
@@ -112,16 +108,16 @@ const Resources = () => (
</Link>
</div>
<div className="flex flex-col md:flex-row gap-4 w-full">
<RepoComponents.Dashboard />
<BuilderComponents.Dashboard />
<ResourceComponents.Repo.Dashboard />
<ResourceComponents.Builder.Dashboard />
{/* <TagsSummary />
<ApiKeysSummary /> */}
</div>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full">
<div className="flex flex-col md:flex-row gap-4 w-full">
<AlerterComponents.Dashboard />
<ProcedureComponents.Dashboard />
<ResourceComponents.Alerter.Dashboard />
<ResourceComponents.Procedure.Dashboard />
</div>
<div className="flex flex-col md:flex-row gap-4 w-full">
<TagsSummary />

View File

@@ -30,8 +30,11 @@ export const Resource = () => {
<Components.Icon id={id} />
<Components.Status id={id} />
</div>
|
<Components.Info id={id} />
{Components.Info.map((Info) => (
<>
| <Info id={id} />
</>
))}
</div>
</div>
}

View File

@@ -1,6 +1,7 @@
import { Page, Section } from "@components/layouts";
import { DeploymentTable } from "@components/resources/deployment";
import { ServerIconComponent, ServerInfo } from "@components/resources/server";
import { ResourceComponents } from "@components/resources";
import { DeploymentTable } from "@components/resources/deployment/table";
import { ServerInfo } from "@components/resources/server/info";
import { TagsFilter, TagsWithBadge, useTagsFilter } from "@components/tags";
import { useRead } from "@lib/hooks";
import { Button } from "@ui/button";
@@ -45,7 +46,7 @@ const Server = ({ id }: { id: string }) => {
{server?.id && <ServerInfo id={server.id} showRegion={false} />}
<Link to={`/servers/${server?.id}`}>
<Button variant="outline">
<ServerIconComponent id={server?.id} />
<ResourceComponents.Server.Icon id={server?.id} />
</Button>
</Link>
</div>

View File

@@ -1,4 +1,5 @@
import { Types } from "@monitor/client";
import { ReactNode } from "react";
export type UsableResource = Exclude<Types.ResourceTarget["type"], "System">;
@@ -15,7 +16,7 @@ export interface RequiredResourceComponents {
Name: IdComponent;
Description: IdComponent;
Info: IdComponent;
Info: IdComponent[];
Status: IdComponent;
Actions: IdComponent;