forked from github-starred/komodo
improve info
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
63
frontend/src/components/resources/build/table.tsx
Normal file
63
frontend/src/components/resources/build/table.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
116
frontend/src/components/resources/deployment/table.tsx
Normal file
116
frontend/src/components/resources/deployment/table.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -99,10 +99,6 @@ const ProcedureConfigInner = ({
|
||||
</Button>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
header: "Stage",
|
||||
cell: ({ row: { index } }) => <>{index + 1}</>,
|
||||
},
|
||||
{
|
||||
header: "Enabled",
|
||||
cell: ({
|
||||
|
||||
@@ -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("");
|
||||
|
||||
46
frontend/src/components/resources/procedure/table.tsx
Normal file
46
frontend/src/components/resources/procedure/table.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
50
frontend/src/components/resources/server/info.tsx
Normal file
50
frontend/src/components/resources/server/info.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
74
frontend/src/components/resources/server/table.tsx
Normal file
74
frontend/src/components/resources/server/table.tsx
Normal 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}</>;
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
3
frontend/src/types.d.ts
vendored
3
frontend/src/types.d.ts
vendored
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user