improve dashboard

This commit is contained in:
mbecker20
2024-05-18 02:34:47 -07:00
parent 40027f7430
commit 41a6e0a65a
7 changed files with 252 additions and 235 deletions
+1 -1
View File
@@ -386,6 +386,7 @@ pub enum Operation {
PruneNetworksServer,
CreateNetwork,
DeleteNetwork,
StopAllContainers,
// build
CreateBuild,
@@ -405,7 +406,6 @@ pub enum Operation {
DeleteDeployment,
DeployContainer,
StopContainer,
StopAllContainers,
StartContainer,
RemoveContainer,
RenameDeployment,
@@ -35,8 +35,6 @@ import { NewLayout } from "@components/layouts";
import { Types } from "@monitor/client";
import { ConfigItem, DoubleInput } from "@components/config/util";
import { usableResourcePath } from "@lib/utils";
import { Card } from "@ui/card";
import { TagsWithBadge } from "@components/tags";
export const ResourceDescription = ({
type,
@@ -345,27 +343,3 @@ export const ServerSelector = ({
/>
</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>
);
};
+47 -35
View File
@@ -1,9 +1,12 @@
import { fmt_date_with_minutes } from "@lib/formatting";
import { Types } from "@monitor/client";
import { ColumnDef } from "@tanstack/react-table";
import { DataTable } from "@ui/data-table";
import { useState } from "react";
import { UpdateDetailsInner, UpdateUser } from "./details";
import { bg_color_class_by_intention } from "@lib/color";
import { Card, CardHeader } from "@ui/card";
import { cn } from "@lib/utils";
import { ResourceLink } from "@components/resources/common";
export const UpdatesTable = ({
updates,
@@ -12,46 +15,55 @@ export const UpdatesTable = ({
updates: Types.UpdateListItem[];
showTarget?: boolean;
}) => {
let data: ColumnDef<Types.UpdateListItem, string>[] = [
{
header: "Operation",
accessorKey: "operation",
},
{
header: "Status",
accessorKey: "status",
},
{
header: "Success",
accessorFn: ({ success }) => (success ? "Ok" : "Fail"),
},
{
header: "Start Time",
accessorFn: ({ start_ts }) => fmt_date_with_minutes(new Date(start_ts)),
},
{
header: "Operator",
accessorKey: "operator",
cell: ({ row }) => <UpdateUser user_id={row.original.operator} />,
},
];
// attach the target column on front
if (showTarget) {
data = [
{
header: "Target",
accessorKey: "target.id",
},
...data,
];
}
const [id, setId] = useState("");
return (
<>
<DataTable
tableKey="updates"
data={updates}
columns={data}
columns={[
{
header: "Operation",
accessorKey: "operation",
},
showTarget && {
header: "Target",
cell: ({ row }) =>
row.original.target.type === "System" ? (
"N/A"
) : (
<ResourceLink
type={row.original.target.type}
id={row.original.target.id}
/>
),
},
{
header: "Status",
cell: ({ row }) => {
const color = bg_color_class_by_intention(
row.original.success ? "Good" : "Critical"
);
return (
<Card className={cn("w-fit", color)}>
<CardHeader className="py-0 px-2">
{row.original.success ? "Success" : "Fail"}
</CardHeader>
</Card>
);
},
},
{
header: "Start Time",
accessorFn: ({ start_ts }) =>
fmt_date_with_minutes(new Date(start_ts)),
},
{
header: "Operator",
accessorKey: "operator",
cell: ({ row }) => <UpdateUser user_id={row.original.operator} />,
},
]}
onRowClick={(row) => setId(row.id)}
/>
<UpdateDetailsInner id={id} open={!!id} setOpen={() => setId("")} />
+59 -105
View File
@@ -1,130 +1,84 @@
import { Page, Section } from "@components/layouts";
import { Box, Key, Tag } from "lucide-react";
import { Link } from "react-router-dom";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { ResourceComponents } from "@components/resources";
import { OpenAlerts } from "@components/alert";
import { useRead } from "@lib/hooks";
import { Page } from "@components/layouts";
import { ResourceComponents } from "@components/resources";
import { TagsWithBadge } from "@components/tags";
import { AllUpdates } from "@components/updates/resource";
import { useRead, useUser } from "@lib/hooks";
import { usableResourcePath } from "@lib/utils";
import { UsableResource } from "@types";
import { Card } from "@ui/card";
import { Separator } from "@ui/separator";
import { History } from "lucide-react";
import { Link } from "react-router-dom";
export const Dashboard = () => {
return (
<Page title="">
<OpenAlerts />
<AllUpdates />
{/* <RecentlyViewed /> */}
<Resources />
<ResourceRow type="Deployment" />
<ResourceRow type="Build" />
<ResourceRow type="Repo" />
<ResourceRow type="Server" />
<ResourceRow type="Procedure" />
</Page>
);
};
const Resources = () => {
const ResourceRow = ({ type }: { type: UsableResource }) => {
const recents = useUser().data?.[`recent_${type.toLowerCase()}s`]?.slice(
0,
6
) as string[] | undefined;
const resources = useRead(`List${type}s`, {})
.data?.filter((r) => !recents?.includes(r.id))
.map((r) => r.id);
const ids = [
...(recents ?? []),
...(resources?.slice(0, 6 - (recents?.length || 0)) ?? []),
];
if (ids.length === 0) return;
const Components = ResourceComponents[type];
return (
<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">
<ResourceComponents.Server.Dashboard />
<ResourceComponents.Deployment.Dashboard />
<div className="flex gap-4">
<Components.Dashboard />
<div className="py-2">
<Separator orientation="vertical" />
</div>
<div className="flex flex-col gap-4 w-full pb-1">
<div className="flex gap-2 items-center text-muted-foreground">
<History className="w-4 h-4" />
<h3>Recent {type}s</h3>
</div>
<div className="w-full lg:max-w-[50%]">
<ResourceComponents.Build.Dashboard />
<div className="grid grid-cols-3 grid-rows-2 gap-4 w-full h-full">
{ids.map((id: string) => (
<RecentCard key={type + id} type={type} id={id} />
))}
</div>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full">
<ResourceComponents.Procedure.Dashboard />
<ResourceComponents.Repo.Dashboard />
</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">
<ResourceComponents.Alerter.Dashboard />
<ResourceComponents.Builder.Dashboard />
</div>
<div className="flex flex-col md:flex-row gap-4 w-full">
<TagsSummary />
<ApiKeysSummary />
</div>
</div>
</Section>
</div>
);
};
// const RecentlyViewed = () => {
// const nav = useNavigate();
// const recently_viewed = useUser().data?.recently_viewed;
// const checkResourceExists = useCheckResourceExists();
// return (
// <Section
// title="Recently Viewed"
// icon={<History className="w-4 h-4" />}
// actions=""
// >
// <div className="grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4">
// {recently_viewed
// ?.filter(checkResourceExists)
// .slice(0, 8)
// .map(({ type, id }, i) => (
// <Fragment key={type + id}>
// {type !== "System" && (
// <Card
// onClick={() => nav(`/${usableResourcePath(type)}/${id}`)}
// className={cn(
// "px-3 py-2 h-fit hover:bg-accent/50 group-focus:bg-accent/50 transition-colors cursor-pointer",
// i > 5 && "hidden 2xl:flex"
// )}
// >
// <CardContent className="flex items-center justify-between gap-4 px-3 py-2 text-sm text-muted-foreground w-full">
// <ResourceLink type={type} id={id} />
// {type}
// </CardContent>
// </Card>
// )}
// </Fragment>
// ))}
// </div>
// </Section>
// );
// };
const TagsSummary = () => {
const tags_count = useRead("ListTags", {}).data?.length;
const RecentCard = ({ type, id }: { type: UsableResource; id: string }) => {
const Components = ResourceComponents[type];
const tags = Components.list_item(id)?.tags;
return (
<Link to="/tags" className="w-full">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Tags</CardTitle>
<CardDescription>{tags_count} Total</CardDescription>
</div>
<Tag className="w-4 h-4" />
<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">
<div className="flex items-center gap-2">
<Components.Icon id={id} />
<Components.Name id={id} />
</div>
</CardHeader>
</Card>
</Link>
);
};
const ApiKeysSummary = () => {
const keys_count = useRead("ListApiKeys", {}).data?.length;
return (
<Link to="/keys" className="w-full">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Api Keys</CardTitle>
<CardDescription>{keys_count} Total</CardDescription>
</div>
<Key className="w-4 h-4" />
<div className="text-sm">
<Components.Status.State id={id} />
</div>
</CardHeader>
</div>
<div className="flex items-end justify-end gap-2 w-full">
<TagsWithBadge tag_ids={tags} />
</div>
</Card>
</Link>
);
-58
View File
@@ -1,58 +0,0 @@
import { OpenAlerts } from "@components/alert";
import { Page } from "@components/layouts";
import { ResourceComponents } from "@components/resources";
import { RecentCard } from "@components/resources/common";
import { AllUpdates } from "@components/updates/resource";
import { useRead, useUser } from "@lib/hooks";
import { UsableResource } from "@types";
import { Separator } from "@ui/separator";
import { History } from "lucide-react";
export const Dashboard = () => {
return (
<Page title="">
<OpenAlerts />
<AllUpdates />
<ResourceRow type="Deployment" />
<ResourceRow type="Build" />
<ResourceRow type="Repo" />
<ResourceRow type="Server" />
<ResourceRow type="Procedure" />
</Page>
);
};
const ResourceRow = ({ type }: { type: UsableResource }) => {
const recents = useUser().data?.[`recent_${type.toLowerCase()}s`]?.slice(
0,
6
) as string[] | undefined;
const resources = useRead(`List${type}s`, {})
.data?.filter((r) => !recents?.includes(r.id))
.map((r) => r.id);
const ids = [
...(recents ?? []),
...(resources?.slice(0, 6 - (recents?.length || 0)) ?? []),
];
const Components = ResourceComponents[type];
return (
<div className="flex gap-4">
<Components.Dashboard />
<div className="py-2">
<Separator orientation="vertical" />
</div>
<div className="flex flex-col gap-4 w-full pb-1">
<div className="flex gap-2 items-center text-muted-foreground">
<History className="w-4 h-4" />
<h3>Recent {type}s</h3>
</div>
<div className="grid grid-cols-3 grid-rows-2 gap-4 w-full h-full">
{ids.map((id: string) => (
<RecentCard key={type + id} type={type} id={id} />
))}
</div>
</div>
</div>
);
};
+1 -1
View File
@@ -1,6 +1,6 @@
import { homeViewAtom } from "@main";
import { useAtom } from "jotai";
import { Dashboard } from "./dashboard2";
import { Dashboard } from "./dashboard";
import { AllResources } from "./all_resources";
import { Tree } from "./tree";
import { useSetTitle } from "@lib/hooks";
+144 -9
View File
@@ -1,9 +1,21 @@
import { Page } from "@components/layouts";
import { ResourceComponents } from "@components/resources";
import { AddTags, ResourceTags } from "@components/tags";
import { UpdatesTable } from "@components/updates/table";
import { useRead, useResourceParamType, useSetTitle } from "@lib/hooks";
import { Types } from "@monitor/client";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { UsableResource } from "@types";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import { SearchX } from "lucide-react";
import { useState } from "react";
import { useParams } from "react-router-dom";
export const Updates = () => {
@@ -18,10 +30,16 @@ export const Updates = () => {
const AllUpdates = () => {
useSetTitle("Updates");
const updates = useRead("ListUpdates", {}).data;
const [operation, setOperation] = useState<Types.Operation | undefined>();
const updates = useRead("ListUpdates", { query: { operation } }).data;
return (
<Page title="Updates">
<UpdatesTable updates={updates?.updates ?? []} />
<Page
title="Updates"
actions={
<OperationSelector selected={operation} onSelect={setOperation} />
}
>
<UpdatesTable updates={updates?.updates ?? []} showTarget />
</Page>
);
};
@@ -35,24 +53,141 @@ const ResourceUpdates = ({
}) => {
const name = useRead(`List${type}s`, {}).data?.find((r) => r.id === id)?.name;
useSetTitle(name && `${name} | Updates`);
const [operation, setOperation] = useState<Types.Operation | undefined>();
const updates = useRead("ListUpdates", {
query: {
"target.type": type,
"target.id": id,
operation,
},
}).data;
const Components = ResourceComponents[type];
return (
<Page
title={<Components.Name id={id} />}
titleRight={
<div className="flex gap-2">
<ResourceTags target={{ id, type }} click_to_delete />
<AddTags target={{ id, type }} />
</div>
titleRight={<h2 className="text-muted-foreground">Updates</h2>}
icon={<Components.BigIcon id={id} />}
actions={
<OperationSelector
selected={operation}
onSelect={setOperation}
options={OPERATIONS_BY_RESOURCE[type]}
/>
}
>
<UpdatesTable updates={updates?.updates ?? []} />
</Page>
);
};
const OPERATIONS_BY_RESOURCE: { [key: string]: Types.Operation[] } = {
Server: [
Types.Operation.CreateServer,
Types.Operation.UpdateServer,
Types.Operation.RenameServer,
Types.Operation.PruneImagesServer,
Types.Operation.PruneContainersServer,
Types.Operation.PruneNetworksServer,
Types.Operation.CreateNetwork,
Types.Operation.DeleteNetwork,
Types.Operation.StopAllContainers,
],
Deployment: [
Types.Operation.CreateDeployment,
Types.Operation.UpdateDeployment,
Types.Operation.RenameDeployment,
Types.Operation.DeployContainer,
Types.Operation.StopContainer,
Types.Operation.StartContainer,
Types.Operation.RemoveContainer,
],
Build: [
Types.Operation.CreateBuild,
Types.Operation.UpdateBuild,
Types.Operation.RunBuild,
Types.Operation.CancelBuild,
],
Repo: [
Types.Operation.CreateRepo,
Types.Operation.UpdateRepo,
Types.Operation.CloneRepo,
Types.Operation.PullRepo,
],
Procedure: [
Types.Operation.CreateProcedure,
Types.Operation.UpdateProcedure,
Types.Operation.RunProcedure,
],
Builder: [Types.Operation.CreateBuilder, Types.Operation.UpdateBuilder],
Alerter: [Types.Operation.CreateAlerter, Types.Operation.UpdateAlerter],
ServerTemplate: [
Types.Operation.CreateServerTemplate,
Types.Operation.UpdateServerTemplate,
Types.Operation.LaunchServer,
],
};
const OperationSelector = ({
selected,
onSelect,
options = Object.values(Types.Operation).filter(
(o) => o !== Types.Operation.None
),
}: {
selected: Types.Operation | undefined;
onSelect: (operation: Types.Operation | undefined) => void;
options?: Types.Operation[];
}) => {
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<div className="h-full w-[200px] cursor-pointer flex items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1">
{selected ?? "Select Operation"}
<CaretSortIcon className="h-4 w-4 opacity-50" />
</div>
</PopoverTrigger>
<PopoverContent align="end" className="w-[200px] max-h-[200px] p-0">
<Command>
<CommandInput
placeholder="Search Operations"
value={input}
onValueChange={setInput}
className="h-9"
/>
<CommandList>
<CommandEmpty className="flex justify-evenly items-center">
No Operations Found
<SearchX className="w-3 h-3" />
</CommandEmpty>
<CommandGroup>
<CommandItem
className="cursor-pointer"
onSelect={() => {
onSelect(undefined);
setOpen(false);
}}
>
<div>All</div>
</CommandItem>
{options.map((operation) => (
<CommandItem
className="cursor-pointer"
onSelect={() => {
onSelect(operation);
setOpen(false);
}}
>
{operation}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};