diff --git a/frontend/src/components/header/index.tsx b/frontend/src/components/header/index.tsx index f0b4d6640..a49ebcaf5 100644 --- a/frontend/src/components/header/index.tsx +++ b/frontend/src/components/header/index.tsx @@ -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 (
@@ -25,6 +26,14 @@ export const Paths = () => { )} + {serverId && ( + <> + + + + + + )}
); }; diff --git a/frontend/src/components/updates/update.tsx b/frontend/src/components/updates/update.tsx new file mode 100644 index 000000000..e83d0696f --- /dev/null +++ b/frontend/src/components/updates/update.tsx @@ -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 ( + + + + + + + + {update.operation + .split("_") + .map((s) => s[0].toUpperCase() + s.slice(1)) + .join(" ")}{" "} + {version_to_string(update.version)} + + +
+ + {new Date(update.start_ts).toLocaleString()} +
+
+ + {update.end_ts + ? readableDuration(update.start_ts, update.end_ts) + : "ongoing"} +
+
+ + +
+ {update.version && ( +
+ + {readableVersion(update.version)} +
+ )} +
+
+
+ {update.logs.map((log, i) => ( + + + {log.stage} + + + Stage {i + 1} of {update.logs.length} + + | + + + {readableDuration(log.start_ts, log.end_ts)} + + + + + {log.command && ( +
+ command +
+                      {log.command}
+                    
+
+ )} + {log.stdout && ( +
+ stdout +
+                      {log.stdout}
+                    
+
+ )} + {log.stderr && ( +
+ stdout +
+                      {log.stderr}
+                    
+
+ )} +
+
+ ))} +
+
+
+ ); +}; diff --git a/frontend/src/components/updates/updates.tsx b/frontend/src/components/updates/updates.tsx new file mode 100644 index 000000000..b5e00dcd3 --- /dev/null +++ b/frontend/src/components/updates/updates.tsx @@ -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; +}) => ( + + + Updates + + + {updates?.map((update) => ( + + +
+ + {update.operation + .split("_") + .map((s) => s[0].toUpperCase() + s.slice(1)) + .join(" ")}{" "} + {version_to_string(update.version)} + +
+
+ + + {update.end_ts + ? new Date(update.end_ts).toLocaleString() + : "ongoing"} + +
+
+ + + + +
+
+
+ +
+
+ ))} +
+
+); diff --git a/frontend/src/hooks.ts b/frontend/src/hooks.ts index 3c165cc32..fac06981f 100644 --- a/frontend/src/hooks.ts +++ b/frontend/src/hooks.ts @@ -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 = (req: T) => useQuery([req], () => client.read(req)); +export const useWrite = () => + useMutation((req: T) => client.write(req)); + +export const useExecute = () => + 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; +}; diff --git a/frontend/src/pages/dashboard/components/recents.tsx b/frontend/src/pages/dashboard/components/recents.tsx new file mode 100644 index 000000000..8207d314a --- /dev/null +++ b/frontend/src/pages/dashboard/components/recents.tsx @@ -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 ( + + + Recently Viewed + + + {recents.map(({ type, id }) => { + if (type === "Deployment") return ; + if (type === "Build") return
; + if (type === "Server") return ; + })} +
+
+ ); +}; diff --git a/frontend/src/pages/dashboard/index.tsx b/frontend/src/pages/dashboard/index.tsx index 4e77ce580..7463ebaf9 100644 --- a/frontend/src/pages/dashboard/index.tsx +++ b/frontend/src/pages/dashboard/index.tsx @@ -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 ( + + + + New Deployment + +
+
Deployment Name
+ setName(e.target.value)} + /> +
+ + + +
+
+ ); +}; + +const NewButton = () => { + const [open, set] = useState<"deployment" | "server" | boolean>(false); + return ( + <> + + + + + + + Resource Type + + + + set("deployment")}> + Deployment + + Build + Server + + + + + + ); +}; + +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 ( + + + + {deployment.name} + + {deployment.status ?? "not deployed"} + + + + + ); +}; const DeploymentsList = () => { const deployments = useRead({ type: "ListDeployments", params: {} }).data; @@ -14,33 +131,38 @@ const DeploymentsList = () => { return (

Deployments

- {deployments?.map((deployment) => ( - - - {deployment.name} - {deployment.version} - - + {deployments?.map(({ id }) => ( + ))}
); }; +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 ( + + + + {server.name} + {server.status} + + + + ); +}; + const ServersList = () => { const servers = useRead({ type: "ListServers", params: {} }).data; return (
-

Servers

+

Deployments

{servers?.map((server) => ( - - - - {server.name} - {server.status} - - - + ))}
); @@ -75,21 +197,21 @@ export const Dashboard = () => {

Hello, {user?.username}.

- +
-
- - +
+
+ + +
+
-
+ {/*
-
+
*/} ); }; diff --git a/frontend/src/pages/deployment/components/deployment-logs.tsx b/frontend/src/pages/deployment/components/deployment-logs.tsx new file mode 100644 index 000000000..f071e45c3 --- /dev/null +++ b/frontend/src/pages/deployment/components/deployment-logs.tsx @@ -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 ( + + + + + + Out + + + Err + {data?.stderr && ( + + )} + + + + {["stdout", "stderr"].map((t) => ( + + +
+                {data?.[t as keyof typeof data] || `no ${t} logs`}
+              
+
+ +
+ ))} +
+
+ ); +}; diff --git a/frontend/src/pages/deployment/index.tsx b/frontend/src/pages/deployment/index.tsx new file mode 100644 index 000000000..1ed9c68f1 --- /dev/null +++ b/frontend/src/pages/deployment/index.tsx @@ -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 ( + + ); +}; + +// export const DeploymentInfo = ({ deploymentId }: { deploymentId: string }) => { +// const deployments = useRead({ type: "ListDeployments", params: {} }).data; +// const deployment = deployments?.find((d) => d.id === deploymentId); + +// return ( +//
+// +// +// +// {data ? deployment?.container?.image ?? "no image" : "..."} +// +// +// +// +// +// +// +// +//
+// ); +// }; + +export const Deployment = () => { + const { deploymentId } = useParams(); + const push = useSetRecentlyViewed(); + + if (!deploymentId) return null; + push("Deployment", deploymentId); + + return ( + } + info={ +
+
deployment info
+ {/* */} + | + + + + + | + {/* */} +
+ } + actions={ + <> + {/* + + */} + + } + tabs={[ + { + title: "Logs", + component: , + }, + { + title: "Config", + component: <>Config, + }, + { + title: "Updates", + component: <>Updates, + }, + ]} + /> + ); +}; diff --git a/frontend/src/pages/server/index.tsx b/frontend/src/pages/server/index.tsx index 2218c9da8..0b8050cba 100644 --- a/frontend/src/pages/server/index.tsx +++ b/frontend/src/pages/server/index.tsx @@ -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 ( }, { path: "signup", element: }, - // { - // path: "deployments", - // children: [ - // { path: "", element: }, - // { path: ":deploymentId", element: }, - // ], - // }, + { + path: "deployments", + children: [ + { path: "", element: "deploymenys" }, + { path: ":deploymentId", element: }, + ], + }, // { // path: "builds", // children: [ diff --git a/frontend/src/util/helpers.ts b/frontend/src/util/helpers.ts index a0f5861a7..014c3d706 100644 --- a/frontend/src/util/helpers.ts +++ b/frontend/src/util/helpers.ts @@ -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}`, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index a297d5b21..d32f9f39b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -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" + // } + // } });