mirror of
https://github.com/moghtech/komodo.git
synced 2026-10-28 16:00:38 +00:00
manage user groups
This commit is contained in:
@@ -16,7 +16,7 @@ export type ReadResponses = {
|
||||
ListUsers: Types.ListUsersResponse;
|
||||
GetUsername: Types.GetUsernameResponse;
|
||||
ListApiKeys: Types.ListApiKeysResponse;
|
||||
ListApiKeysForServiceUser: Types.ListApiKeysForServiceUser;
|
||||
ListApiKeysForServiceUser: Types.ListApiKeysForServiceUserResponse;
|
||||
ListPermissions: Types.ListPermissionsResponse;
|
||||
GetPermissionLevel: Types.GetPermissionLevelResponse;
|
||||
ListUserTargetPermissions: Types.ListUserTargetPermissionsResponse;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CopyButton } from "@components/util";
|
||||
import { Types } from "@monitor/client";
|
||||
import { DataTable } from "@ui/data-table";
|
||||
import { Input } from "@ui/input";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const ONE_DAY_MS = 1000 * 60 * 60 * 24;
|
||||
|
||||
export const KeysTable = ({
|
||||
keys,
|
||||
DeleteKey,
|
||||
}: {
|
||||
keys: Types.ApiKey[];
|
||||
DeleteKey: (params: { api_key: string }) => ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="api-keys"
|
||||
data={keys}
|
||||
columns={[
|
||||
{ header: "Name", accessorKey: "name" },
|
||||
{
|
||||
header: "Key",
|
||||
cell: ({
|
||||
row: {
|
||||
original: { key },
|
||||
},
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input className="w-40" value={key} disabled />
|
||||
<CopyButton content={key} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Expires",
|
||||
accessorFn: ({ expires }) =>
|
||||
expires
|
||||
? "In " +
|
||||
((expires - Date.now()) / ONE_DAY_MS).toFixed() +
|
||||
" Days"
|
||||
: "Never",
|
||||
},
|
||||
{
|
||||
header: "Delete",
|
||||
cell: ({ row }) => <DeleteKey api_key={row.original.key} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
Boxes,
|
||||
FileQuestion,
|
||||
FolderTree,
|
||||
Group,
|
||||
Home,
|
||||
Key,
|
||||
SearchX,
|
||||
Tag,
|
||||
UserCircle2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -87,7 +87,7 @@ const PrimaryDropdown = () => {
|
||||
: location.pathname === "/alerts"
|
||||
? [<AlertTriangle className="w-4 h-4" />, "Alerts"]
|
||||
: location.pathname.split("/")[1] === "user-groups"
|
||||
? [<Group className="w-4 h-4" />, "User Groups"]
|
||||
? [<Users className="w-4 h-4" />, "User Groups"]
|
||||
: location.pathname === "/users" ||
|
||||
location.pathname.split("/")[1] === "users"
|
||||
? [<UserCircle2 className="w-4 h-4" />, "Users"]
|
||||
@@ -309,7 +309,7 @@ const UserGroupDropdown = ({ group_id }: { group_id: string | undefined }) => {
|
||||
variant="ghost"
|
||||
className="flex justify-start items-center gap-2 w-48 px-3"
|
||||
>
|
||||
<Group className="w-4 h-4" />
|
||||
<Users className="w-4 h-4" />
|
||||
{selected ? selected.name : "All User Groups"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -351,7 +351,7 @@ const UserGroupDropdown = ({ group_id }: { group_id: string | undefined }) => {
|
||||
variant="link"
|
||||
className="flex gap-2 items-center p-0"
|
||||
>
|
||||
<Group className="w-4 h-4" />
|
||||
<Users className="w-4 h-4" />
|
||||
{group.name}
|
||||
</Button>
|
||||
</CommandItem>
|
||||
@@ -379,7 +379,10 @@ const UsersDropdown = ({ user_id }: { user_id: string | undefined }) => {
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" className="flex justify-start items-center gap-2 w-48 px-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex justify-start items-center gap-2 w-48 px-3"
|
||||
>
|
||||
<UserAvatar avatar={avatar} />
|
||||
{selected ? selected.username : "All Users"}
|
||||
</Button>
|
||||
|
||||
@@ -57,7 +57,7 @@ export const CreateKeyForServiceUser = ({ user_id }: { user_id: string }) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="items-center gap-2">
|
||||
<Button variant="secondary" className="items-center gap-2">
|
||||
New Api Key <PlusCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { text_color_class_by_intention } from "@lib/color";
|
||||
import { Types } from "@monitor/client";
|
||||
import { DataTable } from "@ui/data-table";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { MinusCircle } from "lucide-react";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
|
||||
export const UserTable = ({
|
||||
users,
|
||||
onUserRemove,
|
||||
}: {
|
||||
users: Types.User[];
|
||||
onUserRemove?: (user_id: string) => void;
|
||||
}) => {
|
||||
const nav = useNavigate();
|
||||
const columns: ColumnDef<Types.User, "User" | "Admin">[] = [
|
||||
{ header: "Username", accessorKey: "username" },
|
||||
{ header: "Type", accessorKey: "config.type" },
|
||||
{
|
||||
header: "Level",
|
||||
accessorFn: (user) => (user.admin ? "Admin" : "User"),
|
||||
},
|
||||
{
|
||||
header: "Enabled",
|
||||
cell: ({ row }) => {
|
||||
const enabledClass = row.original.enabled
|
||||
? text_color_class_by_intention("Good")
|
||||
: text_color_class_by_intention("Critical");
|
||||
return (
|
||||
<div className={enabledClass}>
|
||||
{row.original.enabled ? "Enabled" : "Disabled"}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
if (onUserRemove) {
|
||||
columns.push({
|
||||
header: "Remove",
|
||||
cell: ({ row }) => (
|
||||
<ConfirmButton
|
||||
title="Remove"
|
||||
variant="destructive"
|
||||
icon={<MinusCircle className="w-4 h-4" />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUserRemove(row.original._id?.$oid!);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
return (
|
||||
<DataTable
|
||||
tableKey="users"
|
||||
data={users}
|
||||
columns={columns}
|
||||
onRowClick={(user) => nav(`/users/${user._id!.$oid}`)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -220,10 +220,14 @@ export const ConfirmButton = ({
|
||||
onClick={
|
||||
confirmed
|
||||
? (e) => {
|
||||
e.stopPropagation();
|
||||
onClick && onClick(e);
|
||||
set(false);
|
||||
}
|
||||
: () => set(true)
|
||||
: (e) => {
|
||||
e.stopPropagation();
|
||||
set(true);
|
||||
}
|
||||
}
|
||||
onBlur={() => set(false)}
|
||||
loading={loading}
|
||||
|
||||
@@ -21,47 +21,17 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/dropdown-menu";
|
||||
import { DataTable } from "@ui/data-table";
|
||||
import { KeysTable } from "@components/keys/table";
|
||||
|
||||
export const Keys = () => {
|
||||
useSetTitle("Api Keys");
|
||||
const keys = useRead("ListApiKeys", {}).data ?? [];
|
||||
return (
|
||||
<Page title="Api Keys" actions={<CreateKey />}>
|
||||
<DataTable
|
||||
tableKey="api-keys"
|
||||
data={keys}
|
||||
columns={[
|
||||
{ header: "Name", accessorKey: "name" },
|
||||
{
|
||||
header: "Key",
|
||||
cell: ({
|
||||
row: {
|
||||
original: { key },
|
||||
},
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input className="w-40" value={key} disabled />
|
||||
<CopyButton content={key} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Expires",
|
||||
accessorFn: ({ expires }) =>
|
||||
expires
|
||||
? "In " +
|
||||
((expires - Date.now()) / ONE_DAY_MS).toFixed() +
|
||||
" Days"
|
||||
: "Never",
|
||||
},
|
||||
{
|
||||
header: "Delete",
|
||||
cell: ({ row }) => <DeleteKey api_key={row.original.key} />,
|
||||
},
|
||||
]}
|
||||
<KeysTable
|
||||
keys={keys}
|
||||
DeleteKey={DeleteKey}
|
||||
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
@@ -102,7 +72,7 @@ const CreateKey = () => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="items-center gap-2">
|
||||
<Button variant="secondary" className="items-center gap-2">
|
||||
New Api Key <PlusCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Page, Section } from "@components/layouts";
|
||||
import { PermissionsTable } from "@components/users/permissions-table";
|
||||
import { UserTable } from "@components/users/table";
|
||||
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@ui/command";
|
||||
import { Input } from "@ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { PlusCircle, Save, SearchX, UserCircle2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export const UserGroupPage = () => {
|
||||
const { toast } = useToast();
|
||||
const inv = useInvalidate();
|
||||
const group_id = useParams().id as string;
|
||||
const group = useRead("ListUserGroups", {}).data?.find(
|
||||
(group) => group._id?.$oid === group_id
|
||||
);
|
||||
const users = useRead("ListUsers", {}).data;
|
||||
const [name, setName] = useState<string>();
|
||||
useEffect(() => {
|
||||
if (group) setName(group.name);
|
||||
}, [group?.name]);
|
||||
const renameMutate = useWrite("RenameUserGroup", {
|
||||
onSuccess: () => {
|
||||
inv(["ListUserGroups"]);
|
||||
toast({ title: "Renamed User Group" });
|
||||
},
|
||||
}).mutate;
|
||||
const rename = () => {
|
||||
if (!name) {
|
||||
toast({ title: "New name cannot be empty" });
|
||||
if (group) setName(group.name);
|
||||
return;
|
||||
}
|
||||
renameMutate({ id: group_id, name });
|
||||
};
|
||||
const removeMutate = useWrite("RemoveUserFromUserGroup", {
|
||||
onSuccess: () => {
|
||||
inv(["ListUserGroups"]);
|
||||
toast({ title: "Removed User from User Group" });
|
||||
},
|
||||
}).mutate;
|
||||
if (!group) return null;
|
||||
return (
|
||||
<Page
|
||||
title={
|
||||
<div className="flex gap-4 items-center">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") rename();
|
||||
}}
|
||||
className="text-3xl h-fit p-2"
|
||||
/>
|
||||
{name !== group.name && (
|
||||
<Button size="icon" onClick={rename}>
|
||||
<Save className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Section
|
||||
title="Users"
|
||||
icon={<UserCircle2 className="w-4 h-4" />}
|
||||
actions={<AddUserToGroup group_id={group_id} />}
|
||||
>
|
||||
<UserTable
|
||||
users={
|
||||
users?.filter((user) =>
|
||||
group ? group.users.includes(user._id?.$oid!) : false
|
||||
) ?? []
|
||||
}
|
||||
onUserRemove={(user_id) =>
|
||||
removeMutate({ user_group: group_id, user_id })
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
<PermissionsTable user_target={{ type: "UserGroup", id: group_id }} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const AddUserToGroup = ({ group_id }: { group_id: string }) => {
|
||||
const inv = useInvalidate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
const group = useRead("ListUserGroups", {}).data?.find(
|
||||
(group) => group._id?.$oid === group_id
|
||||
);
|
||||
|
||||
const users = useRead("ListUsers", {}).data?.filter(
|
||||
(user) =>
|
||||
// Only show users not already in group
|
||||
!group?.users.includes(user._id?.$oid!)
|
||||
);
|
||||
|
||||
const addUser = useWrite("AddUserToUserGroup", {
|
||||
onSuccess: () => {
|
||||
inv(["ListUserGroups"]);
|
||||
toast({ title: "Added User to User Group" });
|
||||
},
|
||||
}).mutate;
|
||||
|
||||
if (!users || users.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex justify-start items-center gap-2 w-48 px-3"
|
||||
>
|
||||
<PlusCircle className="w-4 h-4" />
|
||||
Add User
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] max-h-[400px] p-0" sideOffset={12}>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search Users"
|
||||
className="h-9"
|
||||
value={input}
|
||||
onValueChange={setInput}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="flex justify-evenly items-center">
|
||||
No Users Found
|
||||
<SearchX className="w-3 h-3" />
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{users?.map((user) => (
|
||||
<CommandItem
|
||||
key={user.username}
|
||||
onSelect={() => {
|
||||
setOpen(false);
|
||||
addUser({ user_group: group_id, user_id: user._id?.$oid! });
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex gap-2 items-center p-0"
|
||||
>
|
||||
<UserAvatar avatar={(user.config.data as any).avatar} />
|
||||
{user.username}
|
||||
</Button>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const UserAvatar = ({ avatar }: { avatar: string | undefined }) =>
|
||||
avatar ? (
|
||||
<img src={avatar} alt="Avatar" className="w-4" />
|
||||
) : (
|
||||
<UserCircle2 className="w-4" />
|
||||
);
|
||||
@@ -0,0 +1,105 @@
|
||||
import { KeysTable } from "@components/keys/table";
|
||||
import { Page, Section } from "@components/layouts";
|
||||
import { PermissionsTable } from "@components/users/permissions-table";
|
||||
import {
|
||||
CreateKeyForServiceUser,
|
||||
DeleteKeyForServiceUser,
|
||||
} from "@components/users/service-api-key";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
|
||||
import { Label } from "@ui/label";
|
||||
import { Switch } from "@ui/switch";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { Key, UserCheck, UserMinus } from "lucide-react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export const UserPage = () => {
|
||||
const { toast } = useToast();
|
||||
const inv = useInvalidate();
|
||||
const user_id = useParams().id as string;
|
||||
const user = useRead("ListUsers", {}).data?.find(
|
||||
(user) => user._id?.$oid === user_id
|
||||
);
|
||||
const { mutate } = useWrite("UpdateUserBasePermissions", {
|
||||
onSuccess: () => inv(["ListUsers"]),
|
||||
onError: (e) => {
|
||||
console.log(e);
|
||||
toast({ title: "Failed to update user permissions" });
|
||||
},
|
||||
});
|
||||
const enabledClass = user?.enabled ? "text-green-500" : "text-red-500";
|
||||
const avatar = (user?.config.data as any)?.avatar as string | undefined;
|
||||
if (!user) return null;
|
||||
return (
|
||||
<Page
|
||||
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}>
|
||||
{user?.enabled ? "Enabled" : "Disabled"}
|
||||
</div>
|
||||
|<div className="">Level: {user?.admin ? "Admin" : "User"}</div>|
|
||||
<div className="">Type: {user?.config.type}</div>
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
!user.admin && (
|
||||
<div className="flex gap-4 items-center">
|
||||
{(["Server", "Build"] as Array<"Server" | "Build">).map((item) => {
|
||||
const key = `create_${item.toLowerCase()}_permissions` as
|
||||
| "create_server_permissions"
|
||||
| "create_build_permissions";
|
||||
const req_key = `create_${item.toLowerCase()}s`;
|
||||
return (
|
||||
<div key={key} className="flex gap-2 items-center">
|
||||
<Label htmlFor={key}>Create {item}</Label>
|
||||
<Switch
|
||||
id={key}
|
||||
className="flex gap-4"
|
||||
checked={user[key]}
|
||||
onClick={() => mutate({ user_id, [req_key]: !user[key] })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<ConfirmButton
|
||||
title={user.enabled ? "Disable User" : "Enable User"}
|
||||
icon={
|
||||
user.enabled ? (
|
||||
<UserMinus className="w-4 h-4" />
|
||||
) : (
|
||||
<UserCheck className="w-4 h-4" />
|
||||
)
|
||||
}
|
||||
variant={user.enabled ? "destructive" : "outline"}
|
||||
onClick={() => mutate({ user_id, enabled: !user.enabled })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{user.config.type === "Service" && <ApiKeysTable user_id={user_id} />}
|
||||
{!user.admin && (
|
||||
<PermissionsTable user_target={{ type: "User", id: user_id }} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const ApiKeysTable = ({ user_id }: { user_id: string }) => {
|
||||
const keys = useRead("ListApiKeysForServiceUser", { user_id }).data;
|
||||
return (
|
||||
<Section
|
||||
title="Api Keys"
|
||||
icon={<Key className="w-4 h-4" />}
|
||||
actions={<CreateKeyForServiceUser user_id={user_id} />}
|
||||
>
|
||||
<KeysTable keys={keys ?? []} DeleteKey={DeleteKeyForServiceUser} />
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+17
-166
@@ -1,18 +1,12 @@
|
||||
import { Page, Section } from "@components/layouts";
|
||||
import { NewServiceUser, NewUserGroup } from "@components/users/new";
|
||||
import { PermissionsTable } from "@components/users/permissions-table";
|
||||
import { ConfirmButton } from "@components/util";
|
||||
import { text_color_class_by_intention } from "@lib/color";
|
||||
import { useInvalidate, useRead, useSetTitle, useWrite } from "@lib/hooks";
|
||||
import { Button } from "@ui/button";
|
||||
import { UserTable } from "@components/users/table";
|
||||
import { useRead, useSetTitle } from "@lib/hooks";
|
||||
import { DataTable } from "@ui/data-table";
|
||||
import { Input } from "@ui/input";
|
||||
import { Label } from "@ui/label";
|
||||
import { Switch } from "@ui/switch";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { Save, UserCheck, UserMinus } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { UserCircle2, Users } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const UsersPage = () => {
|
||||
useSetTitle("Users");
|
||||
@@ -33,7 +27,11 @@ export const UsersPage = () => {
|
||||
}
|
||||
>
|
||||
{/* User Groups */}
|
||||
<Section title="User Groups" actions={<NewUserGroup />}>
|
||||
<Section
|
||||
title="User Groups"
|
||||
icon={<Users className="w-4 h-4" />}
|
||||
actions={<NewUserGroup />}
|
||||
>
|
||||
<DataTable
|
||||
tableKey="user-groups"
|
||||
data={
|
||||
@@ -53,166 +51,19 @@ export const UsersPage = () => {
|
||||
</Section>
|
||||
|
||||
{/* Users */}
|
||||
<Section title="Users" actions={<NewServiceUser />}>
|
||||
<DataTable
|
||||
tableKey="users"
|
||||
data={
|
||||
<Section
|
||||
title="Users"
|
||||
icon={<UserCircle2 className="w-4 h-4" />}
|
||||
actions={<NewServiceUser />}
|
||||
>
|
||||
<UserTable
|
||||
users={
|
||||
users?.filter((user) =>
|
||||
searchSplit.every((term) => user.username.includes(term))
|
||||
) ?? []
|
||||
}
|
||||
columns={[
|
||||
{ header: "Username", accessorKey: "username" },
|
||||
{ header: "Type", accessorKey: "config.type" },
|
||||
{
|
||||
header: "Level",
|
||||
accessorFn: (user) => (user.admin ? "Admin" : "User"),
|
||||
},
|
||||
{
|
||||
header: "Enabled",
|
||||
cell: ({ row }) => {
|
||||
const enabledClass = row.original.enabled
|
||||
? text_color_class_by_intention("Good")
|
||||
: text_color_class_by_intention("Critical");
|
||||
return (
|
||||
<div className={enabledClass}>
|
||||
{row.original.enabled ? "Enabled" : "Disabled"}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
onRowClick={(user) => nav(`/users/${user._id!.$oid}`)}
|
||||
/>
|
||||
</Section>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserGroupPage = () => {
|
||||
const { toast } = useToast();
|
||||
const inv = useInvalidate();
|
||||
const group_id = useParams().id as string;
|
||||
const group = useRead("ListUserGroups", {}).data?.find(
|
||||
(group) => group._id?.$oid === group_id
|
||||
);
|
||||
const [name, setName] = useState<string>();
|
||||
useEffect(() => {
|
||||
if (group) setName(group.name);
|
||||
}, [group?.name]);
|
||||
const renameMutate = useWrite("RenameUserGroup", {
|
||||
onSuccess: () => {
|
||||
inv(["ListUserGroups"]);
|
||||
toast({ title: "Renamed User Group" });
|
||||
},
|
||||
}).mutate;
|
||||
const rename = () => {
|
||||
if (!name) {
|
||||
toast({ title: "New name cannot be empty" });
|
||||
if (group) setName(group.name);
|
||||
return;
|
||||
}
|
||||
renameMutate({ id: group_id, name });
|
||||
};
|
||||
if (!group) return null;
|
||||
return (
|
||||
<Page
|
||||
title={
|
||||
<div className="flex gap-4 items-center">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") rename();
|
||||
}}
|
||||
className="text-3xl h-fit p-2"
|
||||
/>
|
||||
{name !== group.name && (
|
||||
<Button size="icon" onClick={rename}>
|
||||
<Save className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PermissionsTable user_target={{ type: "UserGroup", id: group_id }} />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserPage = () => {
|
||||
const { toast } = useToast();
|
||||
const inv = useInvalidate();
|
||||
const user_id = useParams().id as string;
|
||||
const user = useRead("ListUsers", {}).data?.find(
|
||||
(user) => user._id?.$oid === user_id
|
||||
);
|
||||
const { mutate } = useWrite("UpdateUserBasePermissions", {
|
||||
onSuccess: () => inv(["ListUsers"]),
|
||||
onError: (e) => {
|
||||
console.log(e);
|
||||
toast({ title: "Failed to update user permissions" });
|
||||
},
|
||||
});
|
||||
const enabledClass = user?.enabled ? "text-green-500" : "text-red-500";
|
||||
const avatar = (user?.config.data as any)?.avatar as string | undefined;
|
||||
if (!user) return null;
|
||||
return (
|
||||
<Page
|
||||
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}>
|
||||
{user?.enabled ? "Enabled" : "Disabled"}
|
||||
</div>
|
||||
|<div className="">Level: {user?.admin ? "Admin" : "User"}</div>|
|
||||
<div className="">Type: {user?.config.type}</div>
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
!user.admin && (
|
||||
<div className="flex gap-4 items-center">
|
||||
{(["Server", "Build"] as Array<"Server" | "Build">).map((item) => {
|
||||
const key = `create_${item.toLowerCase()}_permissions` as
|
||||
| "create_server_permissions"
|
||||
| "create_build_permissions";
|
||||
const req_key = `create_${item.toLowerCase()}s`;
|
||||
return (
|
||||
<div key={key} className="flex gap-2 items-center">
|
||||
<Label htmlFor={key}>Create {item}</Label>
|
||||
<Switch
|
||||
id={key}
|
||||
className="flex gap-4"
|
||||
checked={user[key]}
|
||||
onClick={() => mutate({ user_id, [req_key]: !user[key] })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<ConfirmButton
|
||||
title={user.enabled ? "Disable User" : "Enable User"}
|
||||
icon={
|
||||
user.enabled ? (
|
||||
<UserMinus className="w-4 h-4" />
|
||||
) : (
|
||||
<UserCheck className="w-4 h-4" />
|
||||
)
|
||||
}
|
||||
variant={user.enabled ? "destructive" : "outline"}
|
||||
onClick={() => mutate({ user_id, enabled: !user.enabled })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{!user.admin && (
|
||||
<PermissionsTable user_target={{ type: "User", id: user_id }} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,12 +8,14 @@ import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
import { Tree } from "@pages/home/tree";
|
||||
import { Tags } from "@pages/tags";
|
||||
import { ResourceUpdates } from "@pages/resource_update";
|
||||
import { UserGroupPage, UserPage, UsersPage } from "@pages/users";
|
||||
import { UsersPage } from "@pages/users";
|
||||
import { AllResources } from "@pages/home/all_resources";
|
||||
import { UserDisabled } from "@pages/user_disabled";
|
||||
import { Home } from "@pages/home";
|
||||
import { ResourceStats } from "@pages/resource_stats";
|
||||
import { Alerts } from "@pages/alerts";
|
||||
import { UserPage } from "@pages/user";
|
||||
import { UserGroupPage } from "@pages/user-group";
|
||||
|
||||
const ROUTER = createBrowserRouter([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user