work on dashboard 2

This commit is contained in:
mbecker20
2024-05-17 01:35:34 -07:00
parent e96b676366
commit c9d65300c9
21 changed files with 529 additions and 114 deletions

View File

@@ -363,9 +363,11 @@ export const SystemCommand = ({
export const AddExtraArgMenu = ({ export const AddExtraArgMenu = ({
onSelect, onSelect,
type, type,
disabled,
}: { }: {
onSelect: (suggestion: string) => void; onSelect: (suggestion: string) => void;
type: "Deployment" | "Build"; type: "Deployment" | "Build";
disabled?: boolean;
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -376,6 +378,7 @@ export const AddExtraArgMenu = ({
<Button <Button
variant="secondary" variant="secondary"
className="flex items-center gap-2 w-[200px]" className="flex items-center gap-2 w-[200px]"
disabled={disabled}
> >
<PlusCircle className="w-4 h-4" /> Add Extra Arg <PlusCircle className="w-4 h-4" /> Add Extra Arg
</Button> </Button>

View File

@@ -23,6 +23,8 @@ const useAlerter = (id?: string) =>
useRead("ListAlerters", {}).data?.find((d) => d.id === id); useRead("ListAlerters", {}).data?.find((d) => d.id === id);
export const AlerterComponents: RequiredResourceComponents = { export const AlerterComponents: RequiredResourceComponents = {
list_item: (id) => useAlerter(id),
Dashboard: () => { Dashboard: () => {
const alerters_count = useRead("ListAlerters", {}).data?.length; const alerters_count = useRead("ListAlerters", {}).data?.length;
return ( return (
@@ -91,7 +93,6 @@ export const AlerterComponents: RequiredResourceComponents = {
Table: AlerterTable, Table: AlerterTable,
Name: ({ id }: { id: string }) => <>{useAlerter(id)?.name}</>, Name: ({ id }: { id: string }) => <>{useAlerter(id)?.name}</>,
name: (id) => useAlerter(id)?.name,
Icon: () => <AlarmClock className="w-4 h-4" />, Icon: () => <AlarmClock className="w-4 h-4" />,
BigIcon: () => <AlarmClock className="w-8 h-8" />, BigIcon: () => <AlarmClock className="w-8 h-8" />,

View File

@@ -0,0 +1,105 @@
import {
ColorType,
IChartApi,
ISeriesApi,
Time,
createChart,
} from "lightweight-charts";
import { useEffect, useRef } from "react";
import { useRead } from "@lib/hooks";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { Hammer } from "lucide-react";
import { Link } from "react-router-dom";
import { convertTsMsToLocalUnixTsInSecs } from "@lib/utils";
export const BuildChart = () => {
const container_ref = useRef<HTMLDivElement>(null);
const line_ref = useRef<IChartApi>();
const series_ref = useRef<ISeriesApi<"Histogram">>();
const build_stats = useRead("GetBuildMonthlyStats", {}).data;
const summary = useRead("GetBuildsSummary", {}).data;
const handleResize = () =>
line_ref.current?.applyOptions({
width: container_ref.current?.clientWidth,
});
useEffect(() => {
if (!build_stats) return;
if (line_ref.current) line_ref.current.remove();
const init = () => {
if (!container_ref.current) return;
// INIT LINE
line_ref.current = createChart(container_ref.current, {
width: container_ref.current.clientWidth,
height: container_ref.current.clientHeight,
layout: {
background: { type: ColorType.Solid, color: "transparent" },
textColor: "grey",
fontSize: 12,
},
grid: {
horzLines: { color: "transparent" },
vertLines: { color: "transparent" },
},
handleScale: false,
handleScroll: false,
});
line_ref.current.timeScale().fitContent();
// INIT SERIES
series_ref.current = line_ref.current.addHistogramSeries({
priceLineVisible: false,
});
const max = build_stats.days.reduce((m, c) => Math.max(m, c.time), 0);
series_ref.current.setData(
build_stats.days.map((d) => ({
time: convertTsMsToLocalUnixTsInSecs(d.ts) as Time,
value: d.count,
color:
d.time > max * 0.7
? "darkred"
: d.time > max * 0.35
? "darkorange"
: "darkgreen",
})) ?? []
);
};
// Run the effect
init();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [build_stats]);
return (
<Link to="/builds" className="w-full">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Builds</CardTitle>
<CardDescription className="flex gap-2">
<div>{summary?.total} Total</div> |{" "}
<div>{build_stats?.total_time.toFixed(2)} Hours</div>
</CardDescription>
</div>
<Hammer className="w-4 h-4" />
</div>
</CardHeader>
<CardContent className="hidden xl:flex h-[200px]">
<div className="w-full max-w-full h-full" ref={container_ref} />
</CardContent>
</Card>
</Link>
);
};

View File

@@ -166,6 +166,7 @@ export const BuildConfig = ({ id, titleOther }: { id: string; titleOther: ReactN
], ],
}) })
} }
disabled={disabled}
/> />
), ),
components: { components: {

View File

@@ -1,12 +1,3 @@
import {
ColorType,
IChartApi,
ISeriesApi,
Time,
createChart,
} from "lightweight-charts";
import { useEffect, useRef } from "react";
import { useRead } from "@lib/hooks";
import { import {
Card, Card,
CardContent, CardContent,
@@ -14,90 +5,111 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@ui/card"; } from "@ui/card";
import { PieChart } from "react-minimal-pie-chart";
import { useRead } from "@lib/hooks";
import { Hammer } from "lucide-react"; import { Hammer } from "lucide-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { convertTsMsToLocalUnixTsInSecs } from "@lib/utils"; import { cn } from "@lib/utils";
import {
hex_color_by_intention,
text_color_class_by_intention,
} from "@lib/color";
export const BuildChart = () => { export const BuildDashboard = () => {
const container_ref = useRef<HTMLDivElement>(null);
const line_ref = useRef<IChartApi>();
const series_ref = useRef<ISeriesApi<"Histogram">>();
const build_stats = useRead("GetBuildMonthlyStats", {}).data;
const summary = useRead("GetBuildsSummary", {}).data; const summary = useRead("GetBuildsSummary", {}).data;
const handleResize = () =>
line_ref.current?.applyOptions({
width: container_ref.current?.clientWidth,
});
useEffect(() => {
if (!build_stats) return;
if (line_ref.current) line_ref.current.remove();
const init = () => {
if (!container_ref.current) return;
// INIT LINE
line_ref.current = createChart(container_ref.current, {
width: container_ref.current.clientWidth,
height: container_ref.current.clientHeight,
layout: {
background: { type: ColorType.Solid, color: "transparent" },
textColor: "grey",
fontSize: 12,
},
grid: {
horzLines: { color: "transparent" },
vertLines: { color: "transparent" },
},
handleScale: false,
handleScroll: false,
});
line_ref.current.timeScale().fitContent();
// INIT SERIES
series_ref.current = line_ref.current.addHistogramSeries({
priceLineVisible: false,
});
const max = build_stats.days.reduce((m, c) => Math.max(m, c.time), 0);
series_ref.current.setData(
build_stats.days.map((d) => ({
time: convertTsMsToLocalUnixTsInSecs(d.ts) as Time,
value: d.count,
color:
d.time > max * 0.7
? "darkred"
: d.time > max * 0.35
? "darkorange"
: "darkgreen",
})) ?? []
);
};
// Run the effect
init();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, [build_stats]);
return ( return (
<Link to="/builds" className="w-full"> <Link to="/builds">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer"> <Card className="hover:bg-accent/50 transition-colors cursor-pointer w-fit">
<CardHeader> <CardHeader>
<div className="flex justify-between"> <div className="flex justify-between">
<div> <div>
<CardTitle>Builds</CardTitle> <CardTitle>Builds</CardTitle>
<CardDescription className="flex gap-2"> <CardDescription>{summary?.total} Total</CardDescription>
<div>{summary?.total} Total</div> |{" "}
<div>{build_stats?.total_time.toFixed(2)} Hours</div>
</CardDescription>
</div> </div>
<Hammer className="w-4 h-4" /> <Hammer className="w-4 h-4" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="hidden xl:flex h-[200px]"> <CardContent className="hidden xl:flex h-[200px] items-center justify-between gap-4">
<div className="w-full max-w-full h-full" ref={container_ref} /> <div className="flex flex-col gap-2 text-muted-foreground w-full text-nowrap">
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Good"),
"font-bold"
)}
>
{summary?.ok}{" "}
</span>
Ok
</CardDescription>
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Warning"),
"font-bold"
)}
>
{summary?.building}{" "}
</span>
Building
</CardDescription>
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Critical"),
"font-bold"
)}
>
{summary?.failed}{" "}
</span>
Failed
</CardDescription>
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Unknown"),
"font-bold"
)}
>
{summary?.unknown}{" "}
</span>
Unknown
</CardDescription>
</div>
<div className="flex justify-end items-center w-full">
<PieChart
className="w-32 h-32"
radius={42}
lineWidth={30}
data={[
{
color: hex_color_by_intention("Good"),
value: summary?.ok ?? 0,
title: "ok",
key: "ok",
},
{
color: hex_color_by_intention("Warning"),
value: summary?.building ?? 0,
title: "building",
key: "building",
},
{
color: hex_color_by_intention("Critical"),
value: summary?.failed ?? 0,
title: "failed",
key: "failed",
},
{
color: hex_color_by_intention("Unknown"),
value: summary?.unknown ?? 0,
title: "unknown",
key: "unknown",
},
]}
/>
</div>
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>

View File

@@ -3,7 +3,7 @@ import { useRead } from "@lib/hooks";
import { RequiredResourceComponents } from "@types"; import { RequiredResourceComponents } from "@types";
import { FolderGit, Hammer } from "lucide-react"; import { FolderGit, Hammer } from "lucide-react";
import { BuildConfig } from "./config"; import { BuildConfig } from "./config";
import { BuildChart } from "./dashboard"; import { BuildDashboard } from "./dashboard";
import { BuildTable } from "./table"; import { BuildTable } from "./table";
import { DeleteResource, NewResource } from "../common"; import { DeleteResource, NewResource } from "../common";
import { DeploymentTable } from "../deployment/table"; import { DeploymentTable } from "../deployment/table";
@@ -83,14 +83,15 @@ const ConfigOrDeployments = ({ id }: { id: string }) => {
}; };
export const BuildComponents: RequiredResourceComponents = { export const BuildComponents: RequiredResourceComponents = {
Dashboard: BuildChart, list_item: (id) => useBuild(id),
Dashboard: BuildDashboard,
New: () => <NewResource type="Build" />, New: () => <NewResource type="Build" />,
Table: BuildTable, Table: BuildTable,
Name: ({ id }) => <>{useBuild(id)?.name}</>, Name: ({ id }) => <>{useBuild(id)?.name}</>,
name: (id) => useBuild(id)?.name,
Icon: ({ id }) => <BuildIcon id={id} size={4} />, Icon: ({ id }) => <BuildIcon id={id} size={4} />,
BigIcon: ({ id }) => <BuildIcon id={id} size={8} />, BigIcon: ({ id }) => <BuildIcon id={id} size={8} />,

View File

@@ -36,6 +36,8 @@ export const BuilderInstanceType = ({ id }: { id: string }) => {
}; };
export const BuilderComponents: RequiredResourceComponents = { export const BuilderComponents: RequiredResourceComponents = {
list_item: (id) => useBuilder(id),
Dashboard: () => { Dashboard: () => {
const builders_count = useRead("ListBuilders", {}).data?.length; const builders_count = useRead("ListBuilders", {}).data?.length;
return ( return (
@@ -65,8 +67,9 @@ export const BuilderComponents: RequiredResourceComponents = {
<NewLayout <NewLayout
entityType="Builder" entityType="Builder"
onSuccess={async () => { onSuccess={async () => {
if (!type) return if (!type) return;
const id = (await mutateAsync({ name, config: { type, params: {} } }))._id?.$oid!; const id = (await mutateAsync({ name, config: { type, params: {} } }))
._id?.$oid!;
nav(`/builders/${id}`); nav(`/builders/${id}`);
}} }}
enabled={!!name && !!type} enabled={!!name && !!type}
@@ -103,7 +106,6 @@ export const BuilderComponents: RequiredResourceComponents = {
Table: BuilderTable, Table: BuilderTable,
Name: ({ id }: { id: string }) => <>{useBuilder(id)?.name}</>, Name: ({ id }: { id: string }) => <>{useBuilder(id)?.name}</>,
name: (id) => useBuilder(id)?.name,
Icon: () => <Factory className="w-4 h-4" />, Icon: () => <Factory className="w-4 h-4" />,
BigIcon: () => <Factory className="w-8 h-8" />, BigIcon: () => <Factory className="w-8 h-8" />,

View File

@@ -35,6 +35,8 @@ import { NewLayout } from "@components/layouts";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { ConfigItem, DoubleInput } from "@components/config/util"; import { ConfigItem, DoubleInput } from "@components/config/util";
import { usableResourcePath } from "@lib/utils"; import { usableResourcePath } from "@lib/utils";
import { Card } from "@ui/card";
import { TagsWithBadge } from "@components/tags";
export const ResourceDescription = ({ export const ResourceDescription = ({
type, type,
@@ -87,7 +89,7 @@ export const ResourceSelector = ({
selected: string | undefined; selected: string | undefined;
onSelect?: (id: string) => void; onSelect?: (id: string) => void;
disabled?: boolean; disabled?: boolean;
align?: "start" | "center" | "end" align?: "start" | "center" | "end";
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
@@ -311,11 +313,7 @@ export const LabelsConfig = ({
</div> </div>
); );
export const CopyGithubWebhook = ({ export const CopyGithubWebhook = ({ path }: { path: string }) => {
path,
}: {
path: string;
}) => {
const base_url = useRead("GetCoreInfo", {}).data?.github_webhook_base_url; const base_url = useRead("GetCoreInfo", {}).data?.github_webhook_base_url;
const url = base_url + "/listener/github" + path; const url = base_url + "/listener/github" + path;
return ( return (
@@ -335,7 +333,7 @@ export const ServerSelector = ({
selected: string | undefined; selected: string | undefined;
set: (input: Partial<Types.DeploymentConfig>) => void; set: (input: Partial<Types.DeploymentConfig>) => void;
disabled: boolean; disabled: boolean;
align?: "start" | "center" | "end" align?: "start" | "center" | "end";
}) => ( }) => (
<ConfigItem label="Server"> <ConfigItem label="Server">
<ResourceSelector <ResourceSelector
@@ -346,4 +344,28 @@ export const ServerSelector = ({
align={align} align={align}
/> />
</ConfigItem> </ConfigItem>
); );
export const RecentCard = ({
type,
id,
}: {
type: UsableResource;
id: string;
}) => {
const Components = ResourceComponents[type];
const tags = Components.list_item(id)?.tags;
return (
<Link to={`${usableResourcePath(type)}/${id}`} className="h-full">
<Card className="h-full px-6 py-4 flex flex-col justify-between hover:bg-accent/50 transition-colors cursor-pointer">
<div className="flex items-center justify-between w-full">
<Components.Name id={id} />
<Components.Icon id={id} />
</div>
<div className="flex items-end justify-end gap-2 w-full">
<TagsWithBadge tag_ids={tags} />
</div>
</Card>
</Link>
);
};

View File

@@ -202,6 +202,7 @@ export const DeploymentConfig = ({
], ],
}) })
} }
disabled={disabled}
/> />
), ),
components: { components: {

View File

@@ -19,8 +19,8 @@ export const DeploymentsChart = () => {
const summary = useRead("GetDeploymentsSummary", {}).data; const summary = useRead("GetDeploymentsSummary", {}).data;
return ( return (
<Link to="/deployments" className="w-full"> <Link to="/deployments">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer"> <Card className="hover:bg-accent/50 transition-colors cursor-pointer w-fit">
<CardHeader> <CardHeader>
<div className="flex justify-between"> <div className="flex justify-between">
<div> <div>
@@ -30,9 +30,9 @@ export const DeploymentsChart = () => {
<Rocket className="w-4 h-4" /> <Rocket className="w-4 h-4" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="hidden xl:flex h-[200px] items-center justify-between"> <CardContent className="hidden xl:flex h-[200px] items-center justify-between gap-4">
<div className="flex flex-col gap-2 text-muted-foreground w-full"> <div className="flex flex-col gap-2 text-muted-foreground w-full text-nowrap">
<CardDescription> <CardDescription className="flex items-center gap-2">
<span <span
className={cn( className={cn(
text_color_class_by_intention("Good"), text_color_class_by_intention("Good"),
@@ -43,7 +43,7 @@ export const DeploymentsChart = () => {
</span> </span>
Running Running
</CardDescription> </CardDescription>
<CardDescription> <CardDescription className="flex items-center gap-2">
<span <span
className={cn( className={cn(
text_color_class_by_intention("Critical"), text_color_class_by_intention("Critical"),
@@ -54,7 +54,7 @@ export const DeploymentsChart = () => {
</span> </span>
Stopped Stopped
</CardDescription> </CardDescription>
<CardDescription> <CardDescription className="flex items-center gap-2">
<span <span
className={cn( className={cn(
text_color_class_by_intention("Neutral"), text_color_class_by_intention("Neutral"),
@@ -65,7 +65,7 @@ export const DeploymentsChart = () => {
</span> </span>
Not Deployed Not Deployed
</CardDescription> </CardDescription>
<CardDescription> <CardDescription className="flex items-center gap-2">
<span <span
className={cn( className={cn(
text_color_class_by_intention("Unknown"), text_color_class_by_intention("Unknown"),

View File

@@ -99,6 +99,8 @@ const DeploymentIcon = ({ id, size }: { id?: string; size: number }) => {
}; };
export const DeploymentComponents: RequiredResourceComponents = { export const DeploymentComponents: RequiredResourceComponents = {
list_item: (id) => useDeployment(id),
Dashboard: DeploymentsChart, Dashboard: DeploymentsChart,
New: () => <NewResource type="Deployment" />, New: () => <NewResource type="Deployment" />,
@@ -109,7 +111,6 @@ export const DeploymentComponents: RequiredResourceComponents = {
}, },
Name: ({ id }) => <>{useDeployment(id)?.name}</>, Name: ({ id }) => <>{useDeployment(id)?.name}</>,
name: (id) => useDeployment(id)?.name,
Icon: ({ id }) => <DeploymentIcon id={id} size={4} />, Icon: ({ id }) => <DeploymentIcon id={id} size={4} />,
BigIcon: ({ id }) => <DeploymentIcon id={id} size={8} />, BigIcon: ({ id }) => <DeploymentIcon id={id} size={8} />,

View File

@@ -0,0 +1,117 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { PieChart } from "react-minimal-pie-chart";
import { useRead } from "@lib/hooks";
import { Route } from "lucide-react";
import { Link } from "react-router-dom";
import { cn } from "@lib/utils";
import {
hex_color_by_intention,
text_color_class_by_intention,
} from "@lib/color";
export const ProcedureDashboard = () => {
const summary = useRead("GetProceduresSummary", {}).data;
return (
<Link to="/procedures">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer w-fit">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Procedures</CardTitle>
<CardDescription>{summary?.total} Total</CardDescription>
</div>
<Route className="w-4 h-4" />
</div>
</CardHeader>
<CardContent className="hidden xl:flex h-[200px] items-center justify-between gap-4">
<div className="flex flex-col gap-2 text-muted-foreground w-full text-nowrap">
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Good"),
"font-bold"
)}
>
{summary?.ok}{" "}
</span>
Ok
</CardDescription>
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Warning"),
"font-bold"
)}
>
{summary?.running}{" "}
</span>
Running
</CardDescription>
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Critical"),
"font-bold"
)}
>
{summary?.failed}{" "}
</span>
Failed
</CardDescription>
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Unknown"),
"font-bold"
)}
>
{summary?.unknown}{" "}
</span>
Unknown
</CardDescription>
</div>
<div className="flex justify-end items-center w-full">
<PieChart
className="w-32 h-32"
radius={42}
lineWidth={30}
data={[
{
color: hex_color_by_intention("Good"),
value: summary?.ok ?? 0,
title: "ok",
key: "ok",
},
{
color: hex_color_by_intention("Warning"),
value: summary?.running ?? 0,
title: "running",
key: "running",
},
{
color: hex_color_by_intention("Critical"),
value: summary?.failed ?? 0,
title: "failed",
key: "failed",
},
{
color: hex_color_by_intention("Unknown"),
value: summary?.unknown ?? 0,
title: "unknown",
key: "unknown",
},
]}
/>
</div>
</CardContent>
</Card>
</Link>
);
};

View File

@@ -7,13 +7,18 @@ import { Link } from "react-router-dom";
import { ProcedureConfig } from "./config"; import { ProcedureConfig } from "./config";
import { ProcedureTable } from "./table"; import { ProcedureTable } from "./table";
import { DeleteResource, NewResource } from "../common"; import { DeleteResource, NewResource } from "../common";
import { bg_color_class_by_intention, procedure_state_intention } from "@lib/color"; import {
bg_color_class_by_intention,
procedure_state_intention,
} from "@lib/color";
import { cn } from "@lib/utils"; import { cn } from "@lib/utils";
const useProcedure = (id?: string) => const useProcedure = (id?: string) =>
useRead("ListProcedures", {}).data?.find((d) => d.id === id); useRead("ListProcedures", {}).data?.find((d) => d.id === id);
export const ProcedureComponents: RequiredResourceComponents = { export const ProcedureComponents: RequiredResourceComponents = {
list_item: (id) => useProcedure(id),
Dashboard: () => { Dashboard: () => {
const procedure_count = useRead("ListProcedures", {}).data?.length; const procedure_count = useRead("ListProcedures", {}).data?.length;
return ( return (
@@ -38,7 +43,6 @@ export const ProcedureComponents: RequiredResourceComponents = {
Table: ProcedureTable, Table: ProcedureTable,
Name: ({ id }) => <>{useProcedure(id)?.name}</>, Name: ({ id }) => <>{useProcedure(id)?.name}</>,
name: (id) => useProcedure(id)?.name,
Icon: () => <Route className="w-4" />, Icon: () => <Route className="w-4" />,
BigIcon: () => <Route className="w-8" />, BigIcon: () => <Route className="w-8" />,
@@ -46,7 +50,9 @@ export const ProcedureComponents: RequiredResourceComponents = {
Status: { Status: {
State: ({ id }) => { State: ({ id }) => {
let state = useProcedure(id)?.info.state; let state = useProcedure(id)?.info.state;
const color = bg_color_class_by_intention(procedure_state_intention(state)); const color = bg_color_class_by_intention(
procedure_state_intention(state)
);
return ( return (
<Card className={cn("w-fit", color)}> <Card className={cn("w-fit", color)}>
<CardHeader className="py-0 px-2">{state}</CardHeader> <CardHeader className="py-0 px-2">{state}</CardHeader>

View File

@@ -0,0 +1,117 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { PieChart } from "react-minimal-pie-chart";
import { useRead } from "@lib/hooks";
import { GitBranch } from "lucide-react";
import { Link } from "react-router-dom";
import { cn } from "@lib/utils";
import {
hex_color_by_intention,
text_color_class_by_intention,
} from "@lib/color";
export const RepoDashboard = () => {
const summary = useRead("GetReposSummary", {}).data;
return (
<Link to="/repos">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer w-fit">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Repos</CardTitle>
<CardDescription>{summary?.total} Total</CardDescription>
</div>
<GitBranch className="w-4 h-4" />
</div>
</CardHeader>
<CardContent className="hidden xl:flex h-[200px] items-center justify-between gap-4">
<div className="flex flex-col gap-2 text-muted-foreground w-full text-nowrap">
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Good"),
"font-bold"
)}
>
{summary?.ok}{" "}
</span>
Ok
</CardDescription>
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Warning"),
"font-bold"
)}
>
{(summary?.cloning ?? 0) + (summary?.pulling ?? 0)}{" "}
</span>
Pulling
</CardDescription>
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Critical"),
"font-bold"
)}
>
{summary?.failed}{" "}
</span>
Failed
</CardDescription>
<CardDescription className="flex items-center gap-2">
<span
className={cn(
text_color_class_by_intention("Unknown"),
"font-bold"
)}
>
{summary?.unknown}{" "}
</span>
Unknown
</CardDescription>
</div>
<div className="flex justify-end items-center w-full">
<PieChart
className="w-32 h-32"
radius={42}
lineWidth={30}
data={[
{
color: hex_color_by_intention("Good"),
value: summary?.ok ?? 0,
title: "ok",
key: "ok",
},
{
color: hex_color_by_intention("Warning"),
value: (summary?.cloning ?? 0) + (summary?.pulling ?? 0),
title: "pulling",
key: "pulling",
},
{
color: hex_color_by_intention("Critical"),
value: summary?.failed ?? 0,
title: "failed",
key: "failed",
},
{
color: hex_color_by_intention("Unknown"),
value: summary?.unknown ?? 0,
title: "unknown",
key: "unknown",
},
]}
/>
</div>
</CardContent>
</Card>
</Link>
);
};

View File

@@ -27,6 +27,8 @@ const RepoIcon = ({ id, size }: { id?: string; size: number }) => {
}; };
export const RepoComponents: RequiredResourceComponents = { export const RepoComponents: RequiredResourceComponents = {
list_item: (id) => useRepo(id),
Dashboard: () => { Dashboard: () => {
const repo_count = useRead("ListRepos", {}).data?.length; const repo_count = useRead("ListRepos", {}).data?.length;
return ( return (
@@ -51,7 +53,6 @@ export const RepoComponents: RequiredResourceComponents = {
Table: RepoTable, Table: RepoTable,
Name: ({ id }) => <>{useRepo(id)?.name}</>, Name: ({ id }) => <>{useRepo(id)?.name}</>,
name: (id) => useRepo(id)?.name,
Icon: ({ id }) => <RepoIcon id={id} size={4} />, Icon: ({ id }) => <RepoIcon id={id} size={4} />,
BigIcon: ({ id }) => <RepoIcon id={id} size={8} />, BigIcon: ({ id }) => <RepoIcon id={id} size={8} />,

View File

@@ -24,6 +24,8 @@ export const useServerTemplate = (id?: string) =>
useRead("ListServerTemplates", {}).data?.find((d) => d.id === id); useRead("ListServerTemplates", {}).data?.find((d) => d.id === id);
export const ServerTemplateComponents: RequiredResourceComponents = { export const ServerTemplateComponents: RequiredResourceComponents = {
list_item: (id) => useServerTemplate(id),
Dashboard: () => { Dashboard: () => {
const count = useRead("ListServerTemplates", {}).data?.length; const count = useRead("ListServerTemplates", {}).data?.length;
return ( return (
@@ -91,7 +93,6 @@ export const ServerTemplateComponents: RequiredResourceComponents = {
Table: ServerTemplateTable, Table: ServerTemplateTable,
Name: ({ id }) => <>{useServerTemplate(id)?.name}</>, Name: ({ id }) => <>{useServerTemplate(id)?.name}</>,
name: (id) => useServerTemplate(id)?.name,
Icon: () => <ServerCog className="w-4 h-4" />, Icon: () => <ServerCog className="w-4 h-4" />,
BigIcon: () => <ServerCog className="w-8 h-8" />, BigIcon: () => <ServerCog className="w-8 h-8" />,

View File

@@ -104,6 +104,8 @@ const ConfigOrDeployments = ({ id }: { id: string }) => {
}; };
export const ServerComponents: RequiredResourceComponents = { export const ServerComponents: RequiredResourceComponents = {
list_item: (id) => useServer(id),
Dashboard: ServersChart, Dashboard: ServersChart,
New: () => <NewResource type="Server" />, New: () => <NewResource type="Server" />,
@@ -111,7 +113,6 @@ export const ServerComponents: RequiredResourceComponents = {
Table: ServerTable, Table: ServerTable,
Name: ({ id }: { id: string }) => <>{useServer(id)?.name}</>, Name: ({ id }: { id: string }) => <>{useServer(id)?.name}</>,
name: (id) => useServer(id)?.name,
Icon: ({ id }) => <_ServerIcon id={id} size={4} />, Icon: ({ id }) => <_ServerIcon id={id} size={4} />,
BigIcon: ({ id }) => <_ServerIcon id={id} size={8} />, BigIcon: ({ id }) => <_ServerIcon id={id} size={8} />,

View File

@@ -63,7 +63,7 @@ export function version_is_none({ major, minor, patch }: Types.Version) {
export function resource_name(type: UsableResource, id: string) { export function resource_name(type: UsableResource, id: string) {
const Components = ResourceComponents[type]; const Components = ResourceComponents[type];
return Components.name(id); return Components.list_item(id)?.name;
} }
export const level_to_number = (level: Types.PermissionLevel | undefined) => { export const level_to_number = (level: Types.PermissionLevel | undefined) => {

View File

@@ -1,12 +1,34 @@
import { OpenAlerts } from "@components/alert"; import { OpenAlerts } from "@components/alert";
import { Page } from "@components/layouts"; import { Page } from "@components/layouts";
import { ResourceComponents } from "@components/resources";
import { RecentCard } from "@components/resources/common";
import { AllUpdates } from "@components/updates/resource"; import { AllUpdates } from "@components/updates/resource";
import { useUser } from "@lib/hooks";
import { UsableResource } from "@types";
import { Separator } from "@ui/separator";
export const Dashboard = () => { export const Dashboard = () => {
return ( const user = useUser().data;
return (
<Page title=""> <Page title="">
<OpenAlerts /> <OpenAlerts />
<AllUpdates /> <AllUpdates />
<div className="flex gap-4">
<ResourceComponents.Deployment.Dashboard />
<div className="py-2">
<Separator orientation="vertical" />
</div>
<div className="grid grid-cols-3 gap-4 w-full">
{user?.recent_deployments?.slice(0, 6).map((id) => (
<RecentCard type="Deployment" id={id} />
))}
</div>
</div>
</Page> </Page>
); );
} };
// const ResourceRow = ({ type }: { type: UsableResource }) => {
// const Components =
// }

View File

@@ -1,6 +1,6 @@
import { homeViewAtom } from "@main"; import { homeViewAtom } from "@main";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { Dashboard } from "./dashboard"; import { Dashboard } from "./dashboard2";
import { AllResources } from "./all_resources"; import { AllResources } from "./all_resources";
import { Tree } from "./tree"; import { Tree } from "./tree";
import { useSetTitle } from "@lib/hooks"; import { useSetTitle } from "@lib/hooks";

View File

@@ -6,6 +6,8 @@ type IdComponent = React.FC<{ id: string }>;
type OptionalIdComponent = React.FC<{ id?: string }>; type OptionalIdComponent = React.FC<{ id?: string }>;
export interface RequiredResourceComponents { export interface RequiredResourceComponents {
list_item: (id: string) => Types.ResourceListItem<unknown> | undefined;
/** Summary card for use in dashboard */ /** Summary card for use in dashboard */
Dashboard: React.FC; Dashboard: React.FC;
@@ -17,7 +19,6 @@ export interface RequiredResourceComponents {
/** Name of the resource */ /** Name of the resource */
Name: IdComponent; Name: IdComponent;
name: (id: string) => string | undefined;
/** Icon for the component */ /** Icon for the component */
Icon: OptionalIdComponent; Icon: OptionalIdComponent;