refactor useRead, improve dx alot

This commit is contained in:
karamvir
2023-07-25 02:39:15 -07:00
parent b31f74f0c5
commit 0db97fd5d5
20 changed files with 223 additions and 131 deletions
+3 -1
View File
@@ -7,6 +7,7 @@ import { Link, useLocation, useParams } from "react-router-dom";
import { useUser } from "@hooks";
import { ServerName } from "@resources/server/util";
import { DeploymentName } from "@resources/deployment/util";
import { DesktopUpdates } from "@components/updates/desktop";
export const Paths = () => {
const path = useLocation().pathname.split("/")[1];
@@ -61,12 +62,13 @@ export const Header = () => {
</Link>
<Paths />
</div>
<div className="flex">
<div className="flex ">
{user && (
<Button disabled variant="ghost">
<Circle className="w-4 h-4 fill-green-500 stroke-none" />
</Button>
)}
{user && <DesktopUpdates />}
<ThemeToggle />
{user && (
<Button variant="ghost" onClick={logout}>
@@ -0,0 +1,31 @@
import { useRead } from "@hooks";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@ui/dropdown";
import { Bell } from "lucide-react";
import { SingleUpdate } from "./updates";
import { Button } from "@ui/button";
export const DesktopUpdates = () => {
const updates = useRead("ListUpdates", {}).data;
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="ghost">
<Bell className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{updates?.map((update) => (
<DropdownMenuItem key={update._id?.$oid}>
<SingleUpdate update={update} />
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};
+3 -2
View File
@@ -22,12 +22,13 @@ import {
CardHeader,
CardTitle,
} from "@ui/card";
// import { useRead } from "@hooks";
export const UpdateUser = ({ userId }: { userId: string }) => {
// const { data } = useUpdateUser(userId);
// const { data } = useRead({ type: "GetUser", params: {} });
if (userId === "github") return <>GitHub</>;
if (userId === "auto redeploy") return <>Auto Redeploy</>;
return <>{userId}</>;
return <>{userId.slice(0, 5)}...</>;
};
export const UpdateDetails = ({ update }: { update: Update }) => {
+27 -58
View File
@@ -1,62 +1,31 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { Types } from "@monitor/client";
import { cn, version_to_string } from "@util/helpers";
import { version_to_string } from "@util/helpers";
import { Calendar, User } from "lucide-react";
import { UpdateDetails, UpdateUser } from "./update";
import { Update } from "@monitor/client/dist/types";
export const Updates = ({
updates,
className,
}: {
updates?: Types.Update[];
className?: string;
}) => (
<Card className={cn("w-full h-fit", className)}>
<CardHeader className="flex-row justify-between">
<CardTitle className="text-xl">Updates</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-2 max-h-[50vh] overflow-y-auto">
{updates?.map((update) => (
<Card key={update._id?.$oid}>
<CardHeader className="flex flex-row items-end md:items-center justify-between gap-4">
<div
className="flex flex-col md:flex-row justify-between items-center w-full"
style={{ placeItems: "center start" }}
>
<CardTitle className="whitespace-nowrap w-full">
{update.operation
.split("_")
.map((s) => s[0].toUpperCase() + s.slice(1))
.join(" ")}{" "}
{version_to_string(update.version)}
</CardTitle>
<div className="flex flex-col-reverse md:flex-row md:gap-4 w-full">
<div className="flex gap-2 items-center md:w-[200px]">
<Calendar className="w-4 h-4" />
<CardDescription className="text-xs md:text-sm">
{update.end_ts
? new Date(update.end_ts).toLocaleString()
: "ongoing"}
</CardDescription>
</div>
<div className="flex gap-2 items-center">
<User className="w-4 h-4" />
<CardDescription>
<UpdateUser userId={update.operator} />
</CardDescription>
</div>
</div>
</div>
<UpdateDetails update={update} />
</CardHeader>
</Card>
))}
</CardContent>
</Card>
export const SingleUpdate = ({ update }: { update: Update }) => (
<div className="flex items-center justify-between">
<div>
{update.operation
.split("_")
.map((s) => s[0].toUpperCase() + s.slice(1))
.join(" ")}{" "}
{version_to_string(update.version)}
</div>
<div>
<div className="flex gap-2 items-center md:w-[200px]">
<Calendar className="w-4 h-4" />
<div className="text-xs md:text-sm">
{update.end_ts ? new Date(update.end_ts).toLocaleString() : "ongoing"}
</div>
</div>
</div>
<div className="flex gap-2 items-center">
<User className="w-4 h-4" />
<div>
<UpdateUser userId={update.operator} />
</div>
</div>
<UpdateDetails update={update} />
</div>
);
+25 -3
View File
@@ -3,6 +3,7 @@ import { client } from "./main";
import {
useQuery,
useMutation,
UseQueryOptions,
UseMutationOptions,
} from "@tanstack/react-query";
import { useAtomValue, useSetAtom } from "jotai";
@@ -10,11 +11,32 @@ import { atomWithStorage } from "jotai/utils";
import { useNavigate } from "react-router-dom";
import {
ExecuteResponses,
ReadResponses,
WriteResponses,
} from "@monitor/client/dist/responses";
export const useRead = <T extends Types.ReadRequest>(req: T) =>
useQuery([req], () => client.read(req));
export const useRead = <
T extends Types.ReadRequest["type"],
P = Extract<Types.WriteRequest, { type: T }>["params"]
>(
type: T,
params: P,
config?: Omit<
UseQueryOptions<ReadResponses[T], unknown, ReadResponses[T], (T | P)[]>,
"initialData" | "queryFn" | "queryKey"
>
) =>
useQuery(
[type, params],
async () =>
(await client.read({ type, params } as any)) as ReadResponses[T],
config
);
// export const useRead = <T extends Types.ReadRequest>(
// req: T,
// options?: UseQueryOptions
// ) => useQuery([req], () => client.read(req), options);
export const useWrite = <
T extends Types.WriteRequest["type"],
@@ -50,7 +72,7 @@ export const useExecute = <
config
);
export const useUser = () => useRead({ type: "GetUser", params: {} });
export const useUser = () => useRead("GetUser", {});
export const useLogin = () => {
const { refetch } = useUser();
@@ -12,10 +12,7 @@ import { useRead } from "@hooks";
import { DockerContainerState } from "@monitor/client/dist/types";
export const DeploymentsChart = () => {
const { data, isLoading, isError } = useRead({
type: "ListDeployments",
params: {},
});
const { data, isLoading, isError } = useRead("ListDeployments", {});
const running = data?.filter(
(d) => d.state === DockerContainerState.Running
@@ -12,10 +12,7 @@ import { useRead } from "@hooks";
import { ServerStatus } from "@monitor/client/dist/types";
export const ServersChart = () => {
const { data, isLoading, isError } = useRead({
type: "ListServers",
params: {},
});
const { data, isLoading, isError } = useRead("ListServers", {});
const running = data?.filter((d) => d.status === ServerStatus.Ok).length;
const stopped = data?.filter((d) => d.status === ServerStatus.NotOk).length;
+16 -9
View File
@@ -10,26 +10,33 @@ export const Dashboard = () => {
<div className="flex flex-col gap-24">
<RecentlyViewed />
<div className="flex flex-col gap-6 w-full">
{/* <h1 className="text-4xl"> All Resources </h1> */}
<div>
<div className="flex items-center gap-2 text-muted-foreground">
<Box className="w-4 h-4" />
<h2 className="text-xl">My Resources</h2>
</div>
</div>
<div className="flex gap-4">
<div className="flex gap-4 w-full h-fit">
<DeploymentsChart />
<ServersChart />
</div>
<Link to="/builds" className="w-full max-w-[50%] h-full">
<Card hoverable>
<CardHeader>
<CardTitle>Builds</CardTitle>
</CardHeader>
</Card>
</Link>
<div className="flex gap-4 w-full h-fit">
<Link to="/builds" className="w-full max-w-[50%] h-full">
<Card hoverable>
<CardHeader>
<CardTitle>Builds</CardTitle>
</CardHeader>
</Card>
</Link>
<Link to="/builders" className="w-full max-w-[50%] h-full">
<Card hoverable>
<CardHeader>
<CardTitle>Builders</CardTitle>
</CardHeader>
</Card>
</Link>
</div>
</div>
</div>
</div>
+1 -1
View File
@@ -13,7 +13,7 @@ import { BuildInfo } from "./util";
import { Hammer } from "lucide-react";
export const BuildCard = ({ id }: { id: string }) => {
const builds = useRead({ type: "ListBuilds", params: {} }).data;
const builds = useRead("ListBuilds", {}).data;
const build = builds?.find((server) => server.id === id);
if (!build) return null;
+4 -4
View File
@@ -4,25 +4,25 @@ import { version_to_string } from "@util/helpers";
import { Factory, History } from "lucide-react";
export const BuildName = ({ id }: { id: string }) => {
const builds = useRead({ type: "ListBuilds", params: {} }).data;
const builds = useRead("ListBuilds", {}).data;
const build = builds?.find((b) => b.id === id);
return <>{build?.name ?? "..."}</>;
};
export const BuildVersion = ({ id }: { id: string }) => {
const builds = useRead({ type: "ListBuilds", params: {} }).data;
const builds = useRead("ListBuilds", {}).data;
const build = builds?.find((b) => b.id === id);
return <>{version_to_string(build?.version) ?? "..."}</>;
};
export const BuildBuilder = ({ id }: { id: string }) => {
const builds = useRead({ type: "ListBuilds", params: {} }).data;
const builds = useRead("ListBuilds", {}).data;
const build = builds?.find((b) => b.id === id);
return <>{"build.builder " + build?.id ?? "..."}</>;
};
export const BuildLastBuilt = ({ id }: { id: string }) => {
const builds = useRead({ type: "ListBuilds", params: {} }).data;
const builds = useRead("ListBuilds", {}).data;
const build = builds?.find((b) => b.id === id);
const last = build?.last_built_at;
return <>{last ? new Date(last).toLocaleString() : "not yet built"}</>;
+36
View File
@@ -0,0 +1,36 @@
import { useRead } from "@hooks";
import { ServerStatusIcon } from "@resources/server/util";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@ui/card";
import { Link } from "react-router-dom";
import { Factory } from "lucide-react";
export const BuilderCard = ({ id }: { id: string }) => {
const builders = useRead("ListBuilders", {}).data;
const builder = builders?.find((builder) => builder._id?.$oid === id);
if (!builder) return null;
return (
<Link to={`/builds/${builder._id?.$oid}`} key={builder._id?.$oid}>
<Card hoverable>
<CardHeader className="flex flex-row justify-between">
<div>
<CardTitle>{builder.name}</CardTitle>
<CardDescription> </CardDescription>
</div>
<ServerStatusIcon serverId={builder._id?.$oid} />
</CardHeader>
<CardContent className="flex items-center gap-4">
<Factory className="w-4 h-4" />
<div className="border h-6" />
<div>{builder.description}</div>
</CardContent>
</Card>
</Link>
);
};
+1 -1
View File
@@ -11,7 +11,7 @@ import { DeploymentInfo, DeploymentStatusIcon } from "./util";
import { Rocket } from "lucide-react";
export const DeploymentCard = ({ id }: { id: string }) => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployments = useRead("ListDeployments", {}).data;
const deployment = deployments?.find((d) => d.id === id);
if (!deployment) return null;
return (
@@ -20,7 +20,7 @@ interface DeploymentId {
export const RedeployContainer = ({ deployment_id }: DeploymentId) => {
const { mutate, isLoading } = useExecute("Deploy");
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployments = useRead("ListDeployments", {}).data;
const deployment = deployments?.find((d) => d.id === deployment_id);
return (
<ActionButton
@@ -61,8 +61,8 @@ const StopContainer = ({ deployment_id }: DeploymentId) => {
};
export const StartOrStopContainer = ({ deployment_id }: DeploymentId) => {
const { data } = useRead({ type: "ListDeployments", params: {} });
const deployment = data?.find((d) => d.id == deployment_id);
const deployments = useRead("ListDeployments", {}).data;
const deployment = deployments?.find((d) => d.id === deployment_id);
if (deployment?.state === DockerContainerState.Running)
return <StopContainer deployment_id={deployment_id} />;
return <StartContainer deployment_id={deployment_id} />;
@@ -83,7 +83,8 @@ export const RemoveContainer = ({ deployment_id }: DeploymentId) => {
export const DeleteDeployment = ({ id }: { id: string }) => {
const nav = useNavigate();
const { data } = useRead({ type: "GetDeployment", params: { id } });
const deployments = useRead("ListDeployments", {}).data;
const deployment = deployments?.find((d) => d.id === id);
const { mutate, isLoading } = useWrite("DeleteDeployment", {
onSuccess: () => nav("/deployments"),
});
@@ -107,7 +108,7 @@ export const DeleteDeployment = ({ id }: { id: string }) => {
<div className="flex flex-col gap-2">
<p>
Are you sure you wish to delete this deployment? If so, please
type in <b>{data?.name}</b> below
type in <b>{deployment?.name}</b> below
</p>
<Input value={name} onChange={(e) => setName(e.target.value)} />
</div>
@@ -115,7 +116,7 @@ export const DeleteDeployment = ({ id }: { id: string }) => {
<Button
variant="outline"
intent="danger"
disabled={name !== data?.name || isLoading}
disabled={name !== deployment?.name || isLoading}
onClick={() => mutate({ id })}
>
Delete
@@ -12,11 +12,12 @@ const scroll_to_bottom = (id: string) => () =>
.getElementById(id)
?.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
export const DeploymentLogs = ({ deploymentId }: { deploymentId: string }) => {
const { data, refetch } = useRead({
type: "GetLog",
params: { deployment_id: deploymentId, tail: 200 },
});
export const DeploymentLogs = ({
deployment_id,
}: {
deployment_id: string;
}) => {
const { data, refetch } = useRead("GetLog", { deployment_id, tail: 200 });
useEffect(() => {
const handle = setInterval(() => refetch(), 30000);
+4 -4
View File
@@ -7,7 +7,7 @@ export const DeploymentName = ({
}: {
deploymentId: string | undefined;
}) => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployments = useRead("ListDeployments", {}).data;
const deployment = deployments?.find((d) => d.id === deploymentId);
return <>{deployment?.name ?? "..."}</>;
};
@@ -17,7 +17,7 @@ export const DeploymentStatus = ({
}: {
deploymentId: string | undefined;
}) => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployments = useRead("ListDeployments", {}).data;
const deployment = deployments?.find((d) => d.id === deploymentId);
return <>{deployments ? deployment?.status ?? "not deployed" : "..."}</>;
};
@@ -27,7 +27,7 @@ export const DeploymentStatusIcon = ({
}: {
deploymentId: string | undefined;
}) => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployments = useRead("ListDeployments", {}).data;
const deployment = deployments?.find((d) => d.id === deploymentId);
return (
<Circle
@@ -42,7 +42,7 @@ export const DeploymentStatusIcon = ({
};
export const DeploymentInfo = ({ deploymentId }: { deploymentId: string }) => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployments = useRead("ListDeployments", {}).data;
const deployment = deployments?.find((d) => d.id === deploymentId);
return (
+37 -14
View File
@@ -1,13 +1,14 @@
import { Factory, Hammer, Rocket, Server } from "lucide-react";
import { useRead } from "@hooks";
import { Resources } from "@layouts/resources";
import { DeploymentCard } from "./deployment/card";
import { BuildCard } from "./build/card";
import { ServerCard } from "./server/card";
import { Hammer, Rocket, Server } from "lucide-react";
import { BuilderCard } from "./builder/card";
export const Deployments = () => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const summary = useRead({ type: "GetDeploymentsSummary", params: {} }).data;
const deployments = useRead("ListDeployments", {}).data;
const summary = useRead("GetDeploymentsSummary", {}).data;
return (
<Resources
@@ -27,9 +28,30 @@ export const Deployments = () => {
);
};
export const Servers = () => {
const servers = useRead("ListServers", {}).data;
const summary = useRead("GetServersSummary", {}).data;
return (
<Resources
type="Server"
info={`${summary?.total} Total, ${summary?.healthy} Healthy, ${summary?.unhealthy} Unhealthy`}
icon={<Server className="w-6 h-6" />}
components={(search) => (
<>
{servers
?.filter((d) => d.name.includes(search) || search.includes(d.name))
.map(({ id }) => (
<ServerCard key={id} id={id} />
))}
</>
)}
/>
);
};
export const Builds = () => {
const builds = useRead({ type: "ListBuilds", params: {} }).data;
const summary = useRead({ type: "GetBuildsSummary", params: {} }).data;
const builds = useRead("ListBuilds", {}).data;
const summary = useRead("GetBuildsSummary", {}).data;
return (
<Resources
@@ -49,20 +71,21 @@ export const Builds = () => {
);
};
export const Servers = () => {
const servers = useRead({ type: "ListServers", params: {} }).data;
const summary = useRead({ type: "GetServersSummary", params: {} }).data;
export const Builders = () => {
const builders = useRead("ListBuilders", {}).data;
const summary = useRead("GetBuildersSummary", {}).data;
return (
<Resources
type="Server"
info={`${summary?.total} Total, ${summary?.healthy} Healthy, ${summary?.unhealthy} Unhealthy`}
icon={<Server className="w-6 h-6" />}
type="Builder"
info={`${summary?.total} Total`}
icon={<Factory className="w-6 h-6" />}
components={(search) => (
<>
{servers
{builders
?.filter((d) => d.name.includes(search) || search.includes(d.name))
.map(({ id }) => (
<ServerCard key={id} id={id} />
.map(({ _id }) => (
<BuilderCard key={_id?.$oid} id={_id?.$oid!} />
))}
</>
)}
+2 -2
View File
@@ -11,7 +11,7 @@ import { ServerStatusIcon, ServerStats } from "./util";
import { Server } from "lucide-react";
export const ServerCard = ({ id }: { id: string }) => {
const servers = useRead({ type: "ListServers", params: {} }).data;
const servers = useRead("ListServers", {}).data;
const server = servers?.find((server) => server.id === id);
if (!server) return null;
@@ -28,7 +28,7 @@ export const ServerCard = ({ id }: { id: string }) => {
<CardContent className="flex items-center gap-4">
<Server className="w-4 h-4" />
<div className="border h-6" />
<ServerStats serverId={server.id} />
<ServerStats server_id={server.id} />
</CardContent>
</Card>
</Link>
+6 -9
View File
@@ -5,17 +5,17 @@ import { Circle, Cpu, Database, MemoryStick } from "lucide-react";
import { useEffect } from "react";
export const ServerName = ({ serverId }: { serverId: string | undefined }) => {
const servers = useRead({ type: "ListServers", params: {} }).data;
const servers = useRead("ListServers", {}).data;
const server = servers?.find((s) => s.id === serverId);
return <>{server?.name ?? "..."}</>;
};
export const ServerInfo = ({ serverId }: { serverId: string | undefined }) => {
const servers = useRead({ type: "ListServers", params: {} }).data;
const servers = useRead("ListServers", {}).data;
const server = servers?.find((s) => s.id === serverId);
return (
<div className="flex items-center gap-4 text-muted-foreground">
{serverId && <ServerStats serverId={serverId} />}
{serverId && <ServerStats server_id={serverId} />}
<div>|</div>
<div className="flex items-center gap-4">
<div> Status: {server?.status}</div>
@@ -25,11 +25,8 @@ export const ServerInfo = ({ serverId }: { serverId: string | undefined }) => {
);
};
export const ServerStats = ({ serverId }: { serverId: string }) => {
const { data, refetch } = useRead({
type: "GetBasicSystemStats",
params: { server_id: serverId },
});
export const ServerStats = ({ server_id }: { server_id: string }) => {
const { data, refetch } = useRead("GetBasicSystemStats", { server_id });
useEffect(() => {
const handle = setInterval(() => refetch(), 30000);
@@ -63,7 +60,7 @@ export const ServerStatusIcon = ({
serverId: string | undefined;
sm?: boolean;
}) => {
const servers = useRead({ type: "ListServers", params: {} }).data;
const servers = useRead("ListServers", {}).data;
const server = servers?.find((s) => s.id === serverId);
return (
<Circle
+12 -4
View File
@@ -6,7 +6,7 @@ import { Dashboard } from "@pages/dashboard";
import { Deployment } from "@resources/deployment/page";
import { Server } from "@resources/server/page";
import { Build } from "@resources/build/page";
import { Deployments, Builds, Servers } from "@resources/pages";
import { Deployments, Builds, Servers, Builders } from "@resources/pages";
const router = createBrowserRouter([
{
@@ -24,6 +24,14 @@ const router = createBrowserRouter([
{ path: ":deploymentId", element: <Deployment /> },
],
},
{
path: "servers",
children: [
{ path: "", element: <Servers /> },
{ path: ":serverId", element: <Server /> },
],
},
{
path: "builds",
children: [
@@ -32,10 +40,10 @@ const router = createBrowserRouter([
],
},
{
path: "servers",
path: "builders",
children: [
{ path: "", element: <Servers /> },
{ path: ":serverId", element: <Server /> },
{ path: "", element: <Builders /> },
// { path: ":builderId", element: <Build /> },
],
},
],