diff --git a/frontend/src/components/updates/resource.tsx b/frontend/src/components/updates/resource.tsx new file mode 100644 index 000000000..651b320dc --- /dev/null +++ b/frontend/src/components/updates/resource.tsx @@ -0,0 +1 @@ +export const ResourceUpdates = () => {}; diff --git a/frontend/src/layouts/layout.tsx b/frontend/src/layouts/layout.tsx index 6f687827d..837f30b84 100644 --- a/frontend/src/layouts/layout.tsx +++ b/frontend/src/layouts/layout.tsx @@ -13,7 +13,7 @@ export const Layout = () => { <>
-
+
diff --git a/frontend/src/layouts/resource.tsx b/frontend/src/layouts/resource.tsx index 68ac3505f..3ea2d107a 100644 --- a/frontend/src/layouts/resource.tsx +++ b/frontend/src/layouts/resource.tsx @@ -1,36 +1,18 @@ -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs"; import { ReactNode } from "react"; import { Page } from "./page"; +import { Outlet } from "react-router-dom"; interface ResourceProps { title: ReactNode; info: ReactNode; actions: ReactNode; - tabs: { title: string; component: ReactNode }[]; } -export const Resource = ({ title, info, actions, tabs }: ResourceProps) => ( - - {title}} - subtitle={

{info}

} - actions={actions} - content={ -
- - {tabs.map(({ title }) => ( - - {title} - - ))} - - {tabs.map((t, i) => ( - - {t.component} - - ))} -
- } - /> -
+export const Resource = ({ title, info, actions }: ResourceProps) => ( + {title}} + subtitle={

{info}

} + actions={actions} + content={} + /> ); diff --git a/frontend/src/resources/deployment/components/deployment-logs.tsx b/frontend/src/resources/deployment/components/deployment-logs.tsx index 37a5b003e..ce73f573d 100644 --- a/frontend/src/resources/deployment/components/deployment-logs.tsx +++ b/frontend/src/resources/deployment/components/deployment-logs.tsx @@ -1,21 +1,23 @@ import { Button } from "@ui/button"; import { Card, CardHeader, CardContent } from "@ui/card"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@ui/tabs"; // import { useDeploymentLog } from "@hooks/deployments"; -import { AlertOctagon, ChevronDown } from "lucide-react"; +import { AlertOctagon, ChevronDown, TerminalSquare } from "lucide-react"; import { useEffect } from "react"; import { useRead } from "@hooks"; +import { useParams } from "react-router-dom"; const scroll_to_bottom = (id: string) => () => document .getElementById(id) ?.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" }); -export const DeploymentLogs = ({ - deployment_id, -}: { - deployment_id: string; -}) => { - const { data, refetch } = useRead("GetLog", { deployment_id, tail: 200 }); +export const DeploymentLogs = () => { + const deployment_id = useParams().deploymentId; + const { data, refetch } = useRead( + "GetLog", + { deployment_id, tail: 200 }, + { enabled: !!deployment_id } + ); useEffect(() => { const handle = setInterval(() => refetch(), 30000); @@ -29,38 +31,36 @@ export const DeploymentLogs = ({ return ( - - - - - Out - - - Err - {data?.stderr && ( - - )} - - - - - {["stdout", "stderr"].map((t) => ( - -
-
-                  {data?.[t as keyof typeof data] || `no ${t} logs`}
-                
-
- -
- ))} -
-
+
+
+ +

Logs

+
+ + + Out + + + Err + {data?.stderr && ( + + )} + + +
+ {["stdout", "stderr"].map((t) => ( + +
+
{data?.[t as keyof typeof data] || `no ${t} logs`}
+
+ +
+ ))}
); }; diff --git a/frontend/src/resources/deployment/layout.tsx b/frontend/src/resources/deployment/layout.tsx new file mode 100644 index 000000000..3e91e7d24 --- /dev/null +++ b/frontend/src/resources/deployment/layout.tsx @@ -0,0 +1,69 @@ +import { Link, useParams } from "react-router-dom"; +import { useSetRecentlyViewed } from "@hooks"; +import { Resource } from "@layouts/resource"; +import { CardDescription } from "@ui/card"; +import { + DeploymentBuild, + DeploymentName, + DeploymentServer, + DeploymentStatus, + DeploymentStatusIcon, +} from "./util"; +import { + RedeployContainer, + RemoveContainer, + StartOrStopContainer, +} from "./components/actions"; +import { Button } from "@ui/button"; +import { Bell, Settings } from "lucide-react"; + +export const DeploymentLayout = () => { + const { deploymentId } = useParams(); + const push = useSetRecentlyViewed(); + + if (!deploymentId) return null; + push("Deployment", deploymentId); + + return ( + + + + } + info={ +
+
+ + +
+ | + + | + +
+ } + actions={ +
+
+ + + +
+
+ + + + + + +
+
+ } + /> + ); +}; diff --git a/frontend/src/resources/deployment/page.tsx b/frontend/src/resources/deployment/page.tsx index a261ec5e6..b053520e7 100644 --- a/frontend/src/resources/deployment/page.tsx +++ b/frontend/src/resources/deployment/page.tsx @@ -1,22 +1,32 @@ import { useParams } from "react-router-dom"; -import { useSetRecentlyViewed } from "@hooks"; -import { Resource } from "@layouts/resource"; -import { CardDescription } from "@ui/card"; -import { - DeploymentBuild, - DeploymentName, - DeploymentServer, - DeploymentStatus, - DeploymentStatusIcon, -} from "./util"; -import { - RedeployContainer, - RemoveContainer, - StartOrStopContainer, -} from "./components/actions"; +import { useRead, useSetRecentlyViewed } from "@hooks"; import { DeploymentLogs } from "./components/deployment-logs"; +import { Card, CardHeader, CardTitle } from "@ui/card"; +import { Bell } from "lucide-react"; -export const Deployment = () => { +const DeploymentUpdates = ({ id }: { id: string }) => { + const updates = useRead("ListUpdates", { target: { id } }).data; + + return ( +
+
+ +

Updates

+
+
+ {updates?.slice(0, 3).map((u) => ( + + + {u.operation} + + + ))} +
+
+ ); +}; + +export const DeploymentPage = () => { const { deploymentId } = useParams(); const push = useSetRecentlyViewed(); @@ -24,41 +34,9 @@ export const Deployment = () => { push("Deployment", deploymentId); return ( - } - info={ -
-
- - -
- | - - | - -
- } - actions={ -
- - - -
- } - tabs={[ - { - title: "Logs", - component: , - }, - { - title: "Config", - component: <>Config, - }, - { - title: "Updates", - component: <>Updates, - }, - ]} - /> +
+ + +
); }; diff --git a/frontend/src/resources/deployment/updates.tsx b/frontend/src/resources/deployment/updates.tsx new file mode 100644 index 000000000..de2166d24 --- /dev/null +++ b/frontend/src/resources/deployment/updates.tsx @@ -0,0 +1,43 @@ +import { useRead } from "@hooks"; +import { Table, TableCell, TableHead, TableHeader, TableRow } from "@ui/table"; +import { fmt_update_date } from "@util/helpers"; +import { Check, X } from "lucide-react"; +import { useParams } from "react-router-dom"; + +export const DeploymentUpdates = () => { + const deploymentId = useParams().deploymentId; + const updates = useRead("ListUpdates", { target: { id: deploymentId } }).data; + + return ( + + + + Start + End + Operation + Operator + Success + + + {updates?.map((update) => ( + + {fmt_update_date(new Date(update.start_ts))} + + {update.end_ts + ? fmt_update_date(new Date(update.end_ts)) + : "ongoing..."} + + {update.operation} + {update.operator} + + {update.success ? ( + + ) : ( + + )} + + + ))} +
+ ); +}; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 2548a0f48..566e118eb 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -3,10 +3,13 @@ import { Layout } from "@layouts/layout"; import { Login } from "@pages/auth/login"; import { Signup } from "@pages/auth/signup"; 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, Builders } from "@resources/pages"; +import { DeploymentLogs } from "@resources/deployment/components/deployment-logs"; +import { DeploymentUpdates } from "@resources/deployment/updates"; +import { DeploymentLayout } from "@resources/deployment/layout"; +import { DeploymentPage } from "@resources/deployment/page"; const router = createBrowserRouter([ { @@ -17,14 +20,24 @@ const router = createBrowserRouter([ { path: "login", element: }, { path: "signup", element: }, + // Deployments { path: "deployments", children: [ { path: "", element: }, - { path: ":deploymentId", element: }, + { + path: ":deploymentId", + element: , + children: [ + { path: "", element: }, + { path: "updates", element: }, + { path: "config", element: <>deployment config! }, + ], + }, ], }, + // Servers { path: "servers", children: [ @@ -32,6 +45,8 @@ const router = createBrowserRouter([ { path: ":serverId", element: }, ], }, + + // Builds { path: "builds", children: [ @@ -39,6 +54,8 @@ const router = createBrowserRouter([ { path: ":buildId", element: }, ], }, + + // Builders { path: "builders", children: [ diff --git a/frontend/src/ui/table.tsx b/frontend/src/ui/table.tsx new file mode 100644 index 000000000..dba7337b4 --- /dev/null +++ b/frontend/src/ui/table.tsx @@ -0,0 +1,121 @@ +import * as React from "react"; + +import { cn } from "@util/helpers"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/frontend/src/util/helpers.ts b/frontend/src/util/helpers.ts index 783ae0507..1ab99a543 100644 --- a/frontend/src/util/helpers.ts +++ b/frontend/src/util/helpers.ts @@ -298,3 +298,6 @@ export function readableImageNameTag( return ["none", "none"]; } } + +export const fmt_update_date = (d: Date) => + `${d.getDate()}/${d.getMonth()} @ ${d.getHours()}:${d.getMinutes()}`;