From b7319e602865acb58e79e4c1123d643a6657589c Mon Sep 17 00:00:00 2001 From: Justin Brooks Date: Thu, 3 Oct 2024 21:43:26 -0400 Subject: [PATCH] add release navside bar --- ...argetConfigEditor.tsx => ConfigEditor.tsx} | 2 +- .../_components/CreateTarget.tsx | 4 +- .../_components/EditTarget.tsx | 4 +- .../release-drawer/ReleaseDrawer.tsx | 220 ++++++++++++++++++ .../target-drawer/OverviewContent.tsx | 4 +- .../src/app/[workspaceSlug]/layout.tsx | 2 + .../[systemSlug]/deployments/TableRelease.tsx | 3 + packages/api/src/router/release.ts | 16 +- packages/db/src/schema/release.ts | 13 +- 9 files changed, 258 insertions(+), 10 deletions(-) rename apps/webservice/src/app/[workspaceSlug]/_components/{TargetConfigEditor.tsx => ConfigEditor.tsx} (94%) create mode 100644 apps/webservice/src/app/[workspaceSlug]/_components/release-drawer/ReleaseDrawer.tsx diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/TargetConfigEditor.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/ConfigEditor.tsx similarity index 94% rename from apps/webservice/src/app/[workspaceSlug]/_components/TargetConfigEditor.tsx rename to apps/webservice/src/app/[workspaceSlug]/_components/ConfigEditor.tsx index 1dd6324be..7cc820faa 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/TargetConfigEditor.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/ConfigEditor.tsx @@ -17,7 +17,7 @@ loader.init().then((monaco) => { }); }); -export const TargetConfigEditor: React.FC<{ +export const ConfigEditor: React.FC<{ value: string; onChange?: (v: string) => void; readOnly?: boolean; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/CreateTarget.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/CreateTarget.tsx index 3167099ae..89d203584 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/CreateTarget.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/CreateTarget.tsx @@ -33,7 +33,7 @@ import { Input } from "@ctrlplane/ui/input"; import { Label } from "@ctrlplane/ui/label"; import { api } from "~/trpc/react"; -import { TargetConfigEditor } from "./TargetConfigEditor"; +import { ConfigEditor } from "./ConfigEditor"; const createTargetSchema = z.object({ name: z.string(), @@ -183,7 +183,7 @@ export const CreateTargetDialog: React.FC<{ Config - + diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/EditTarget.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/EditTarget.tsx index 25affbcce..a6b39d360 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/EditTarget.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/EditTarget.tsx @@ -32,7 +32,7 @@ import { Input } from "@ctrlplane/ui/input"; import { Label } from "@ctrlplane/ui/label"; import { api } from "~/trpc/react"; -import { TargetConfigEditor } from "./TargetConfigEditor"; +import { ConfigEditor } from "./ConfigEditor"; type TargetWithMetadata = Target & { metadata: Record; @@ -207,7 +207,7 @@ export const EditTargetDialog: React.FC<{ Config - + diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/release-drawer/ReleaseDrawer.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/release-drawer/ReleaseDrawer.tsx new file mode 100644 index 000000000..8bd0181ee --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/_components/release-drawer/ReleaseDrawer.tsx @@ -0,0 +1,220 @@ +"use client"; + +import type { Release, ReleaseDependency } from "@ctrlplane/db/schema"; +import { useRouter, useSearchParams } from "next/navigation"; +import { IconSparkles } from "@tabler/icons-react"; +import { format } from "date-fns"; +import yaml from "js-yaml"; + +import { Drawer, DrawerContent, DrawerTitle } from "@ctrlplane/ui/drawer"; +import { Input } from "@ctrlplane/ui/input"; +import { ReservedMetadataKey } from "@ctrlplane/validators/targets"; + +import { api } from "~/trpc/react"; +import { useMatchSorterWithSearch } from "~/utils/useMatchSorter"; +import { ConfigEditor } from "../ConfigEditor"; + +const param = "release_id"; +export const useReleaseDrawer = () => { + const router = useRouter(); + const params = useSearchParams(); + const releaseId = params.get(param); + + const setReleaseId = (id: string | null) => { + const url = new URL(window.location.href); + if (id === null) { + url.searchParams.delete(param); + } else { + url.searchParams.set(param, id); + } + router.replace(url.toString()); + }; + + const removeReleaseId = () => setReleaseId(null); + + return { releaseId, setReleaseId, removeReleaseId }; +}; + +export const ReleaseDrawer: React.FC = () => { + const { releaseId, removeReleaseId } = useReleaseDrawer(); + const isOpen = releaseId != null && releaseId != ""; + const setIsOpen = removeReleaseId; + const releaseQ = api.release.byId.useQuery(releaseId ?? "", { + enabled: isOpen, + refetchInterval: 10_000, + }); + const release = releaseQ.data; + + return ( + + +
+
+ {release?.name} +
+
+
+ {release && } +
+
+
+ ); +}; + +const ReleaseConfigInfo: React.FC<{ config: Record }> = ({ + config, +}) => { + yaml.dump(config); + return ; +}; + +const OverviewContent: React.FC<{ + release: Release & { + metadata: Record; + dependencies: ReleaseDependency[]; + }; +}> = ({ release }) => { + const { metadata } = release; + const links = + metadata[ReservedMetadataKey.Links] != null + ? (JSON.parse(metadata[ReservedMetadataKey.Links]) as Record< + string, + string + >) + : null; + + return ( +
+
+
Properties
+
+
+ + + + + + + + + + + + + + + + + + + + +
Name{release.name}
Version{release.version}
Created At{format(release.createdAt, "MM/dd/yyyy mm:hh:ss")}
+ Links + + {links == null ? ( + + Not set + + ) : ( + <> + {Object.entries(links).map(([name, url]) => ( + + {name} + + ))} + + )} +
+
+
+ + + + + + + +
+ External ID + +
+
+
+
+
+ +
+
Config
+
+ +
+
+ +
+
+ Metadata ({Object.keys(metadata).length}) +
+
+ +
+
+
+ ); +}; + +const ReleaseMetadataInfo: React.FC<{ metadata: Record }> = ( + props, +) => { + const metadata = Object.entries(props.metadata).sort(([keyA], [keyB]) => + keyA.localeCompare(keyB), + ); + const { search, setSearch, result } = useMatchSorterWithSearch(metadata, { + keys: ["0", "1"], + }); + return ( +
+
+
+ setSearch(e.target.value)} + /> +
+
+ {result.map(([key, value]) => ( +
+ + {Object.values(ReservedMetadataKey).includes( + key as ReservedMetadataKey, + ) && ( + + )}{" "} + + {key}: + {value} +
+ ))} +
+
+
+ ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/OverviewContent.tsx b/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/OverviewContent.tsx index c81aa583a..1233e9c7b 100644 --- a/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/OverviewContent.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/_components/target-drawer/OverviewContent.tsx @@ -16,13 +16,13 @@ import { ReservedMetadataKey } from "@ctrlplane/validators/targets"; import { api } from "~/trpc/react"; import { useMatchSorterWithSearch } from "~/utils/useMatchSorter"; -import { TargetConfigEditor } from "../TargetConfigEditor"; +import { ConfigEditor } from "../ConfigEditor"; const TargetConfigInfo: React.FC<{ config: Record }> = ({ config, }) => { yaml.dump(config); - return ; + return ; }; const TargetMetadataInfo: React.FC<{ metadata: Record }> = ( diff --git a/apps/webservice/src/app/[workspaceSlug]/layout.tsx b/apps/webservice/src/app/[workspaceSlug]/layout.tsx index 14f0dbb23..f6f216d55 100644 --- a/apps/webservice/src/app/[workspaceSlug]/layout.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/layout.tsx @@ -3,6 +3,7 @@ import { notFound, redirect } from "next/navigation"; import { auth } from "@ctrlplane/auth"; import { api } from "~/trpc/server"; +import { ReleaseDrawer } from "./_components/release-drawer/ReleaseDrawer"; import { TargetDrawer } from "./_components/target-drawer/TargetDrawer"; import { SidebarPanels } from "./SidebarPanels"; @@ -31,6 +32,7 @@ export default async function WorkspaceLayout({ + ); } diff --git a/apps/webservice/src/app/[workspaceSlug]/systems/[systemSlug]/deployments/TableRelease.tsx b/apps/webservice/src/app/[workspaceSlug]/systems/[systemSlug]/deployments/TableRelease.tsx index 7c6b96333..6492983ac 100644 --- a/apps/webservice/src/app/[workspaceSlug]/systems/[systemSlug]/deployments/TableRelease.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/systems/[systemSlug]/deployments/TableRelease.tsx @@ -13,6 +13,7 @@ import { Badge } from "@ctrlplane/ui/badge"; import { Button } from "@ctrlplane/ui/button"; import { CreateReleaseDialog } from "~/app/[workspaceSlug]/_components/CreateRelease"; +import { useReleaseDrawer } from "~/app/[workspaceSlug]/_components/release-drawer/ReleaseDrawer"; import { api } from "~/trpc/react"; import { DeployButton } from "./DeployButton"; import { Release } from "./TableCells"; @@ -83,6 +84,7 @@ export const ReleaseTable: React.FC<{ targets: { id: string }[]; }[]; }> = ({ deployment, environments }) => { + const { setReleaseId } = useReleaseDrawer(); const { workspaceSlug, systemSlug } = useParams<{ workspaceSlug: string; systemSlug: string; @@ -168,6 +170,7 @@ export const ReleaseTable: React.FC<{ return ( setReleaseId(r.id)} className={cn( "sticky left-0 z-10 min-w-[250px] backdrop-blur-lg", "items-center border-b border-l px-4 text-lg", diff --git a/packages/api/src/router/release.ts b/packages/api/src/router/release.ts index f69449c86..9e3f32080 100644 --- a/packages/api/src/router/release.ts +++ b/packages/api/src/router/release.ts @@ -25,6 +25,7 @@ import { release, releaseDependency, releaseJobTrigger, + releaseMetadata, target, } from "@ctrlplane/db/schema"; import { @@ -110,7 +111,20 @@ export const releaseRouter = createTRPCRouter({ })) .value() .at(0), - ), + ) + .then(async (data) => { + if (data == null) return null; + return { + ...data, + metadata: Object.fromEntries( + await ctx.db + .select() + .from(releaseMetadata) + .where(eq(releaseMetadata.releaseId, data.id)) + .then((r) => r.map((k) => [k.key, k.value])), + ), + }; + }), ), deploy: createTRPCRouter({ diff --git a/packages/db/src/schema/release.ts b/packages/db/src/schema/release.ts index e19e439c8..71c4c83db 100644 --- a/packages/db/src/schema/release.ts +++ b/packages/db/src/schema/release.ts @@ -45,6 +45,8 @@ export const releaseDependency = pgTable( }), ); +export type ReleaseDependency = InferSelectModel; + const createReleaseDependency = createInsertSchema(releaseDependency).omit({ id: true, }); @@ -55,7 +57,10 @@ export const release = pgTable( id: uuid("id").primaryKey().defaultRandom(), name: text("name").notNull(), version: text("version").notNull(), - config: jsonb("config").notNull().default("{}"), + config: jsonb("config") + .notNull() + .default("{}") + .$type>(), deploymentId: uuid("deployment_id") .notNull() .references(() => deployment.id, { onDelete: "cascade" }), @@ -66,7 +71,11 @@ export const release = pgTable( export type Release = InferSelectModel; -export const createRelease = createInsertSchema(release) +export const createRelease = createInsertSchema(release, { + version: z.string().min(1), + name: z.string().min(1), + config: z.record(z.any()), +}) .omit({ id: true }) .extend({ releaseDependencies: z