mirror of
https://github.com/moghtech/komodo.git
synced 2026-07-19 18:19:56 -05:00
manage users
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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>
|
||||
// );
|
||||
// };
|
||||
|
||||
Reference in New Issue
Block a user