From d02df02bbeef87276de5fdc70ea8fd9301bbb351 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 30 Mar 2024 18:04:45 -0700 Subject: [PATCH] manage users --- frontend/src/components/config/util.tsx | 10 +- frontend/src/components/layouts.tsx | 56 +--- .../components/resources/alerter/index.tsx | 2 +- .../src/components/resources/build/index.tsx | 2 +- .../components/resources/builder/index.tsx | 2 +- .../components/resources/deployment/index.tsx | 7 +- .../components/resources/procedure/index.tsx | 2 +- .../src/components/resources/server/index.tsx | 2 +- frontend/src/components/topbar.tsx | 216 +++++++++++++ frontend/src/components/util.tsx | 211 ++---------- frontend/src/lib/hooks.ts | 23 +- frontend/src/pages/users.tsx | 304 +++++++++++++++++- 12 files changed, 583 insertions(+), 254 deletions(-) create mode 100644 frontend/src/components/topbar.tsx diff --git a/frontend/src/components/config/util.tsx b/frontend/src/components/config/util.tsx index 53bbd036a..2db6346c7 100644 --- a/frontend/src/components/config/util.tsx +++ b/frontend/src/components/config/util.tsx @@ -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 ( - @@ -199,7 +201,7 @@ export const ResourceSelector = ({ {resources.map((resource) => ( onSelect(resource.id)} + onSelect={() => onSelect && onSelect(resource.id)} className="flex items-center justify-between" >
{resource.name}
diff --git a/frontend/src/components/layouts.tsx b/frontend/src/components/layouts.tsx index 009fd5660..c7c5da39b 100644 --- a/frontend/src/components/layouts.tsx +++ b/frontend/src/components/layouts.tsx @@ -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 ( <> -
-
-
- - MONITOR - -
- - {type && } -
-
-
- -
- - - - {/* */} - -
-
-
-
+ ); @@ -103,10 +74,12 @@ interface SectionProps { export const Section = ({ title, icon, actions, children }: SectionProps) => (
- {(title || icon) &&
- {icon} -

{title}

-
} + {(title || icon) && ( +
+ {icon} +

{title}

+
+ )} {actions}
{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; @@ -129,13 +102,16 @@ export const NewResource = ({ return ( - - New {type} + New {entityType}
{children}
diff --git a/frontend/src/components/resources/alerter/index.tsx b/frontend/src/components/resources/alerter/index.tsx index 7bdfecef5..139ae5453 100644 --- a/frontend/src/components/resources/alerter/index.tsx +++ b/frontend/src/components/resources/alerter/index.tsx @@ -29,7 +29,7 @@ const NewAlerter = () => { return ( !!type && mutateAsync({ name, config: { type, params: {} } }) } diff --git a/frontend/src/components/resources/build/index.tsx b/frontend/src/components/resources/build/index.tsx index 4ea5ae715..8e244cabb 100644 --- a/frontend/src/components/resources/build/index.tsx +++ b/frontend/src/components/resources/build/index.tsx @@ -22,7 +22,7 @@ const NewBuild = () => { const [name, setName] = useState(""); return ( mutateAsync({ name, config: {} })} enabled={!!name} > diff --git a/frontend/src/components/resources/builder/index.tsx b/frontend/src/components/resources/builder/index.tsx index 8c193ed2d..29fab125c 100644 --- a/frontend/src/components/resources/builder/index.tsx +++ b/frontend/src/components/resources/builder/index.tsx @@ -29,7 +29,7 @@ const NewBuilder = () => { return ( !!type && mutateAsync({ name, config: { type, params: {} } }) } diff --git a/frontend/src/components/resources/deployment/index.tsx b/frontend/src/components/resources/deployment/index.tsx index ee9391e92..28ff51bed 100644 --- a/frontend/src/components/resources/deployment/index.tsx +++ b/frontend/src/components/resources/deployment/index.tsx @@ -101,10 +101,7 @@ export const DeploymentTable = ({ cell: ({ row }) => { const id = row.original.id; return ( - + + ); +}; + +const ResourceTypeDropdown = () => { + const type = useResourceParamType(); + const Components = ResourceComponents[type]; + + const [icon, title] = type + ? [, type + "s"] + : location.pathname === "/tree" + ? [, "Tree"] + : location.pathname === "/keys" + ? [, "Api Keys"] + : location.pathname === "/tags" + ? [, "Tags"] + : location.pathname === "/users" + ? [, "Users"] + : [, "Dashboard"]; + + return ( + + + + + + + + + + Dashboard + + + + + + + + + Resources + + + + + + Tree + + + + + + {RESOURCE_TARGETS.map((rt) => { + const RTIcon = ResourceComponents[rt].Icon; + return ( + + + + {rt}s + + + ); + })} + + + + + + + Tags + + + + + + + + + Api Keys + + + + + + Users + + + + + + ); +}; + +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 ( + + + + + + + + + + All {type}s + + + + + {!list?.length && ( + No {type}s Found. + )} + + {list?.map(({ id, name }) => ( + + + + {name} + + + ))} + + + + ); +}; diff --git a/frontend/src/components/util.tsx b/frontend/src/components/util.tsx index 93c6d88ba..c90b6c08b 100644 --- a/frontend/src/components/util.tsx +++ b/frontend/src/components/util.tsx @@ -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 ( - - ); -}; - 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 - ? [, type + "s"] - : location.pathname === "/tree" - ? [, "Tree"] - : location.pathname === "/keys" - ? [, "Api Keys"] - : location.pathname === "/tags" - ? [, "Tags"] - : location.pathname === "/users" - ? [, "Users"] - : [, "Dashboard"]; - - return ( - - - - - - - - - - Dashboard - - - - - - - - - Resources - - - - - - Tree - - - - - - {RESOURCE_TARGETS.map((rt) => { - const RTIcon = ResourceComponents[rt].Icon; - return ( - - - - {rt}s - - - ); - })} - - - - - - - Tags - - - - - - - - - Api Keys - - - - - - Users - - - - - - ); -}; - -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 ( - - - - - - - - - - All {type}s - - - - - {!list?.length && ( - No {type}s Found. - )} - - {list?.map(({ id, name }) => ( - - - - {name} - - - ))} - - - - ); -}; - export const Logout = () => ( + + ); +}; diff --git a/frontend/src/lib/hooks.ts b/frontend/src/lib/hooks.ts index 42ef8aef2..4b016f511 100644 --- a/frontend/src/lib/hooks.ts +++ b/frontend/src/lib/hooks.ts @@ -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"], diff --git a/frontend/src/pages/users.tsx b/frontend/src/pages/users.tsx index 2d7753e1c..e728fe674 100644 --- a/frontend/src/pages/users.tsx +++ b/frontend/src/pages/users.tsx @@ -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 && ( + {user?.username}{" "} + {avatar && } +
+ } subtitle={
@@ -98,19 +115,292 @@ export const UserPage = () => { ) } > - + {!user.admin && } ) ); }; +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 ( -
- +
+
setShowNone(!showNone)} + > + + +
+ setSearch(e.target.value)} + className="w-48" + /> +
+ } + > + + 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 ( + + ); + }, + }, + { + header: "Level", + cell: ({ row: { original: permission } }) => ( + + ), + }, + ]} + /> ); -} \ No newline at end of file +}; + +// const CreatePermission = ({ user_id }: { user_id: string }) => { +// const inv = useInvalidate(); +// const { mutate } = useWrite("UpdateUserPermissionsOnTarget", { +// onSuccess: () => inv(["ListUserPermissions", { user_id }]), +// }); +// const [type, setType] = useState("Server"); +// const [id, setId] = useState(""); +// const [permission, setPermission] = useState(Types.PermissionLevel.Read); +// return ( +// +// mutate({ user_id, target: { type, id }, permission }) +// } +// enabled +// > +//
+// Target Type{" "} +// +//
+//
+// Target +//
+//
+// Level{" "} +// +//
+//
+// ); +// };