Skip to content

Commit e327bae

Browse files
committed
init basic tree view
1 parent 43f7789 commit e327bae

File tree

2 files changed

+219
-0
lines changed

2 files changed

+219
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"use client";
2+
3+
import type * as SCHEMA from "@ctrlplane/db/schema";
4+
import Link from "next/link";
5+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
6+
import {
7+
IconChevronDown,
8+
IconPlant,
9+
IconShip,
10+
IconTopologyComplex,
11+
} from "@tabler/icons-react";
12+
13+
import { urls } from "~/app/urls";
14+
import { api } from "~/trpc/react";
15+
16+
const ChevronIcon: React.FC<{ expandable?: boolean }> = ({ expandable }) =>
17+
expandable ? (
18+
<IconChevronDown
19+
className={
20+
"h-3 w-3 shrink-0 text-accent-foreground/50 transition-transform duration-200"
21+
}
22+
/>
23+
) : (
24+
<div className="h-3 w-3"></div>
25+
);
26+
27+
const SystemDeployments: React.FC<{
28+
workspaceSlug: string;
29+
systemId: string;
30+
systemSlug: string;
31+
}> = ({ workspaceSlug, systemId, systemSlug }) => {
32+
const { data: deployments } = api.deployment.bySystemId.useQuery(systemId, {
33+
placeholderData: (prev) => prev,
34+
});
35+
return (
36+
<AccordionPrimitive.Item value="deployments">
37+
<AccordionPrimitive.Trigger className="flex w-full flex-1 items-center gap-2 rounded-md px-2 py-2 pl-6 transition-all hover:bg-accent/50 first:[&[data-state=open]>svg]:rotate-90">
38+
<ChevronIcon expandable />
39+
<IconShip className="h-4 w-4 text-blue-400" />
40+
<span>Deployments</span>
41+
<div className="flex-grow" />
42+
<div className="rounded-full border border-blue-500/50 bg-blue-500/10 px-2 py-0 text-xs text-blue-400">
43+
{deployments?.length ?? "-"}
44+
</div>
45+
</AccordionPrimitive.Trigger>
46+
<AccordionPrimitive.Content>
47+
{deployments?.map((d) => (
48+
<Link
49+
key={d.id}
50+
href={urls
51+
.workspace(workspaceSlug)
52+
.system(systemSlug)
53+
.deployment(d.slug)
54+
.baseUrl()}
55+
className="flex w-full flex-1 items-center gap-2 rounded-md px-2 py-2 pl-12 hover:bg-accent/50"
56+
>
57+
{d.name}
58+
</Link>
59+
))}
60+
</AccordionPrimitive.Content>
61+
</AccordionPrimitive.Item>
62+
);
63+
};
64+
65+
const SystemEnvironments: React.FC<{
66+
workspaceSlug: string;
67+
systemId: string;
68+
systemSlug: string;
69+
}> = ({ workspaceSlug, systemId, systemSlug }) => {
70+
const { data: environments } = api.environment.bySystemId.useQuery(systemId, {
71+
placeholderData: (prev) => prev,
72+
});
73+
74+
return (
75+
<AccordionPrimitive.Item value="environments">
76+
<AccordionPrimitive.Trigger className="flex w-full flex-1 items-center gap-2 rounded-md px-2 py-2 pl-6 transition-all hover:bg-accent/50 first:[&[data-state=open]>svg]:rotate-90">
77+
<ChevronIcon expandable />
78+
<IconPlant className="h-4 w-4 text-green-400" />
79+
<span>Environments</span>
80+
<div className="flex-grow" />
81+
<div className="rounded-full border border-green-500/50 bg-green-500/10 px-2 py-0 text-xs text-green-400">
82+
{environments?.length ?? "-"}
83+
</div>
84+
</AccordionPrimitive.Trigger>
85+
<AccordionPrimitive.Content>
86+
{environments?.map((e) => (
87+
<Link
88+
key={e.id}
89+
href={urls
90+
.workspace(workspaceSlug)
91+
.system(systemSlug)
92+
.environment(e.id)
93+
.baseUrl()}
94+
className="flex w-full flex-1 items-center gap-2 rounded-md px-2 py-2 pl-12 hover:bg-accent/50"
95+
>
96+
{e.name}
97+
</Link>
98+
))}
99+
</AccordionPrimitive.Content>
100+
</AccordionPrimitive.Item>
101+
);
102+
};
103+
104+
export const SystemTreePageContent: React.FC<{
105+
workspace: SCHEMA.Workspace;
106+
}> = ({ workspace }) => {
107+
const workspaceId = workspace.id;
108+
const { data } = api.system.list.useQuery(
109+
{ workspaceId, query: undefined },
110+
{ placeholderData: (prev) => prev },
111+
);
112+
113+
const systems = data?.items ?? [];
114+
115+
return (
116+
<div className="text-sm">
117+
<AccordionPrimitive.Root type="multiple">
118+
{systems.map((s) => (
119+
<AccordionPrimitive.Item key={s.id} value={s.id}>
120+
<AccordionPrimitive.Trigger className="flex w-full flex-1 items-center gap-2 rounded-md px-2 py-2 transition-all hover:bg-accent/50 first:[&[data-state=open]>svg]:rotate-90">
121+
<ChevronIcon expandable />
122+
<IconTopologyComplex className="h-4 w-4" />
123+
<span>{s.name}</span>
124+
</AccordionPrimitive.Trigger>
125+
<AccordionPrimitive.Content>
126+
<AccordionPrimitive.Root type="multiple">
127+
<SystemDeployments
128+
workspaceSlug={workspace.slug}
129+
systemId={s.id}
130+
systemSlug={s.slug}
131+
/>
132+
<SystemEnvironments
133+
workspaceSlug={workspace.slug}
134+
systemId={s.id}
135+
systemSlug={s.slug}
136+
/>
137+
</AccordionPrimitive.Root>
138+
</AccordionPrimitive.Content>
139+
</AccordionPrimitive.Item>
140+
))}
141+
</AccordionPrimitive.Root>
142+
</div>
143+
);
144+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { Metadata } from "next";
2+
import { notFound } from "next/navigation";
3+
import { IconMenu2, IconTopologyComplex } from "@tabler/icons-react";
4+
5+
import {
6+
Breadcrumb,
7+
BreadcrumbItem,
8+
BreadcrumbList,
9+
BreadcrumbPage,
10+
} from "@ctrlplane/ui/breadcrumb";
11+
import { Separator } from "@ctrlplane/ui/separator";
12+
import { SidebarTrigger } from "@ctrlplane/ui/sidebar";
13+
14+
import { PageHeader } from "~/app/[workspaceSlug]/(app)/_components/PageHeader";
15+
import { Sidebars } from "~/app/[workspaceSlug]/sidebars";
16+
import { api } from "~/trpc/server";
17+
import { SystemTreePageContent } from "./SystemTreePageContent";
18+
19+
export const generateMetadata = async ({
20+
params,
21+
}: {
22+
params: Promise<{ workspaceSlug: string }>;
23+
}): Promise<Metadata> => {
24+
const { workspaceSlug } = await params;
25+
26+
return api.workspace
27+
.bySlug(workspaceSlug)
28+
.then((workspace) => ({
29+
title: `Systems | ${workspace?.name ?? workspaceSlug} | Ctrlplane`,
30+
description: `Manage and deploy systems for the ${workspace?.name ?? workspaceSlug} workspace.`,
31+
}))
32+
.catch(() => ({
33+
title: "Systems | Ctrlplane",
34+
description: "Manage and deploy your systems with Ctrlplane.",
35+
}));
36+
};
37+
38+
const SystemTreePageHeader: React.FC = () => {
39+
return (
40+
<PageHeader className="z-20 flex items-center justify-between">
41+
<div className="flex items-center gap-2">
42+
<SidebarTrigger name={Sidebars.Deployments}>
43+
<IconMenu2 className="h-4 w-4" />
44+
</SidebarTrigger>
45+
<Separator orientation="vertical" className="mr-2 h-4" />
46+
<Breadcrumb>
47+
<BreadcrumbList>
48+
<BreadcrumbItem className="hidden md:block">
49+
<BreadcrumbPage className="flex items-center gap-1.5">
50+
<IconTopologyComplex className="h-4 w-4" />
51+
Systems Tree View
52+
</BreadcrumbPage>
53+
</BreadcrumbItem>
54+
</BreadcrumbList>
55+
</Breadcrumb>
56+
</div>
57+
</PageHeader>
58+
);
59+
};
60+
61+
export default async function SystemsTreePage(props: {
62+
params: Promise<{ workspaceSlug: string }>;
63+
}) {
64+
const params = await props.params;
65+
const workspace = await api.workspace.bySlug(params.workspaceSlug);
66+
if (workspace == null) notFound();
67+
return (
68+
<div className="flex h-full flex-col">
69+
<SystemTreePageHeader />
70+
<div className="scrollbar-thin scrollbar-thumb-neutral-700 scrollbar-track-neutral-800 flex-1 overflow-y-auto">
71+
<SystemTreePageContent workspace={workspace} />
72+
</div>
73+
</div>
74+
);
75+
}

0 commit comments

Comments
 (0)