diff --git a/apps/webservice/src/app/[workspaceSlug]/(targets)/targets/TargetDrawer.tsx b/apps/webservice/src/app/[workspaceSlug]/(targets)/targets/TargetDrawer.tsx new file mode 100644 index 00000000..7b238848 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(targets)/targets/TargetDrawer.tsx @@ -0,0 +1,422 @@ +"use client"; + +import type { Target, TargetProvider } from "@ctrlplane/db/schema"; +import { useEffect, useRef, useState } from "react"; +import Link from "next/link"; +import { format } from "date-fns"; +import { + TbExternalLink, + TbHistory, + TbInfoCircle, + TbLock, + TbLockOpen, + TbPackage, + TbSparkles, + TbTag, + TbVariable, +} from "react-icons/tb"; + +import { cn } from "@ctrlplane/ui"; +import { Button } from "@ctrlplane/ui/button"; +import { Card } from "@ctrlplane/ui/card"; +import { Drawer, DrawerContent, DrawerTitle } from "@ctrlplane/ui/drawer"; +import { Input } from "@ctrlplane/ui/input"; +import { TableCell, TableHead } from "@ctrlplane/ui/table"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@ctrlplane/ui/tooltip"; +import { ReservedMetadataKey } from "@ctrlplane/validators/targets"; + +import { api } from "~/trpc/react"; +import { useMatchSorterWithSearch } from "~/utils/useMatchSorter"; + +const TargetMetadataInfo: 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} +
+ ))} +
+
+
+ ); +}; + +const DeploymentsContent: React.FC<{ targetId: string }> = ({ targetId }) => { + const deployments = api.deployment.byTargetId.useQuery(targetId); + const targetValues = api.deployment.variable.byTargetId.useQuery(targetId); + + if (!deployments.data || deployments.data.length === 0) { + return ( +
+ This target is not part of any deployments. +
+ ); + } + + return ( +
+ {deployments.data.map((deployment) => { + const deploymentVariables = targetValues.data?.filter( + (v) => v.deploymentId === deployment.id, + ); + return ( +
+
+
+ {deployment.name}{" "} + + / {deployment.environment.name} + +
+
+ {deployment.releaseJobTrigger.release?.version ?? + "No deployments"} +
+
+ + + {deploymentVariables != null && + deploymentVariables.length === 0 && ( +
+ No variables found +
+ )} + {deploymentVariables && ( + + + {deploymentVariables.map(({ key, value }) => ( + + {key} + {value.value} + + + ))} + +
+ )} +
+
+ ); + })} +
+ ); +}; + +const OverviewContent: React.FC<{ + target: Target & { + metadata: Record; + provider: TargetProvider | null; + }; +}> = ({ target }) => { + const links = + target.metadata[ReservedMetadataKey.Links] != null + ? (JSON.parse(target.metadata[ReservedMetadataKey.Links]) as Record< + string, + string + >) + : null; + return ( +
+
+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Identifier + {target.identifier}
Name{target.name}
Version{target.version}
Kind{target.kind}
+ Target Provider + + {target.provider != null ? ( + target.provider.name + ) : ( + + + + + Not set + + + +

+ The next target provider to insert a target with the + same identifier will become the owner of this target. +

+
+
+
+ )} +
Last Sync + {target.updatedAt && + format(target.updatedAt, "MM/dd/yyyy mm:hh:ss")} +
+ Links + + {links == null ? ( + + Not set + + ) : ( + <> + {Object.entries(links).map(([name, url]) => ( + + {name} + + ))} + + )} +
+
+ +
+
Metadata
+
+ +
+
+
+ ); +}; + +export const TargetDrawer: React.FC<{ + isOpen: boolean; + setIsOpen: (v: boolean) => void; + targetId?: string; +}> = ({ isOpen, setIsOpen, targetId }) => { + const targetQ = api.target.byId.useQuery(targetId ?? "", { + enabled: targetId != null, + refetchInterval: 10_000, + }); + + const target = targetQ.data; + + const [activeTab, setActiveTab] = useState("overview"); + const ref = useRef(null); + + useEffect(() => { + if (isOpen) ref.current?.blur(); + }, [isOpen]); + + const lockTarget = api.target.lock.useMutation(); + const unlockTarget = api.target.unlock.useMutation(); + const utils = api.useUtils(); + + const links = + target?.metadata[ReservedMetadataKey.Links] != null + ? (JSON.parse(target.metadata[ReservedMetadataKey.Links]) as Record< + string, + string + >) + : null; + + return ( + + + {target != null && ( + <> +
+
+ {target.name} + +
+ {links != null && ( +
+ {Object.entries(links).map(([label, url]) => ( + + + {label} + + ))} +
+ )} +
+ +
+
+ + + + + +
+
+ {activeTab === "deployment" && ( + + )} + {activeTab === "overview" && ( + + )} +
+
+ + )} +
+
+ ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(targets)/targets/page.tsx b/apps/webservice/src/app/[workspaceSlug]/(targets)/targets/page.tsx index b2e072f3..5e8fbf37 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(targets)/targets/page.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(targets)/targets/page.tsx @@ -1,50 +1,22 @@ "use client"; -import type { Target, TargetProvider } from "@ctrlplane/db/schema"; import { useEffect, useMemo, useState } from "react"; -import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { capitalCase } from "change-case"; -import { format } from "date-fns"; import _ from "lodash"; -import { - TbCategory, - TbLock, - TbLockOpen, - TbTag, - TbTarget, - TbX, -} from "react-icons/tb"; +import { TbCategory, TbTag, TbTarget, TbX } from "react-icons/tb"; -import { cn } from "@ctrlplane/ui"; import { Badge } from "@ctrlplane/ui/badge"; import { Button } from "@ctrlplane/ui/button"; -import { Card } from "@ctrlplane/ui/card"; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "@ctrlplane/ui/hover-card"; -import { Input } from "@ctrlplane/ui/input"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@ctrlplane/ui/resizable"; import { Skeleton } from "@ctrlplane/ui/skeleton"; -import { TableCell, TableHead } from "@ctrlplane/ui/table"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ctrlplane/ui/tabs"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@ctrlplane/ui/tooltip"; -import { SpecialMetadataKey } from "@ctrlplane/validators/targets"; import type { TargetFilter } from "./TargetFilter"; import { api } from "~/trpc/react"; -import { useMatchSorterWithSearch } from "~/utils/useMatchSorter"; import { useFilters } from "../../_components/filter/Filter"; import { FilterDropdown } from "../../_components/filter/FilterDropdown"; import { @@ -53,205 +25,10 @@ import { } from "../../_components/filter/FilterDropdownItems"; import { NoFilterMatch } from "../../_components/filter/NoFilterMatch"; import { MetadataFilterDialog } from "./MetadataFilterDialog"; +import { TargetDrawer } from "./TargetDrawer"; import { TargetGettingStarted } from "./TargetGettingStarted"; import { TargetsTable } from "./TargetsTable"; -const TargetGeneral: React.FC< - Target & { metadata: Record; provider: TargetProvider | null } -> = (target) => { - const metadata = Object.entries(target.metadata).sort(([keyA], [keyB]) => - keyA.localeCompare(keyB), - ); - const { search, setSearch, result } = useMatchSorterWithSearch(metadata, { - keys: ["0", "1"], - }); - const links = - target.metadata[SpecialMetadataKey.CtrlplaneLinks] != null - ? (JSON.parse( - target.metadata[SpecialMetadataKey.CtrlplaneLinks], - ) as Record) - : null; - return ( -
-
-
Properties
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Identifier - {target.identifier}
Name{target.name}
Version{target.version}
Kind{target.kind}
- Target Provider - - {target.provider != null ? ( - target.provider.name - ) : ( - - - - - Not set - - - -

- The next target provider to insert a target with the - same identifier will become the owner of this target. -

-
-
-
- )} -
Last Sync - {target.updatedAt && - format(target.updatedAt, "MM/dd/yyyy mm:hh:ss")} -
- Links - - {links == null ? ( - - Not set - - ) : ( - <> - {Object.entries(links).map(([name, url]) => ( - - {name} - - ))} - - )} -
-
- -
-
Metadata
-
-
- setSearch(e.target.value)} - /> -
-
- {result.map(([key, value]) => ( -
- {key}:{" "} - {value} -
- ))} -
-
-
-
- ); -}; - -const DeploymentsContent: React.FC<{ targetId: string }> = ({ targetId }) => { - const deployments = api.deployment.byTargetId.useQuery(targetId); - const targetValues = api.deployment.variable.byTargetId.useQuery(targetId); - - if (!deployments.data || deployments.data.length === 0) { - return ( -
- This target is not part of any deployments. -
- ); - } - - return ( -
- {deployments.data.map((deployment) => { - const deploymentVariables = targetValues.data?.filter( - (v) => v.deploymentId === deployment.id, - ); - return ( -
-
-
- {deployment.name}{" "} - - / {deployment.environment.name} - -
-
- {deployment.releaseJobTrigger.release?.version ?? - "No deployments"} -
-
- - - {deploymentVariables != null && - deploymentVariables.length === 0 && ( -
- No variables found -
- )} - {deploymentVariables && ( - - - {deploymentVariables.map(({ key, value }) => ( - - {key} - {value.value} - - - ))} - -
- )} -
-
- ); - })} -
- ); -}; - export default function TargetsPage({ params, }: { @@ -287,199 +64,123 @@ export default function TargetsPage({ workspace.data?.id ?? "", { enabled: workspace.isSuccess && workspace.data?.id !== "" }, ); - const lockTarget = api.target.lock.useMutation(); - const unlockTarget = api.target.unlock.useMutation(); - const utils = api.useUtils(); const [selectedTargetId, setSelectedTargetId] = useState(null); const targetId = selectedTargetId ?? targets.data?.items.at(0)?.id; - const targetIdInput = targetId ?? targets.data?.items.at(0)?.id; - const target = api.target.byId.useQuery(targetIdInput ?? "", { - enabled: targetIdInput != null, - refetchInterval: 10_000, - }); - if (targetsAll.isSuccess && targetsAll.data.total === 0) return ; return ( - - -
-
-
- {filters.map((f, idx) => ( - - {capitalCase(f.key)} - - {f.key === "name" && "contains"} - {f.key === "kind" && "is"} - {f.key === "metadata" && "matches"} - - - {typeof f.value === "string" ? ( - f.value - ) : ( - - - {Object.entries(f.value).length} key - {Object.entries(f.value).length > 1 ? "s" : ""} - - - {Object.entries(f.value).map(([key, value]) => ( -
- {key}:{" "} - {value} -
- ))} -
-
- )} -
- - -
- ))} +
+
+
+ {filters.map((f, idx) => ( + + {capitalCase(f.key)} + + {f.key === "name" && "contains"} + {f.key === "kind" && "is"} + {f.key === "metadata" && "matches"} + + + {typeof f.value === "string" ? ( + f.value + ) : ( + + + {Object.entries(f.value).length} key + {Object.entries(f.value).length > 1 ? "s" : ""} + + + {Object.entries(f.value).map(([key, value]) => ( +
+ {key}:{" "} + {value} +
+ ))} +
+
+ )} +
- - filters={filters} - addFilters={addFilters} - className="min-w-[200px] bg-neutral-900 p-1" +
+ + + + ))} + + + filters={filters} + addFilters={addFilters} + className="min-w-[200px] bg-neutral-900 p-1" + > + + Name + + + Kind + + + Metadata + + +
- {targets.data?.total != null && ( -
- Total: - - {targets.data.total} - -
- )} + {targets.data?.total != null && ( +
+ Total: + + {targets.data.total} +
+ )} +
- {targets.isLoading && ( -
- {_.range(10).map((i) => ( - - ))} -
- )} - {targets.isSuccess && targets.data.total === 0 && ( - + {_.range(10).map((i) => ( + - )} - {targets.data != null && targets.data.total > 0 && ( -
- setSelectedTargetId(r.id)} - /> -
- )} -
- - - -
- {target.data?.name ? ( - - {target.data.name} - - ) : ( - - )} - {target.data != null && ( - - )} + ))}
-
- {target.data && ( - - - - General - - - Deployments - - - - - - - - {targetId && } - - - )} + )} + {targets.isSuccess && targets.data.total === 0 && ( + + )} + {targets.data != null && targets.data.total > 0 && ( +
+ setSelectedTargetId(r.id)} + />
- - + )} + setSelectedTargetId(null)} + targetId={targetId} + /> +
); } diff --git a/packages/ui/src/drawer.tsx b/packages/ui/src/drawer.tsx index 0cde646d..07afcbff 100644 --- a/packages/ui/src/drawer.tsx +++ b/packages/ui/src/drawer.tsx @@ -1,18 +1,14 @@ "use client"; import * as React from "react"; -import { Drawer as DrawerPrimitive } from "vaul"; +import * as DrawerPrimitive from "@radix-ui/react-dialog"; import { cn } from "@ctrlplane/ui"; const Drawer = ({ - shouldScaleBackground = true, ...props }: React.ComponentProps) => ( - + ); Drawer.displayName = "Drawer"; @@ -28,27 +24,37 @@ const DrawerOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( )); DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; +interface DrawerContentProps + extends React.ComponentPropsWithoutRef { + showBar?: boolean; +} + const DrawerContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + DrawerContentProps +>(({ className, children, showBar = true, ...props }, ref) => ( -
+ {showBar ? ( +
+ ) : null} {children} diff --git a/packages/validators/src/targets/index.ts b/packages/validators/src/targets/index.ts index b0fa9cbc..b7fb3c74 100644 --- a/packages/validators/src/targets/index.ts +++ b/packages/validators/src/targets/index.ts @@ -60,6 +60,10 @@ export type MetadataCondition = | RegexCondition | EqualCondition; -export enum SpecialMetadataKey { - CtrlplaneLinks = "ctrlplane/links", +export enum ReservedMetadataKey { + ExternalId = "ctrlplane/external-id", + Links = "ctrlplane/links", + ParentTargetIdentifier = "ctrlplane/parent-target-identifier", + KubernetesVersion = "kubernetes/version", + KubernetesFlavor = "kubernetes/flavor", } diff --git a/providers/google-compute-scanner/src/gke.ts b/providers/google-compute-scanner/src/gke.ts index 3c43ea78..6941d77a 100644 --- a/providers/google-compute-scanner/src/gke.ts +++ b/providers/google-compute-scanner/src/gke.ts @@ -62,8 +62,11 @@ export const getKubernetesClusters = async (): Promise< }, metadata: omitNullUndefined({ "ctrlplane/links": JSON.stringify({ "Google Console": appUrl }), + "ctrlplane/external-id": cluster.id ?? "", + + "kubernetes/flavor": "gke", + "kubernetes/version": masterVersion.version, - "kubernetes/distribution": "gke", "kubernetes/status": cluster.status, "kubernetes/node-count": String(cluster.currentNodeCount ?? 0), @@ -112,6 +115,7 @@ export const getKubernetesNamespace = async ( identifier: `${env.GOOGLE_PROJECT_ID}/${cluster.name}/${n.metadata!.name}`, config: { namespace: n.metadata!.name }, metadata: { + "ctrlplane/parent-target-identifier": target.identifier, "kubernetes/namespace": n.metadata!.name, }, },