add single deployment page etc

This commit is contained in:
karamvir
2023-07-23 23:31:19 -07:00
parent b57688033e
commit 6c41362bfa
12 changed files with 611 additions and 47 deletions
+12 -3
View File
@@ -2,13 +2,14 @@ import { ThemeToggle } from "@components/util";
import { Button } from "@ui/button";
import { ChevronRight, Circle, LogOut } from "lucide-react";
import { Link, useLocation } from "react-router-dom";
import { Link, useLocation, useParams } from "react-router-dom";
import { useUser } from "@hooks";
import { ServerName } from "@pages/server";
export const Paths = () => {
const location = useLocation();
const path = location.pathname.split("/")[1];
const path = useLocation().pathname.split("/")[1];
const { serverId } = useParams();
return (
<div className="hidden md:flex items-center gap-2">
@@ -25,6 +26,14 @@ export const Paths = () => {
</Link>
</>
)}
{serverId && (
<>
<ChevronRight className="w-4 h-4" />
<Link to={`/servers/${serverId}`}>
<ServerName serverId={serverId} />
</Link>
</>
)}
</div>
);
};
+126
View File
@@ -0,0 +1,126 @@
import { Button } from "@ui/button";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@ui/sheet";
import { Update } from "@monitor/client/dist/types";
import {
readableDuration,
readableVersion,
version_to_string,
} from "@util/helpers";
import { Calendar, Clock, Milestone, Search, User } from "lucide-react";
// import { UpdateUser } from ".";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { useRead } from "@hooks";
export const UpdateUser = ({ userId }: { userId: string }) => {
// const { data } = useUpdateUser(userId);
if (userId === "github") return <>GitHub</>;
if (userId === "auto redeploy") return <>Auto Redeploy</>;
return <>{userId}</>;
};
export const UpdateDetails = ({ update }: { update: Update }) => {
return (
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 w-full max-w-fit"
>
<Search className="w-4 h-4" />
</Button>
</SheetTrigger>
<SheetContent position="right" size="lg">
<SheetHeader className="mb-4">
<SheetTitle>
{update.operation
.split("_")
.map((s) => s[0].toUpperCase() + s.slice(1))
.join(" ")}{" "}
{version_to_string(update.version)}
</SheetTitle>
<SheetDescription className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
{new Date(update.start_ts).toLocaleString()}
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
{update.end_ts
? readableDuration(update.start_ts, update.end_ts)
: "ongoing"}
</div>
<div className="flex items-center gap-2">
<User className="w-4 h-4" />
<UpdateUser userId={update.operator} />
</div>
{update.version && (
<div className="flex items-center gap-2">
<Milestone className="w-4 h-4" />
{readableVersion(update.version)}
</div>
)}
</SheetDescription>
</SheetHeader>
<div className="max-h-[80vh] overflow-y-auto grid gap-2">
{update.logs.map((log, i) => (
<Card>
<CardHeader>
<CardTitle>{log.stage}</CardTitle>
<CardDescription className="flex gap-2">
<span>
Stage {i + 1} of {update.logs.length}
</span>
<span>|</span>
<span className="flex items-center gap-2">
<Clock className="w-4 h-4" />
{readableDuration(log.start_ts, log.end_ts)}
</span>
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2">
{log.command && (
<div>
<CardDescription>command</CardDescription>
<pre className="max-h-[500px] overflow-y-auto">
{log.command}
</pre>
</div>
)}
{log.stdout && (
<div>
<CardDescription>stdout</CardDescription>
<pre className="max-h-[500px] overflow-y-auto">
{log.stdout}
</pre>
</div>
)}
{log.stderr && (
<div>
<CardDescription>stdout</CardDescription>
<pre className="max-h-[500px] overflow-y-auto">
{log.stderr}
</pre>
</div>
)}
</CardContent>
</Card>
))}
</div>
</SheetContent>
</Sheet>
);
};
@@ -0,0 +1,62 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { Types } from "@monitor/client";
import { cn, version_to_string } from "@util/helpers";
import { Calendar, User } from "lucide-react";
import { UpdateDetails, UpdateUser } from "./update";
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>
);
+25
View File
@@ -1,13 +1,38 @@
import { Types } from "@monitor/client";
import { client } from "./main";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useAtomValue, useSetAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
export const useRead = <T extends Types.ReadRequest>(req: T) =>
useQuery([req], () => client.read(req));
export const useWrite = <T extends Types.WriteRequest>() =>
useMutation((req: T) => client.write(req));
export const useExecute = <T extends Types.ExecuteRequest>() =>
useMutation((req: T) => client.execute(req));
export const useUser = () => useRead({ type: "GetUser", params: {} });
export const useLogin = () =>
useMutation(client.login, {
onSuccess: (jwt) => localStorage.setItem("monitor-auth-token", jwt ?? ""),
});
// const recents_atom = atomWithStorage<
// { type: "Deployment" | "Build" | "Server"; id: string }[]
// >("recents", []);
const recents_atom = atomWithStorage<
{ type: "Deployment" | "Build" | "Server"; id: string }[]
>("recently-viewed", []);
export const useGetRecentlyViewed = () => useAtomValue(recents_atom);
export const useSetRecentlyViewed = () => {
const set = useSetAtom(recents_atom);
const push = (type: "Deployment" | "Build" | "Server", id: string) =>
set((res) => [{ type, id }, ...res.filter((r) => r.id !== id)].slice(0, 5));
return push;
};
@@ -0,0 +1,21 @@
import { useGetRecentlyViewed } from "@hooks";
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
import { DeploymentCard, ServerCard } from "..";
export const RecentlyViewed = () => {
const recents = useGetRecentlyViewed();
return (
<Card className="w-full">
<CardHeader>
<CardTitle>Recently Viewed</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{recents.map(({ type, id }) => {
if (type === "Deployment") return <DeploymentCard key={id} id={id} />;
if (type === "Build") return <div></div>;
if (type === "Server") return <ServerCard key={id} id={id} />;
})}
</CardContent>
</Card>
);
};
+150 -28
View File
@@ -1,12 +1,129 @@
import { useRead, useUser } from "@hooks";
import { useRead, useUser, useWrite } from "@hooks";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { version_to_string } from "@util/helpers";
import { ServersChart } from "./components/servers-chart";
import { DeploymentsChart } from "./components/deployments-chart";
import { Input } from "@ui/input";
import { Button } from "@ui/button";
import { PlusCircle } from "lucide-react";
import { ChevronDown, PlusCircle } from "lucide-react";
import { Link } from "react-router-dom";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@ui/dropdown";
import { Button } from "@ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@ui/dialog";
import { useState } from "react";
import { RecentlyViewed } from "./components/recents";
const NewDeployment = ({
open,
set,
}: {
open: boolean;
set: (b: boolean) => void;
}) => {
const { mutate } = useWrite();
const [name, setName] = useState("");
return (
<Dialog open={open} onOpenChange={set}>
<DialogContent>
<DialogHeader>
<DialogTitle>New Deployment</DialogTitle>
</DialogHeader>
<div className="flex items-center justify-between">
<div>Deployment Name</div>
<Input
className="max-w-[50%]"
placeholder="Deployment Name"
name={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<DialogFooter>
<Button
variant="outline"
intent="success"
onClick={() => {
mutate({
type: "CreateDeployment",
params: { name, config: {} },
});
set(false);
}}
>
Create
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
const NewButton = () => {
const [open, set] = useState<"deployment" | "server" | boolean>(false);
return (
<>
<DropdownMenu>
<DropdownMenuTrigger>
<Button
className="w-[200px] flex items-center justify-between"
variant="outline"
>
<div className="flex items-center gap-2">
<PlusCircle className="w-4 h-4 text-green-500" />
Add New
</div>
<ChevronDown className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[200px]">
<DropdownMenuLabel className="text-xs">
Resource Type
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => set("deployment")}>
Deployment
</DropdownMenuItem>
<DropdownMenuItem> Build </DropdownMenuItem>
<DropdownMenuItem> Server </DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<NewDeployment open={open === "deployment"} set={set} />
</>
);
};
export const DeploymentCard = ({ id }: { id: string }) => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployment = deployments?.find((d) => d.id === id);
if (!deployment) return null;
return (
<Link to={`/deployments/${deployment.id}`}>
<Card className="hover:bg-accent">
<CardHeader>
<CardTitle>{deployment.name}</CardTitle>
<CardDescription>
{deployment.status ?? "not deployed"}
</CardDescription>
</CardHeader>
</Card>
</Link>
);
};
const DeploymentsList = () => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
@@ -14,33 +131,38 @@ const DeploymentsList = () => {
return (
<div className="flex flex-col gap-2 w-full border-r pr-4">
<h2 className="text-lg">Deployments</h2>
{deployments?.map((deployment) => (
<Card>
<CardHeader>
<CardTitle>{deployment.name}</CardTitle>
<CardDescription>{deployment.version}</CardDescription>
</CardHeader>
</Card>
{deployments?.map(({ id }) => (
<DeploymentCard key={id} id={id} />
))}
</div>
);
};
export const ServerCard = ({ id }: { id: string }) => {
const servers = useRead({ type: "ListServers", params: {} }).data;
const server = servers?.find((server) => server.id === id);
if (!server) return null;
return (
<Link to={`/servers/${server.id}`} key={server.id}>
<Card className="hover:bg-accent">
<CardHeader>
<CardTitle>{server.name}</CardTitle>
<CardDescription>{server.status}</CardDescription>
</CardHeader>
</Card>
</Link>
);
};
const ServersList = () => {
const servers = useRead({ type: "ListServers", params: {} }).data;
return (
<div className="flex flex-col gap-2 w-full border-r pr-4">
<h2 className="text-lg">Servers</h2>
<h2 className="text-lg">Deployments</h2>
{servers?.map((server) => (
<Link to={`/servers/${server.id}`} key={server.id}>
<Card>
<CardHeader>
<CardTitle>{server.name}</CardTitle>
<CardDescription>{server.status}</CardDescription>
</CardHeader>
</Card>
</Link>
<ServerCard key={server.id} id={server.id} />
))}
</div>
);
@@ -75,21 +197,21 @@ export const Dashboard = () => {
<h1 className="text-xl"> Hello, {user?.username}.</h1>
<div className="flex gap-4">
<Input className="w-[300px]" placeholder="Search" />
<Button className="w-[120px]" variant="outline" intent="success">
<PlusCircle className="w-4 h-4 mr-2 text-green-500" />
Add New
</Button>
<NewButton />
</div>
</div>
<div className="flex gap-4">
<DeploymentsChart />
<ServersChart />
<div className="flex gap-24">
<div className="flex gap-4 w-full h-fit">
<DeploymentsChart />
<ServersChart />
</div>
<RecentlyViewed />
</div>
<div className="flex gap-4">
{/* <div className="flex gap-4">
<DeploymentsList />
<ServersList />
<BuildsList />
</div>
</div> */}
</>
);
};
@@ -0,0 +1,65 @@
import { Button } from "@ui/button";
import { Card, CardHeader, CardContent } from "@ui/card";
import { Tabs, TabsList, TabsTrigger } from "@ui/tabs";
// import { useDeploymentLog } from "@hooks/deployments";
import { TabsContent } from "@radix-ui/react-tabs";
import { AlertOctagon, ChevronDown } from "lucide-react";
import { useEffect } from "react";
import { useRead } from "@hooks";
const scroll_to_bottom = (id: string) => () =>
document
.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 },
});
useEffect(() => {
const handle = setInterval(() => refetch(), 30000);
return () => clearInterval(handle);
}, [refetch]);
useEffect(() => {
scroll_to_bottom("stdout")();
scroll_to_bottom("stderr")();
}, [data]);
return (
<Tabs defaultValue="stdout">
<Card>
<CardHeader className="flex-row justify-end">
<TabsList>
<TabsTrigger value="stdout" onClick={scroll_to_bottom("stdout")}>
Out
</TabsTrigger>
<TabsTrigger value="stderr" onClick={scroll_to_bottom("stderr")}>
Err
{data?.stderr && (
<AlertOctagon className="w-4 h-4 ml-2 stroke-red-500" />
)}
</TabsTrigger>
</TabsList>
</CardHeader>
{["stdout", "stderr"].map((t) => (
<TabsContent key={t} className="h-full relative" value={t}>
<CardContent className="h-[50vh] overflow-y-scroll">
<pre id={t}>
{data?.[t as keyof typeof data] || `no ${t} logs`}
</pre>
</CardContent>
<Button
className="absolute bottom-8 right-12"
onClick={scroll_to_bottom(t)}
>
<ChevronDown className="h-4 w-4" />
</Button>
</TabsContent>
))}
</Card>
</Tabs>
);
};
+126
View File
@@ -0,0 +1,126 @@
// import { DeploymentLogs } from "@pages/resource/deployment/components/logs";
import { Resource } from "@layouts/resource";
// import { Updates } from "@components/updates";
import { useParams } from "react-router-dom";
import { CardDescription } from "@ui/card";
// import { DeploymentConfig } from "@pages/resource/deployment/components/config";
// import {
// RedeployContainer,
// StartOrStopContainer,
// RemoveContainer,
// } from "@pages/resource/deployment/components/actions";
// import { DeleteDeployment } from "@pages/resource/deployment/components/delete";
import { useRead, useSetRecentlyViewed } from "@hooks";
import { Circle } from "lucide-react";
import { cn } from "@util/helpers";
import { DeploymentLogs } from "./components/deployment-logs";
import { Updates } from "@components/updates/updates";
export const DeploymentName = ({
deploymentId,
}: {
deploymentId: string | undefined;
}) => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployment = deployments?.find((d) => d.id === deploymentId);
return <>{deployment?.name ?? "..."}</>;
};
export const DeploymentStatus = ({
deploymentId,
}: {
deploymentId: string | undefined;
}) => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployment = deployments?.find((d) => d.id === deploymentId);
return <>{deployments ? deployment?.status ?? "not deployed" : "..."}</>;
};
export const DeploymentStatusIcon = ({
deploymentId,
}: {
deploymentId: string | undefined;
}) => {
const deployments = useRead({ type: "ListDeployments", params: {} }).data;
const deployment = deployments?.find((d) => d.id === deploymentId);
return (
<Circle
className={cn(
"w-4 h-4 stroke-none",
deployment?.status === "running" && "fill-green-500",
deployment?.status === "exited" && "fill-red-500",
deployment?.status === "not_deployed" && "fill-blue-500"
)}
/>
);
};
// export const DeploymentInfo = ({ deploymentId }: { deploymentId: string }) => {
// const deployments = useRead({ type: "ListDeployments", params: {} }).data;
// const deployment = deployments?.find((d) => d.id === deploymentId);
// return (
// <div className="flex flex-col gap-2 md:flex-row md:gap-4">
// <Link to={`/builds/${deployment?.deployment.build_id}`}>
// <CardDescription className="flex items-center">
// <HardDrive className="w-4 h-4 mr-2" />
// {data ? deployment?.container?.image ?? "no image" : "..."}
// </CardDescription>
// </Link>
// <Link to={`/servers/${deployment?.deployment.server_id}`}>
// <CardDescription className="flex items-center gap-2">
// <Server className="w-4 h-4" />
// <ServerName serverId={deployment?.deployment.server_id} />
// </CardDescription>
// </Link>
// </div>
// );
// };
export const Deployment = () => {
const { deploymentId } = useParams();
const push = useSetRecentlyViewed();
if (!deploymentId) return null;
push("Deployment", deploymentId);
return (
<Resource
title={<DeploymentName deploymentId={deploymentId} />}
info={
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-4">
<div>deployment info</div>
{/* <DeploymentInfo deploymentId={deploymentId} /> */}
<CardDescription className="hidden md:block">|</CardDescription>
<CardDescription className="flex items-center gap-2">
<DeploymentStatusIcon deploymentId={deploymentId} />
<DeploymentStatus deploymentId={deploymentId} />
</CardDescription>
<CardDescription className="hidden md:block">|</CardDescription>
{/* <DeleteDeployment deploymentId={deploymentId} /> */}
</div>
}
actions={
<>
{/* <RedeployContainer deploymentId={deploymentId} />
<StartOrStopContainer deploymentId={deploymentId} />
<RemoveContainer deploymentId={deploymentId} /> */}
</>
}
tabs={[
{
title: "Logs",
component: <DeploymentLogs deploymentId={deploymentId} />,
},
{
title: "Config",
component: <>Config</>,
},
{
title: "Updates",
component: <>Updates</>,
},
]}
/>
);
};
+7 -2
View File
@@ -1,4 +1,4 @@
import { useRead } from "@hooks";
import { useRead, useSetRecentlyViewed } from "@hooks";
import { Resource } from "@layouts/resource";
import { ServerStatus } from "@monitor/client/dist/types";
import { CardDescription } from "@ui/card";
@@ -83,7 +83,12 @@ export const ServerStatusIcon = ({
export const Server = () => {
const { serverId } = useParams();
// const { data } = useRead({ type: "GetServer", params: { id: serverId! } });
const push = useSetRecentlyViewed();
// if (!serverId) return null;
// push("Server", serverId!);
if (!serverId) return null;
push("Server", serverId);
return (
<Resource
+8 -7
View File
@@ -8,6 +8,7 @@ import { Login } from "@pages/auth/login";
import { Signup } from "@pages/auth/signup";
import { Dashboard } from "@pages/dashboard";
import { Server } from "@pages/server";
import { Deployment } from "@pages/deployment";
const router = createBrowserRouter([
{
@@ -18,13 +19,13 @@ const router = createBrowserRouter([
{ path: "login", element: <Login /> },
{ path: "signup", element: <Signup /> },
// {
// path: "deployments",
// children: [
// { path: "", element: <Deployments /> },
// { path: ":deploymentId", element: <Deployment /> },
// ],
// },
{
path: "deployments",
children: [
{ path: "", element: "deploymenys" },
{ path: ":deploymentId", element: <Deployment /> },
],
},
// {
// path: "builds",
// children: [
+4 -2
View File
@@ -62,7 +62,7 @@ export function readableMonitorTimestamp(rfc3339_ts: string) {
} ${pm ? "PM" : "AM"}`;
}
export function readableDuration(start_ts: string, end_ts: string) {
export function readableDuration(start_ts: number, end_ts: number) {
const start = new Date(start_ts);
const end = new Date(end_ts);
const durr = end.getTime() - start.getTime();
@@ -146,7 +146,9 @@ export function copyToClipboard(text: string) {
navigator.clipboard.writeText(text);
}
export function parseEnvVarseToDotEnv(envVars: Types.EnvironmentVar[] | undefined) {
export function parseEnvVarseToDotEnv(
envVars: Types.EnvironmentVar[] | undefined
) {
return envVars?.reduce(
(prev, { variable, value }) =>
prev + (prev ? "\n" : "") + `${variable}=${value}`,
+5 -5
View File
@@ -5,9 +5,9 @@ import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths()],
resolve: {
alias: {
"@monitor/client": "../client/ts"
}
}
// resolve: {
// alias: {
// "@monitor/client": "../client/ts"
// }
// }
});