Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Enhance API Key Detail Page: Change Permissions Visualization to Tree Format #2238

Merged
merged 14 commits into from
Oct 17, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Link from "next/link";

import { type Interval, IntervalSelect } from "@/app/(app)/apis/[apiId]/select";
import { CreateNewPermission } from "@/app/(app)/authorization/permissions/create-new-permission";
import type { NestedPermissions } from "@/app/(app)/authorization/roles/[roleId]/tree";
import { CreateNewRole } from "@/app/(app)/authorization/roles/create-new-role";
import { StackedColumnChart } from "@/components/dashboard/charts";
import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder";
Expand All @@ -23,7 +24,7 @@ import { cn } from "@/lib/utils";
import { BarChart, Minus } from "lucide-react";
import ms from "ms";
import { notFound } from "next/navigation";
import { Chart } from "./chart";
import PermissionTree from "./permission-list";
import { VerificationTable } from "./verification-table";

export default async function APIKeyDetailPage(props: {
Expand Down Expand Up @@ -71,6 +72,7 @@ export default async function APIKeyDetailPage(props: {
},
},
});

if (!key || key.workspace.tenantId !== tenantId) {
return notFound();
}
Expand Down Expand Up @@ -155,6 +157,38 @@ export default async function APIKeyDetailPage(props: {
}
}

const roleTee = key.workspace.roles.map((role) => {
const nested: NestedPermissions = {};
for (const permission of key.workspace.permissions) {
let n = nested;
const parts = permission.name.split(".");
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (!(p in n)) {
n[p] = {
id: permission.id,
name: permission.name,
description: permission.description,
checked: role.permissions.some((p) => p.permissionId === permission.id),
part: p,
permissions: {},
path: parts.slice(0, i).join("."),
};
}
n = n[p].permissions;
}
}
const data = {
id: role.id,
name: role.name,
description: role.description,
keyId: key.id,
active: key.roles.some((keyRole) => keyRole.roleId === role.id),
nestedPermissions: nested,
};
return data;
});

return (
<div className="flex flex-col">
<div className="flex items-center justify-between w-full">
Expand Down Expand Up @@ -308,7 +342,8 @@ export default async function APIKeyDetailPage(props: {
</div>
</div>

<Chart
<PermissionTree roles={roleTee} />
{/* <Chart
chronark marked this conversation as resolved.
Show resolved Hide resolved
apiId={props.params.apiId}
key={JSON.stringify(key)}
data={key}
Expand All @@ -320,7 +355,7 @@ export default async function APIKeyDetailPage(props: {
...p,
active: transientPermissionIds.has(p.id),
}))}
/>
/> */}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"use client";
import { RecursivePermission } from "@/app/(app)/authorization/roles/[roleId]/tree";
import { CopyButton } from "@/components/dashboard/copy-button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { ChevronRight } from "lucide-react";
import { useEffect, useState } from "react";
import { RoleToggle } from "./role-toggle";

export type NestedPermission = {
id: string;
checked: boolean;
description: string | null;
name: string;
part: string;
path: string;
permissions: NestedPermissions;
};

export type NestedPermissions = Record<string, NestedPermission>;

export type Role = {
id: string;
name: string;
keyId: string;
active: boolean;
description: string | null;
nestedPermissions: NestedPermissions;
};

type PermissionTreeProps = {
roles: Role[];
};

export default function PermissionTree({ roles }: PermissionTreeProps) {
const [openAll, setOpenAll] = useState(false);
const [openRoles, setOpenRoles] = useState<string[]>([]);

useEffect(() => {
setOpenRoles(openAll ? roles.map((role) => role.id) : []);
}, [openAll, roles]);
chronark marked this conversation as resolved.
Show resolved Hide resolved

return (
<Card>
<CardHeader className="flex-row items-start justify-between">
<div className="flex flex-col space-y-1.5">
<CardTitle>Permissions</CardTitle>
</div>
<div className="flex items-center gap-2">
<Label>{openAll ? "Collapse" : "Expand"} All Roles</Label>
<Switch checked={openAll} onCheckedChange={setOpenAll} />
</div>
</CardHeader>

<CardContent className="flex flex-col gap-4">
{roles.map((role) => {
const isOpen = openRoles.includes(role.id);
return (
<Collapsible
key={role.id}
open={isOpen}
onOpenChange={(open) => {
setOpenRoles((prev) =>
open
? prev.includes(role.id)
? prev
: [...prev, role.id]
: prev.filter((id) => id !== role.id),
);
}}
>
<CollapsibleTrigger className="flex items-center gap-1 transition-all [&[data-state=open]>svg]:rotate-90 ">
<Tooltip delayDuration={50}>
<TooltipTrigger className="flex items-center gap-2">
<ChevronRight className="w-4 h-4 transition-transform duration-200" />
<RoleToggle keyId={role.keyId} roleId={role.id} checked={role.active} />
<span className="text-sm">{role.name}</span>
</TooltipTrigger>
<TooltipContent side="top" align="start" avoidCollisions={true}>
<div className="flex items-center justify-start max-w-sm gap-2 text-content">
<span className="text-ellipsis overflow-hidden hover:overflow-visible">
{role.name}
</span>
<div>
<CopyButton value={role.name} />
</div>
</div>
</TooltipContent>
</Tooltip>
</CollapsibleTrigger>
chronark marked this conversation as resolved.
Show resolved Hide resolved
<CollapsibleContent className="pt-2 pb-4">
<div className="flex flex-col gap-1 ml-4">
{Object.entries(role.nestedPermissions).map(([k, p]) => (
<RecursivePermission
key={p.id}
k={k}
{...p}
roleId={role.id}
openAll={openAll}
/>
))}
</div>
</CollapsibleContent>
</Collapsible>
);
})}
</CardContent>
</Card>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const Tree: React.FC<Props> = ({ nestedPermissions, role }) => {
);
};

const RecursivePermission: React.FC<
export const RecursivePermission: React.FC<
NestedPermission & { k: string; roleId: string; openAll: boolean }
> = ({ k, openAll, id, name, permissions, roleId, checked, description }) => {
const [open, setOpen] = useState(openAll);
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { ThemeProvider } from "./theme-provider";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
adjustFontFallback: false,
});

const pangea = localFont({
Expand Down
2 changes: 2 additions & 0 deletions apps/engineering/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import "./global.css";

const inter = Inter({
subsets: ["latin"],
display: "swap",
adjustFontFallback: false,
});

export default function Layout({ children }: { children: ReactNode }) {
Expand Down
6 changes: 5 additions & 1 deletion apps/planetfall/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });
const inter = Inter({
subsets: ["latin"],
display: "swap",
adjustFontFallback: false,
});

export const metadata: Metadata = {
title: "Create Next App",
Expand Down
6 changes: 5 additions & 1 deletion apps/workflows/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });
const inter = Inter({
subsets: ["latin"],
display: "swap",
adjustFontFallback: false,
});

export const metadata: Metadata = {
title: "Create Next App",
Expand Down