Skip to content

Commit

Permalink
Merge branch 'main' of github.com:sizzldev/ctrlplane into resource-re…
Browse files Browse the repository at this point in the history
…latioships-visualization
  • Loading branch information
adityachoudhari26 committed Nov 18, 2024
2 parents 4b993b9 + 796a182 commit 80eede6
Show file tree
Hide file tree
Showing 30 changed files with 5,848 additions and 146 deletions.
2 changes: 2 additions & 0 deletions apps/webservice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"openapi": "ts-node tooling/openapi/merge.ts"
},
"dependencies": {
"@ai-sdk/openai": "^0.0.72",
"@ctrlplane/api": "workspace:*",
"@ctrlplane/auth": "workspace:*",
"@ctrlplane/db": "workspace:*",
Expand Down Expand Up @@ -50,6 +51,7 @@
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"add": "^2.0.6",
"ai": "^3.4.33",
"change-case": "^5.4.4",
"dagre": "^0.8.5",
"date-fns": "catalog:",
Expand Down
15 changes: 15 additions & 0 deletions apps/webservice/src/app/[workspaceSlug]/SidebarContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client";

import { createContext, useContext } from "react";

type SidebarContextType = {
activeSidebarItem: string | null;
setActiveSidebarItem: (item: string | null) => void;
};

export const SidebarContext = createContext<SidebarContextType>({
activeSidebarItem: null,
setActiveSidebarItem: () => {},
});

export const useSidebar = () => useContext(SidebarContext);
11 changes: 10 additions & 1 deletion apps/webservice/src/app/[workspaceSlug]/SidebarLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { usePathname } from "next/navigation";

import { cn } from "@ctrlplane/ui";

import { useSidebar } from "./SidebarContext";

export const SidebarLink: React.FC<{
href: string;
children: React.ReactNode;
exact?: boolean;
className?: string;
hideActiveEffect?: boolean;
}> = ({ href, exact, children, hideActiveEffect }) => {
}> = ({ href, exact, children, className, hideActiveEffect }) => {
const { setActiveSidebarItem } = useSidebar();
const pathname = usePathname();
const active = hideActiveEffect
? false
Expand All @@ -20,7 +24,12 @@ export const SidebarLink: React.FC<{
return (
<Link
href={href}
onClick={() => {
console.log("setting null");
setActiveSidebarItem(null);
}}
className={cn(
className,
active ? "bg-neutral-800/70" : "hover:bg-neutral-800/50",
"flex items-center gap-2 rounded-md px-2 py-1",
)}
Expand Down
7 changes: 2 additions & 5 deletions apps/webservice/src/app/[workspaceSlug]/SidebarMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ export const SidebarMain: React.FC<{
deploymentSlug?: string;
}>();

const system = api.system.bySlug.useQuery(
{ workspaceSlug, systemSlug: systemSlug ?? "" },
{ enabled: systemSlug != null },
);
const system = systems.find((s) => s.slug === systemSlug);
const deployment = api.deployment.bySlug.useQuery(
{
workspaceSlug,
Expand Down Expand Up @@ -56,7 +53,7 @@ export const SidebarMain: React.FC<{

<SidebarCreateMenu
workspace={workspace}
systemId={system.data?.id}
systemId={system?.id}
deploymentId={deployment.data?.id}
/>
</div>
Expand Down
82 changes: 65 additions & 17 deletions apps/webservice/src/app/[workspaceSlug]/SidebarPanels.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
"use client";

import type { System, Workspace } from "@ctrlplane/db/schema";
import React from "react";
import React, { useState } from "react";
import { usePathname } from "next/navigation";

import { Popover, PopoverContent, PopoverTrigger } from "@ctrlplane/ui/popover";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@ctrlplane/ui/resizable";

import { SidebarContext } from "./SidebarContext";
import { SidebarMain } from "./SidebarMain";
import { SidebarPopoverSystem } from "./SidebarPopoverSystem";
import { SidebarPopoverTargets } from "./SidebarPopoverTargets";
import { SidebarSettings } from "./SidebarSettings";

export const SidebarPanels: React.FC<{
Expand All @@ -20,22 +24,66 @@ export const SidebarPanels: React.FC<{
}> = ({ children, systems, workspace }) => {
const pathname = usePathname();
const isSettingsPage = pathname.includes("/settings");
const [open, setOpen] = useState(false);
const [activeSidebarItem, setActiveSidebarItem] = useState<string | null>(
null,
);

return (
<ResizablePanelGroup direction="horizontal" className="h-full">
<ResizablePanel
className="min-w-[220px] max-w-[300px] bg-black"
defaultSize={10}
>
<div className="scrollbar-thin scrollbar-thumb-neutral-800 scrollbar-track-neutral-900 h-[100vh] overflow-auto pb-12">
{isSettingsPage ? (
<SidebarSettings workspaceSlug={workspace.slug} />
) : (
<SidebarMain workspace={workspace} systems={systems} />
)}
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={90}>{children}</ResizablePanel>
</ResizablePanelGroup>
<SidebarContext.Provider
value={{ activeSidebarItem, setActiveSidebarItem }}
>
<ResizablePanelGroup direction="horizontal" className="relative h-full">
<Popover
open={open && activeSidebarItem != null}
onOpenChange={setOpen}
>
<PopoverTrigger
onClick={(e) => e.preventDefault()}
className="focus-visible:outline-none"
>
<ResizablePanel
className="min-w-[220px] max-w-[300px] bg-black"
defaultSize={10}
onMouseEnter={() => setOpen(true)}
>
<div className="scrollbar-thin scrollbar-thumb-neutral-800 scrollbar-track-neutral-900 h-[100vh] overflow-auto pb-12">
{isSettingsPage ? (
<SidebarSettings workspaceSlug={workspace.slug} />
) : (
<SidebarMain workspace={workspace} systems={systems} />
)}
</div>
</ResizablePanel>
</PopoverTrigger>
<PopoverContent
side="right"
sideOffset={1}
className="h-[100vh] w-[300px] rounded-none border-y-0 border-l-0 bg-black"
>
{activeSidebarItem === "targets" && (
<SidebarPopoverTargets workspace={workspace} />
)}
{activeSidebarItem?.startsWith("systems:") && (
<SidebarPopoverSystem
systemId={activeSidebarItem.replace("systems:", "")}
workspace={workspace}
/>
)}
</PopoverContent>
</Popover>

<ResizableHandle />
<ResizablePanel
defaultSize={90}
onMouseEnter={() => {
setOpen(false);
setActiveSidebarItem(null);
}}
>
{children}
</ResizablePanel>
</ResizablePanelGroup>{" "}
</SidebarContext.Provider>
);
};
51 changes: 51 additions & 0 deletions apps/webservice/src/app/[workspaceSlug]/SidebarPopoverSystem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import type { Workspace } from "@ctrlplane/db/schema";

import { api } from "~/trpc/react";
import { SidebarLink } from "./SidebarLink";

export const SidebarPopoverSystem: React.FC<{
systemId: string;
workspace: Workspace;
}> = ({ workspace, systemId }) => {
const system = api.system.byId.useQuery(systemId);
const environments = api.environment.bySystemId.useQuery(systemId);
const deployments = api.deployment.bySystemId.useQuery(systemId);
return (
<div className="space-y-4 text-sm">
<div className="text-lg font-semibold">{system.data?.name}</div>

<div className="space-y-1.5">
<div className="text-xs font-semibold uppercase text-muted-foreground">
Environments
</div>
<div>
{environments.data?.map(({ name }) => (
<SidebarLink
href={`/${workspace.slug}/systems/${system.data?.slug}/environments`}
>
{name}
</SidebarLink>
))}
</div>
</div>

<div className="space-y-1.5">
<div className="text-xs font-semibold uppercase text-muted-foreground">
Deployments
</div>
<div>
{deployments.data?.map(({ id, name, slug }) => (
<SidebarLink
key={id}
href={`/${workspace.slug}/systems/${system.data?.slug}/deployments/${slug}`}
>
{name}
</SidebarLink>
))}
</div>
</div>
</div>
);
};
125 changes: 125 additions & 0 deletions apps/webservice/src/app/[workspaceSlug]/SidebarPopoverTargets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use client";

import type { Workspace } from "@ctrlplane/db/schema";
import { usePathname } from "next/navigation";
import { IconBookmark } from "@tabler/icons-react";
import LZString from "lz-string";

import { Badge } from "@ctrlplane/ui/badge";
import {
ComparisonOperator,
FilterType,
} from "@ctrlplane/validators/conditions";
import { ResourceFilterType } from "@ctrlplane/validators/resources";

import { api } from "~/trpc/react";
import { TargetIcon } from "./_components/TargetIcon";
import { SidebarLink } from "./SidebarLink";

export const SidebarPopoverTargets: React.FC<{ workspace: Workspace }> = ({
workspace,
}) => {
const pathname = usePathname();
const kinds = api.workspace.resourceKinds.useQuery(workspace.id);

const views = api.resource.view.list.useQuery(workspace.id);
const viewsWithHash = views.data?.map((view) => ({
...view,
hash: LZString.compressToEncodedURIComponent(JSON.stringify(view.filter)),
}));

const recentlyAdded = api.resource.byWorkspaceId.list.useQuery({
workspaceId: workspace.id,
orderBy: [{ property: "createdAt", direction: "desc" }],
limit: 5,
});

const totalTargets =
(recentlyAdded.data?.total ?? 0) - (recentlyAdded.data?.items.length ?? 0);

return (
<div className="space-y-4 text-sm">
<div className="text-lg font-semibold">Targets</div>

<div className="space-y-1.5">
<div className="text-xs font-semibold uppercase text-muted-foreground">
Saved Views
</div>
<div>
<div className="rounded-md text-xs text-neutral-600">
No saved filters found.
</div>
{viewsWithHash != null && viewsWithHash.length > 0 && (
<>
{viewsWithHash.map(({ id, name, hash }) => (
<SidebarLink
href={`/${workspace.slug}/targets?filter=${hash}`}
key={id}
>
<IconBookmark className="h-4 w-4 text-muted-foreground" />
{name}
</SidebarLink>
))}
</>
)}
</div>
</div>

<div className="space-y-1.5">
<div className="text-xs font-semibold uppercase text-muted-foreground">
Kinds
</div>
<div>
{kinds.data?.map(({ version, kind, count }) => (
<SidebarLink
href={`/${workspace.slug}/targets?filter=${LZString.compressToEncodedURIComponent(
JSON.stringify({
type: FilterType.Comparison,
operator: ComparisonOperator.And,
conditions: [
{
type: ResourceFilterType.Kind,
value: kind,
operator: "equals",
},
],
}),
)}`}
key={`${version}/${kind}`}
>
<TargetIcon version={version} kind={kind} />
<span className="flex-grow">{kind}</span>
<Badge
variant="secondary"
className="rounded-full bg-neutral-500/10 text-xs text-muted-foreground"
>
{count}
</Badge>
</SidebarLink>
))}
</div>
</div>

<div className="space-y-1.5">
<div className="text-xs font-semibold uppercase text-muted-foreground">
Recently Added Targets
</div>
<div>
{recentlyAdded.data?.items.map((resource) => (
<SidebarLink
href={`${pathname}?target_id=${resource.id}`}
key={resource.id}
>
<span className="flex-grow">{resource.name}</span>
</SidebarLink>
))}
{totalTargets > 0 && (
<div className="mt-2 px-1 text-xs text-muted-foreground">
+{totalTargets} other targets
</div>
)}
</div>
</div>
</div>
);
};
3 changes: 3 additions & 0 deletions apps/webservice/src/app/[workspaceSlug]/SidebarSystems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import {
} from "@ctrlplane/ui/collapsible";

import { CreateSystemDialog } from "./_components/CreateSystem";
import { useSidebar } from "./SidebarContext";
import { SidebarLink } from "./SidebarLink";

const SystemCollapsible: React.FC<{ system: System }> = ({ system }) => {
const { setActiveSidebarItem } = useSidebar();
const [open, setOpen] = useLocalStorage(
`sidebar-systems-${system.id}`,
"false",
Expand All @@ -36,6 +38,7 @@ const SystemCollapsible: React.FC<{ system: System }> = ({ system }) => {
open={open === "true"}
onOpenChange={() => setOpen(open === "true" ? "false" : "true")}
className="space-y-1 text-sm"
onMouseEnter={() => setActiveSidebarItem(`systems:${system.id}`)}
>
<CollapsibleTrigger className="flex w-full items-center gap-2 rounded-md px-2 py-1 hover:bg-neutral-800/50">
<span className="truncate">{system.name}</span>
Expand Down
Loading

0 comments on commit 80eede6

Please sign in to comment.