manage users

This commit is contained in:
mbecker20
2024-03-30 18:04:45 -07:00
parent 1f143b814e
commit d02df02bbe
12 changed files with 583 additions and 254 deletions
+6 -4
View File
@@ -162,10 +162,12 @@ export const ResourceSelector = ({
type,
selected,
onSelect,
disabled,
}: {
type: UsableResources;
selected: string | undefined;
onSelect: (id: string) => void;
onSelect?: (id: string) => void;
disabled?: boolean;
}) => {
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
@@ -178,9 +180,9 @@ export const ResourceSelector = ({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="secondary" className="flex gap-2">
<Button variant="secondary" className="flex gap-2" disabled={disabled}>
{name ?? `Select ${type}`}
<ChevronsUpDown className="w-3 h-3" />
{!disabled && <ChevronsUpDown className="w-3 h-3" />}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] max-h-[200px] p-0" sideOffset={12}>
@@ -199,7 +201,7 @@ export const ResourceSelector = ({
{resources.map((resource) => (
<CommandItem
key={resource.id}
onSelect={() => onSelect(resource.id)}
onSelect={() => onSelect && onSelect(resource.id)}
className="flex items-center justify-between"
>
<div className="p-1">{resource.name}</div>
+16 -40
View File
@@ -1,12 +1,7 @@
import { useResourceParamType } from "@lib/hooks";
import { Button } from "@ui/button";
import { ThemeToggle } from "@ui/theme";
import { PlusCircle } from "lucide-react";
import { ReactNode, useState } from "react";
import { Link, Outlet } from "react-router-dom";
import { Omnibar } from "./omnibar";
import { WsStatusIndicator } from "@lib/socket";
import { UsableResource } from "@types";
import {
Dialog,
DialogContent,
@@ -25,37 +20,13 @@ import {
CardContent,
CardFooter,
} from "@ui/card";
import { Logout, ResourceTypeDropdown, ResourcesDropdown } from "./util";
import { HeaderUpdates } from "./updates/header";
import { ResourceTags } from "./tags";
import { Topbar } from "./topbar";
export const Layout = () => {
const type = useResourceParamType();
return (
<>
<div className="sticky top-0 border-b bg-background z-50 w-full">
<div className="container flex items-center justify-between py-4 gap-8">
<div className="flex items-center gap-4">
<Link to={"/"} className="text-2xl tracking-widest">
MONITOR
</Link>
<div className="flex gap-2">
<ResourceTypeDropdown />
{type && <ResourcesDropdown />}
</div>
</div>
<div className="flex md:gap-4">
<Omnibar />
<div className="flex">
<WsStatusIndicator />
<HeaderUpdates />
<ThemeToggle />
{/* <UserSettings /> */}
<Logout />
</div>
</div>
</div>
</div>
<Topbar />
<Outlet />
</>
);
@@ -103,10 +74,12 @@ interface SectionProps {
export const Section = ({ title, icon, actions, children }: SectionProps) => (
<div className="flex flex-col gap-4">
<div className="flex items-start justify-between">
{(title || icon) && <div className="flex items-center gap-2 text-muted-foreground">
{icon}
<h2 className="text-xl">{title}</h2>
</div>}
{(title || icon) && (
<div className="flex items-center gap-2 text-muted-foreground">
{icon}
<h2 className="text-xl">{title}</h2>
</div>
)}
{actions}
</div>
{children}
@@ -114,12 +87,12 @@ export const Section = ({ title, icon, actions, children }: SectionProps) => (
);
export const NewResource = ({
type,
entityType,
children,
enabled,
onSuccess,
}: {
type: UsableResource;
entityType: String;
children: ReactNode;
enabled: boolean;
onSuccess: () => Promise<unknown>;
@@ -129,13 +102,16 @@ export const NewResource = ({
return (
<Dialog open={open} onOpenChange={set}>
<DialogTrigger asChild>
<Button variant="secondary" className="items-center gap-2">
New {type} <PlusCircle className="w-4 h-4" />
<Button
variant="secondary"
className="items-center gap-2"
>
New {entityType} <PlusCircle className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>New {type}</DialogTitle>
<DialogTitle>New {entityType}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-8">{children}</div>
<DialogFooter>
@@ -29,7 +29,7 @@ const NewAlerter = () => {
return (
<NewResource
type="Alerter"
entityType="Alerter"
onSuccess={async () =>
!!type && mutateAsync({ name, config: { type, params: {} } })
}
@@ -22,7 +22,7 @@ const NewBuild = () => {
const [name, setName] = useState("");
return (
<NewResource
type="Build"
entityType="Build"
onSuccess={() => mutateAsync({ name, config: {} })}
enabled={!!name}
>
@@ -29,7 +29,7 @@ const NewBuilder = () => {
return (
<NewResource
type="Builder"
entityType="Builder"
onSuccess={async () =>
!!type && mutateAsync({ name, config: { type, params: {} } })
}
@@ -101,10 +101,7 @@ export const DeploymentTable = ({
cell: ({ row }) => {
const id = row.original.id;
return (
<Link
to={`/deployments/${id}`}
className="flex items-center gap-2"
>
<Link to={`/deployments/${id}`}>
<Button variant="link" className="flex gap-2 items-center p-0">
<ResourceComponents.Deployment.Icon id={id} />
<ResourceComponents.Deployment.Name id={id} />
@@ -214,7 +211,7 @@ export const DeploymentComponents: RequiredResourceComponents = {
const [name, setName] = useState("");
return (
<NewResource
type="Deployment"
entityType="Deployment"
onSuccess={() => mutateAsync({ name, config: {} })}
enabled={!!name}
>
@@ -95,7 +95,7 @@ export const ProcedureComponents: RequiredResourceComponents = {
const [type, setType] = useState<Types.ProcedureConfig["type"]>("Sequence");
return (
<NewResource
type="Procedure"
entityType="Procedure"
onSuccess={() => mutateAsync({ name, config: { type, data: [] } })}
enabled={!!name}
>
@@ -133,7 +133,7 @@ const NewServer = () => {
const [name, setName] = useState("");
return (
<NewResource
type="Server"
entityType="Server"
onSuccess={() => mutateAsync({ name, config: {} })}
enabled={!!name}
>
+216
View File
@@ -0,0 +1,216 @@
import { useRead, useResourceParamType } from "@lib/hooks";
import { ResourceComponents } from "./resources";
import {
Box,
Boxes,
FolderTree,
Key,
Moon,
SunMedium,
Tag,
UserCircle2,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@ui/dropdown-menu";
import { Button } from "@ui/button";
import { Link, useParams } from "react-router-dom";
import { RESOURCE_TARGETS } from "@lib/utils";
import { Omnibar } from "./omnibar";
import { WsStatusIndicator } from "@lib/socket";
import { HeaderUpdates } from "./updates/header";
import { useEffect, useState } from "react";
import { Logout } from "./util";
export const Topbar = () => {
const type = useResourceParamType();
return (
<div className="sticky top-0 border-b bg-background z-50 w-full">
<div className="container flex items-center justify-between py-4 gap-8">
<div className="flex items-center gap-4">
<Link to={"/"} className="text-2xl tracking-widest">
MONITOR
</Link>
<div className="flex gap-2">
<ResourceTypeDropdown />
{type && <ResourcesDropdown />}
</div>
</div>
<div className="flex md:gap-4">
<Omnibar />
<div className="flex">
<WsStatusIndicator />
<HeaderUpdates />
<ThemeToggle />
{/* <UserSettings /> */}
<Logout />
</div>
</div>
</div>
</div>
);
};
const ThemeToggle = () => {
const [theme, set] = useState(localStorage.getItem("theme"));
useEffect(() => {
localStorage.setItem("theme", theme ?? "dark");
if (theme === "dark") document.body.classList.remove("dark");
else document.body.classList.add("dark");
}, [theme]);
return (
<Button
variant="ghost"
onClick={() => set(theme === "dark" ? "light" : "dark")}
>
<SunMedium className="w-4 h-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="w-4 h-4 absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
);
};
const ResourceTypeDropdown = () => {
const type = useResourceParamType();
const Components = ResourceComponents[type];
const [icon, title] = type
? [<Components.Icon />, type + "s"]
: location.pathname === "/tree"
? [<FolderTree className="w-4 h-4" />, "Tree"]
: location.pathname === "/keys"
? [<Key className="w-4 h-4" />, "Api Keys"]
: location.pathname === "/tags"
? [<Tag className="w-4 h-4" />, "Tags"]
: location.pathname === "/users"
? [<UserCircle2 className="w-4 h-4" />, "Users"]
: [<Box className="w-4 h-4" />, "Dashboard"];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-36 justify-between px-3">
<div className="flex items-center gap-2">
{icon}
{title}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-36" side="bottom">
<DropdownMenuGroup>
<Link to="/">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Box className="w-4 h-4" />
Dashboard
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<Link to="/resources">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Boxes className="w-4 h-4" />
Resources
</DropdownMenuItem>
</Link>
<Link to="/tree">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<FolderTree className="w-4 h-4" />
Tree
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
{RESOURCE_TARGETS.map((rt) => {
const RTIcon = ResourceComponents[rt].Icon;
return (
<Link key={rt} to={`/${rt.toLowerCase()}s`}>
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<RTIcon />
{rt}s
</DropdownMenuItem>
</Link>
);
})}
<DropdownMenuSeparator />
<Link to="/tags">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Tag className="w-4 h-4" />
Tags
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<Link to="/keys">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Box className="w-4 h-4" />
Api Keys
</DropdownMenuItem>
</Link>
<Link to="/users">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<UserCircle2 className="w-4 h-4" />
Users
</DropdownMenuItem>
</Link>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
const ResourcesDropdown = () => {
const type = useResourceParamType();
const id = useParams().id as string;
const list = useRead(`List${type}s`, {}).data;
const selected = list?.find((i) => i.id === id);
const Components = ResourceComponents[type];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-48 justify-between px-3">
<div className="flex items-center gap-2">
<Components.Icon id={selected?.id} />
{selected ? selected.name : `All ${type}s`}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="bottom">
<DropdownMenuGroup>
<Link to={`/${type.toLowerCase()}s`}>
<DropdownMenuItem className="flex items-center gap-2">
<Components.Icon />
All {type}s
</DropdownMenuItem>
</Link>
</DropdownMenuGroup>
<DropdownMenuGroup>
{!list?.length && (
<DropdownMenuItem disabled>No {type}s Found.</DropdownMenuItem>
)}
{list?.map(({ id, name }) => (
<Link key={id} to={`/${type.toLowerCase()}s/${id}`}>
<DropdownMenuItem className="flex items-center gap-2">
<Components.Icon id={id} />
{name}
</DropdownMenuItem>
</Link>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
+24 -187
View File
@@ -7,22 +7,7 @@ import {
useState,
} from "react";
import { Button } from "../ui/button";
import {
Box,
Boxes,
Check,
Copy,
FolderTree,
Key,
Loader2,
LogOut,
Moon,
Settings,
SunMedium,
Tag,
User,
UserCircle2,
} from "lucide-react";
import { Check, Copy, Loader2, LogOut, Settings, User } from "lucide-react";
import { Input } from "../ui/input";
import {
Dialog,
@@ -33,25 +18,18 @@ import {
DialogFooter,
} from "@ui/dialog";
import { toast, useToast } from "@ui/use-toast";
import { RESOURCE_TARGETS, cn } from "@lib/utils";
import {
useInvalidate,
useRead,
useResourceParamType,
useWrite,
} from "@lib/hooks";
import { Link, useNavigate, useParams } from "react-router-dom";
import { cn } from "@lib/utils";
import { useInvalidate, useWrite } from "@lib/hooks";
import { Link, useNavigate } from "react-router-dom";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@ui/dropdown-menu";
import { ResourceComponents } from "./resources";
import { Types } from "@monitor/client";
import { AUTH_TOKEN_STORAGE_KEY } from "@main";
import { UsableResource } from "@types";
import { ResourceComponents } from "./resources";
export const WithLoading = ({
children,
@@ -88,26 +66,6 @@ export const ConfigInput = ({
/>
);
export const ThemeToggle = () => {
const [theme, set] = useState(localStorage.getItem("theme"));
useEffect(() => {
localStorage.setItem("theme", theme ?? "dark");
if (theme === "dark") document.body.classList.remove("dark");
else document.body.classList.add("dark");
}, [theme]);
return (
<Button
variant="ghost"
onClick={() => set(theme === "dark" ? "light" : "dark")}
>
<SunMedium className="w-4 h-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="w-4 h-4 absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
</Button>
);
};
export const ActionButton = forwardRef<
HTMLButtonElement,
{
@@ -326,145 +284,6 @@ export const ConfirmButton = ({
);
};
export const ResourceTypeDropdown = () => {
const type = useResourceParamType();
const Components = ResourceComponents[type];
const [icon, title] = type
? [<Components.Icon />, type + "s"]
: location.pathname === "/tree"
? [<FolderTree className="w-4 h-4" />, "Tree"]
: location.pathname === "/keys"
? [<Key className="w-4 h-4" />, "Api Keys"]
: location.pathname === "/tags"
? [<Tag className="w-4 h-4" />, "Tags"]
: location.pathname === "/users"
? [<UserCircle2 className="w-4 h-4" />, "Users"]
: [<Box className="w-4 h-4" />, "Dashboard"];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-36 justify-between px-3">
<div className="flex items-center gap-2">
{icon}
{title}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-36" side="bottom">
<DropdownMenuGroup>
<Link to="/">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Box className="w-4 h-4" />
Dashboard
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<Link to="/resources">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Boxes className="w-4 h-4" />
Resources
</DropdownMenuItem>
</Link>
<Link to="/tree">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<FolderTree className="w-4 h-4" />
Tree
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
{RESOURCE_TARGETS.map((rt) => {
const RTIcon = ResourceComponents[rt].Icon;
return (
<Link key={rt} to={`/${rt.toLowerCase()}s`}>
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<RTIcon />
{rt}s
</DropdownMenuItem>
</Link>
);
})}
<DropdownMenuSeparator />
<Link to="/tags">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Tag className="w-4 h-4" />
Tags
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<Link to="/keys">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Box className="w-4 h-4" />
Api Keys
</DropdownMenuItem>
</Link>
<Link to="/users">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<UserCircle2 className="w-4 h-4" />
Users
</DropdownMenuItem>
</Link>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
export const ResourcesDropdown = () => {
const type = useResourceParamType();
const id = useParams().id as string;
const list = useRead(`List${type}s`, {}).data;
const selected = list?.find((i) => i.id === id);
const Components = ResourceComponents[type];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-48 justify-between px-3">
<div className="flex items-center gap-2">
<Components.Icon id={selected?.id} />
{selected ? selected.name : `All ${type}s`}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="bottom">
<DropdownMenuGroup>
<Link to={`/${type.toLowerCase()}s`}>
<DropdownMenuItem className="flex items-center gap-2">
<Components.Icon />
All {type}s
</DropdownMenuItem>
</Link>
</DropdownMenuGroup>
<DropdownMenuGroup>
{!list?.length && (
<DropdownMenuItem disabled>No {type}s Found.</DropdownMenuItem>
)}
{list?.map(({ id, name }) => (
<Link key={id} to={`/${type.toLowerCase()}s/${id}`}>
<DropdownMenuItem className="flex items-center gap-2">
<Components.Icon id={id} />
{name}
</DropdownMenuItem>
</Link>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
export const Logout = () => (
<Button
variant="ghost"
@@ -550,3 +369,21 @@ const alert_level_text_color = (level: Types.SeverityLevel) => {
export const AlertLevel = ({ level }: { level: Types.SeverityLevel }) => {
return <div className={alert_level_text_color(level)}>{level}</div>;
};
export const ResourceLink = ({
type,
id,
}: {
type: UsableResource;
id: string;
}) => {
const Components = ResourceComponents[type];
return (
<Link to={`/${type.toLowerCase()}s/${id}`}>
<Button variant="link" className="flex gap-2 items-center p-0">
<Components.Icon id={id} />
<Components.Name id={id} />
</Button>
</Link>
);
};
+17 -6
View File
@@ -14,12 +14,15 @@ import {
useQueryClient,
} from "@tanstack/react-query";
import { UsableResource } from "@types";
import { useToast } from "@ui/use-toast";
import { useEffect } from "react";
import { useParams } from "react-router-dom";
// ============== RESOLVER ==============
const token = () => ({ jwt: localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) ?? "" });
const token = () => ({
jwt: localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) ?? "",
});
const client = () => Client(MONITOR_BASE_URL, { type: "jwt", params: token() });
export const useLoginOptions = () =>
@@ -88,11 +91,19 @@ export const useWrite = <
type: T,
config?: C
) =>
useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().write({ type, params } as R),
...config,
});
{
const { toast } = useToast();
return useMutation({
mutationKey: [type],
mutationFn: (params: P) => client().write({ type, params } as R),
...config,
onError: (e, v, c) => {
console.log("useWrite error:", e);
toast({ title: `Request ${type} Failed`, });
config?.onError && config.onError(e, v, c);
},
});
};
export const useExecute = <
T extends Types.ExecuteRequest["type"],
+297 -7
View File
@@ -1,11 +1,22 @@
import { Page, Section } from "@components/layouts";
import { ConfirmButton } from "@components/util";
import { ConfirmButton, ResourceLink } from "@components/util";
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { UsableResource } from "@types";
import { DataTable } from "@ui/data-table";
import { Input } from "@ui/input";
import { Label } from "@ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ui/select";
import { Switch } from "@ui/switch";
import { useToast } from "@ui/use-toast";
import { UserCheck, UserMinus } from "lucide-react";
import { useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
export const UsersPage = () => {
@@ -45,10 +56,16 @@ export const UserPage = () => {
},
});
const enabledClass = user?.enabled ? "text-green-500" : "text-red-500";
const avatar = (user?.config.data as any)?.avatar as string | undefined;
return (
user && (
<Page
title={user?.username}
title={
<div className="flex gap-4 items-center">
{user?.username}{" "}
{avatar && <img src={avatar} alt="" className="w-7 h-7" />}
</div>
}
subtitle={
<div className="text-sm text-muted-foreground flex gap-2">
<div className={enabledClass}>
@@ -98,19 +115,292 @@ export const UserPage = () => {
)
}
>
<PermissionsTable />
{!user.admin && <PermissionsTable />}
</Page>
)
);
};
const useUserPermissions = (user_id: string) => {
const permissions = useRead("ListUserPermissions", { user_id }).data;
const servers = useRead("ListServers", {}).data;
const deployments = useRead("ListDeployments", {}).data;
const builds = useRead("ListBuilds", {}).data;
const repos = useRead("ListRepos", {}).data;
const procedures = useRead("ListProcedures", {}).data;
const builders = useRead("ListBuilders", {}).data;
const alerters = useRead("ListAlerters", {}).data;
const perms: (Types.Permission & { name: string })[] = [];
servers?.forEach((server) => {
const perm = permissions?.find(
(p) => p.target.type === "Server" && p.target.id === server.id
);
if (perm) {
perms.push({ ...perm, name: server.name });
} else {
perms.push({
user_id,
name: server.name,
level: Types.PermissionLevel.None,
target: { type: "Server", id: server.id },
});
}
});
deployments?.forEach((deployment) => {
const perm = permissions?.find(
(p) => p.target.type === "Deployment" && p.target.id === deployment.id
);
if (perm) {
perms.push({ ...perm, name: deployment.name });
} else {
perms.push({
user_id,
name: deployment.name,
level: Types.PermissionLevel.None,
target: { type: "Deployment", id: deployment.id },
});
}
});
builds?.forEach((build) => {
const perm = permissions?.find(
(p) => p.target.type === "Build" && p.target.id === build.id
);
if (perm) {
perms.push({ ...perm, name: build.name });
} else {
perms.push({
user_id,
name: build.name,
level: Types.PermissionLevel.None,
target: { type: "Build", id: build.id },
});
}
});
repos?.forEach((repo) => {
const perm = permissions?.find(
(p) => p.target.type === "Repo" && p.target.id === repo.id
);
if (perm) {
perms.push({ ...perm, name: repo.name });
} else {
perms.push({
user_id,
name: repo.name,
level: Types.PermissionLevel.None,
target: { type: "Repo", id: repo.id },
});
}
});
procedures?.forEach((procedure) => {
const perm = permissions?.find(
(p) => p.target.type === "Procedure" && p.target.id === procedure.id
);
if (perm) {
perms.push({ ...perm, name: procedure.name });
} else {
perms.push({
user_id,
name: procedure.name,
level: Types.PermissionLevel.None,
target: { type: "Procedure", id: procedure.id },
});
}
});
builders?.forEach((builder) => {
const perm = permissions?.find(
(p) => p.target.type === "Builder" && p.target.id === builder.id
);
if (perm) {
perms.push({ ...perm, name: builder.name });
} else {
perms.push({
user_id,
name: builder.name,
level: Types.PermissionLevel.None,
target: { type: "Builder", id: builder.id },
});
}
});
alerters?.forEach((alerter) => {
const perm = permissions?.find(
(p) => p.target.type === "Alerter" && p.target.id === alerter.id
);
if (perm) {
perms.push({ ...perm, name: alerter.name });
} else {
perms.push({
user_id,
name: alerter.name,
level: Types.PermissionLevel.None,
target: { type: "Alerter", id: alerter.id },
});
}
});
return perms;
};
const PermissionsTable = () => {
const [showNone, setShowNone] = useState(false);
const [search, setSearch] = useState("");
const searchSplit = search.toLowerCase().split(" ");
const inv = useInvalidate();
const user_id = useParams().id as string;
// const permissions = useRead("")
const permissions = useUserPermissions(user_id);
const { mutate } = useWrite("UpdateUserPermissionsOnTarget", {
onSuccess: () => inv(["ListUserPermissions"]),
});
return (
<Section title="Permissions">
<DataTable data={[]} columns={[]} />
<Section
title="Permissions"
actions={
<div className="flex gap-6 items-center">
<div
className="flex gap-3 items-center"
onClick={() => setShowNone(!showNone)}
>
<Label htmlFor="show-none">Show All Resources</Label>
<Switch id="show-none" checked={showNone} />
</div>
<Input
placeholder="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-48"
/>
</div>
}
>
<DataTable
data={
permissions
?.filter((permission) =>
showNone ? true : permission.level !== Types.PermissionLevel.None
)
.filter((permission) =>
searchSplit.every(
(search) =>
permission.name.toLowerCase().includes(search) ||
permission.target.type.toLowerCase().includes(search)
)
) ?? []
}
columns={[
{
header: "Resource",
accessorKey: "target.type",
},
{
header: "Target",
cell: ({
row: {
original: { target },
},
}) => {
return (
<ResourceLink
type={target.type as UsableResource}
id={target.id}
/>
);
},
},
{
header: "Level",
cell: ({ row: { original: permission } }) => (
<Select
value={permission.level}
onValueChange={(value) =>
mutate({
...permission,
user_id,
permission: value as Types.PermissionLevel,
})
}
>
<SelectTrigger className="w-32 capitalize">
<SelectValue />
</SelectTrigger>
<SelectContent className="w-32">
{Object.keys(Types.PermissionLevel).map((permission) => (
<SelectItem
value={permission}
key={permission}
className="capitalize"
>
{permission}
</SelectItem>
))}
</SelectContent>
</Select>
),
},
]}
/>
</Section>
);
}
};
// const CreatePermission = ({ user_id }: { user_id: string }) => {
// const inv = useInvalidate();
// const { mutate } = useWrite("UpdateUserPermissionsOnTarget", {
// onSuccess: () => inv(["ListUserPermissions", { user_id }]),
// });
// const [type, setType] = useState<UsableResource>("Server");
// const [id, setId] = useState("");
// const [permission, setPermission] = useState(Types.PermissionLevel.Read);
// return (
// <NewResource
// entityType="Permission"
// onSuccess={async () =>
// mutate({ user_id, target: { type, id }, permission })
// }
// enabled
// >
// <div className="flex items-center justify-between">
// Target Type{" "}
// <Select
// value={type}
// onValueChange={(value) => setType(value as UsableResource)}
// >
// <SelectTrigger className="w-32 capitalize">
// <SelectValue />
// </SelectTrigger>
// <SelectContent className="w-32">
// {RESOURCE_TARGETS.map((type) => (
// <SelectItem value={type} key={type} className="capitalize">
// {type}
// </SelectItem>
// ))}
// </SelectContent>
// </Select>
// </div>
// <div className="flex items-center justify-between">
// Target <ResourceSelector type={type} selected={id} onSelect={setId} />
// </div>
// <div className="flex items-center justify-between">
// Level{" "}
// <Select
// value={permission}
// onValueChange={(value) =>
// setPermission(value as Types.PermissionLevel)
// }
// >
// <SelectTrigger className="w-32 capitalize">
// <SelectValue />
// </SelectTrigger>
// <SelectContent className="w-32">
// {Object.keys(Types.PermissionLevel).map((permission) => (
// <SelectItem
// value={permission}
// key={permission}
// className="capitalize"
// >
// {permission}
// </SelectItem>
// ))}
// </SelectContent>
// </Select>
// </div>
// </NewResource>
// );
// };