fix permissioning endpoint

This commit is contained in:
mbecker20
2024-04-20 17:37:22 -07:00
parent 01ea85e627
commit a7a7d0552b
16 changed files with 252 additions and 108 deletions

View File

@@ -20,8 +20,8 @@ use super::{update::ResourceTarget, MongoId};
#[unique_doc_index(doc! { #[unique_doc_index(doc! {
"user_target.type": 1, "user_target.type": 1,
"user_target.id": 1, "user_target.id": 1,
"target.type": 1, "resource_target.type": 1,
"target.id": 1 "resource_target.id": 1
})] })]
pub struct Permission { pub struct Permission {
/// The id of the permission document /// The id of the permission document

View File

@@ -1,24 +0,0 @@
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { useRead } from "@lib/hooks";
import { Link } from "react-router-dom";
import { Key } from "lucide-react";
export const ApiKeysSummary = () => {
const keys_count = useRead("ListApiKeys", {}).data?.length;
return (
<Link to="/keys" className="w-full">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Api Keys</CardTitle>
<CardDescription>{keys_count} Total</CardDescription>
</div>
<Key className="w-4 h-4" />
</div>
</CardHeader>
</Card>
</Link>
);
};

View File

@@ -1,24 +0,0 @@
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { useRead } from "@lib/hooks";
import { Link } from "react-router-dom";
import { Tag } from "lucide-react";
export const TagsSummary = () => {
const tags_count = useRead("ListTags", {}).data?.length;
return (
<Link to="/tags" className="w-full">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Tags</CardTitle>
<CardDescription>{tags_count} Total</CardDescription>
</div>
<Tag className="w-4 h-4" />
</div>
</CardHeader>
</Card>
</Link>
);
};

View File

@@ -1,4 +1,4 @@
import { useRead } from "@lib/hooks"; import { useRead, useUser } from "@lib/hooks";
import { Button } from "@ui/button"; import { Button } from "@ui/button";
import { import {
CommandDialog, CommandDialog,
@@ -9,7 +9,7 @@ import {
CommandSeparator, CommandSeparator,
CommandItem, CommandItem,
} from "@ui/command"; } from "@ui/command";
import { Home, Search } from "lucide-react"; import { Home, Search, UserCircle2 } from "lucide-react";
import { useState, useEffect, Fragment } from "react"; import { useState, useEffect, Fragment } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ResourceComponents } from "./resources"; import { ResourceComponents } from "./resources";
@@ -21,6 +21,8 @@ import { ServerComponents } from "./resources/server";
import { ProcedureComponents } from "./resources/procedure"; import { ProcedureComponents } from "./resources/procedure";
export const Omnibar = () => { export const Omnibar = () => {
const user = useUser().data;
const [open, set] = useState(false); const [open, set] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const nav = (value: string) => { const nav = (value: string) => {
@@ -68,6 +70,13 @@ export const Omnibar = () => {
<Home className="w-4 h-4" /> <Home className="w-4 h-4" />
Home Home
</CommandItem> </CommandItem>
<CommandItem
className="flex items-center gap-2 cursor-pointer"
onSelect={() => nav("/servers")}
>
<ServerComponents.Icon />
Servers
</CommandItem>
<CommandItem <CommandItem
className="flex items-center gap-2 cursor-pointer" className="flex items-center gap-2 cursor-pointer"
onSelect={() => nav("/deployments")} onSelect={() => nav("/deployments")}
@@ -82,13 +91,6 @@ export const Omnibar = () => {
<BuildComponents.Icon /> <BuildComponents.Icon />
Builds Builds
</CommandItem> </CommandItem>
<CommandItem
className="flex items-center gap-2 cursor-pointer"
onSelect={() => nav("/servers")}
>
<ServerComponents.Icon />
Servers
</CommandItem>
<CommandItem <CommandItem
className="flex items-center gap-2 cursor-pointer" className="flex items-center gap-2 cursor-pointer"
onSelect={() => nav("/procedures")} onSelect={() => nav("/procedures")}
@@ -96,6 +98,15 @@ export const Omnibar = () => {
<ProcedureComponents.Icon /> <ProcedureComponents.Icon />
Procedures Procedures
</CommandItem> </CommandItem>
{user?.admin && (
<CommandItem
className="flex items-center gap-2 cursor-pointer"
onSelect={() => nav("/users")}
>
<UserCircle2 className="w-4 h-4" />
Users
</CommandItem>
)}
</CommandGroup> </CommandGroup>
<CommandSeparator /> <CommandSeparator />

View File

@@ -131,6 +131,7 @@ export const AlerterComponents: RequiredResourceComponents = {
}, },
Name: ({ id }: { id: string }) => <>{useAlerter(id)?.name}</>, Name: ({ id }: { id: string }) => <>{useAlerter(id)?.name}</>,
name: (id) => useAlerter(id)?.name,
Icon: () => <AlarmClock className="w-4 h-4" />, Icon: () => <AlarmClock className="w-4 h-4" />,

View File

@@ -21,6 +21,7 @@ export const BuildComponents: RequiredResourceComponents = {
Table: BuildTable, Table: BuildTable,
Name: ({ id }) => <>{useBuild(id)?.name}</>, Name: ({ id }) => <>{useBuild(id)?.name}</>,
name: (id) => useBuild(id)?.name,
Icon: ({ id }) => { Icon: ({ id }) => {
if (id) return <IconStrictId id={id} />; if (id) return <IconStrictId id={id} />;

View File

@@ -125,6 +125,7 @@ export const BuilderComponents: RequiredResourceComponents = {
}, },
Name: ({ id }: { id: string }) => <>{useBuilder(id)?.name}</>, Name: ({ id }: { id: string }) => <>{useBuilder(id)?.name}</>,
name: (id) => useBuilder(id)?.name,
Icon: () => <Factory className="w-4 h-4" />, Icon: () => <Factory className="w-4 h-4" />,

View File

@@ -40,6 +40,7 @@ export const DeploymentComponents: RequiredResourceComponents = {
}, },
Name: ({ id }) => <>{useDeployment(id)?.name}</>, Name: ({ id }) => <>{useDeployment(id)?.name}</>,
name: (id) => useDeployment(id)?.name,
Icon: ({ id }) => { Icon: ({ id }) => {
const state = useDeployment(id)?.info.state; const state = useDeployment(id)?.info.state;

View File

@@ -36,6 +36,7 @@ export const ProcedureComponents: RequiredResourceComponents = {
Table: ProcedureTable, Table: ProcedureTable,
Name: ({ id }) => <>{useProcedure(id)?.name}</>, Name: ({ id }) => <>{useProcedure(id)?.name}</>,
name: (id) => useProcedure(id)?.name,
Icon: () => <Route className="w-4" />, Icon: () => <Route className="w-4" />,

View File

@@ -84,6 +84,7 @@ export const RepoComponents: RequiredResourceComponents = {
}, },
Name: ({ id }: { id: string }) => <>{useRepo(id)?.name}</>, Name: ({ id }: { id: string }) => <>{useRepo(id)?.name}</>,
name: (id) => useRepo(id)?.name,
Icon: () => <GitBranch className="w-4 h-4" />, Icon: () => <GitBranch className="w-4 h-4" />,

View File

@@ -38,6 +38,7 @@ export const ServerComponents: RequiredResourceComponents = {
Table: ServerTable, Table: ServerTable,
Name: ({ id }: { id: string }) => <>{useServer(id)?.name}</>, Name: ({ id }: { id: string }) => <>{useServer(id)?.name}</>,
name: (id) => useServer(id)?.name,
Icon: ({ id }) => { Icon: ({ id }) => {
const status = useServer(id)?.info.status; const status = useServer(id)?.info.status;

View File

@@ -74,6 +74,7 @@ export const Topbar = () => {
const PrimaryDropdown = () => { const PrimaryDropdown = () => {
const type = useResourceParamType(); const type = useResourceParamType();
const Components = type && ResourceComponents[type]; const Components = type && ResourceComponents[type];
console.log(location.pathname.split("/"));
const [icon, title] = Components const [icon, title] = Components
? [<Components.Icon />, type + "s"] ? [<Components.Icon />, type + "s"]
@@ -85,7 +86,8 @@ const PrimaryDropdown = () => {
? [<Tag className="w-4 h-4" />, "Tags"] ? [<Tag className="w-4 h-4" />, "Tags"]
: location.pathname === "/alerts" : location.pathname === "/alerts"
? [<AlertTriangle className="w-4 h-4" />, "Alerts"] ? [<AlertTriangle className="w-4 h-4" />, "Alerts"]
: location.pathname === "/users" : location.pathname === "/users" ||
location.pathname.split("/")[1] === "users"
? [<UserCircle2 className="w-4 h-4" />, "Users"] ? [<UserCircle2 className="w-4 h-4" />, "Users"]
: [<FileQuestion className="w-4 h-4" />, "Unknown"]; : [<FileQuestion className="w-4 h-4" />, "Unknown"];
// : [<Box className="w-4 h-4" />, "Dashboard"]; // : [<Box className="w-4 h-4" />, "Dashboard"];
@@ -177,8 +179,8 @@ const SecondaryDropdown = () => {
const type = useResourceParamType(); const type = useResourceParamType();
if (type) return <ResourcesDropdown type={type} />; if (type) return <ResourcesDropdown type={type} />;
if (location.pathname !== "/") return;
if (location.pathname === "/") {
const Icon = ICONS[view]; const Icon = ICONS[view];
return ( return (
@@ -207,6 +209,13 @@ const SecondaryDropdown = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );
}
const [_, base, user_id] = location.pathname.split("/");
if (base === "users") {
return <UsersDropdown user_id={user_id} />;
}
}; };
const ResourcesDropdown = ({ type }: { type: UsableResource }) => { const ResourcesDropdown = ({ type }: { type: UsableResource }) => {
@@ -274,3 +283,83 @@ const ResourcesDropdown = ({ type }: { type: UsableResource }) => {
</Popover> </Popover>
); );
}; };
const UsersDropdown = ({ user_id }: { user_id: string | undefined }) => {
const nav = useNavigate();
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
const users = useRead("GetUsers", {}).data;
const selected = user_id
? users?.find((user) => user._id?.$oid === user_id)
: undefined;
const avatar = (selected?.config.data as { avatar?: string })?.avatar;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" className="justify-between w-[300px] px-3">
<div className="flex items-center gap-2">
<UserAvatar avatar={avatar} />
{selected ? selected.username : "All Users"}
</div>
</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>
<CommandItem
onSelect={() => {
setOpen(false);
nav(`/users`);
}}
>
<Button variant="link" className="flex gap-2 items-center p-0">
<UserCircle2 className="w-4" />
All Users
</Button>
</CommandItem>
{users?.map((user) => (
<CommandItem
key={user.username}
onSelect={() => {
setOpen(false);
nav(`/users/${user._id?.$oid}`);
}}
>
<Button
variant="link"
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" />
);

View File

@@ -1,3 +1,4 @@
import { ResourceComponents } from "@components/resources";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { UsableResource } from "@types"; import { UsableResource } from "@types";
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from "clsx";
@@ -56,3 +57,8 @@ function keep_line(line: string) {
export function version_is_none({ major, minor, patch }: Types.Version) { export function version_is_none({ major, minor, patch }: Types.Version) {
return major === 0 && minor === 0 && patch === 0; return major === 0 && minor === 0 && patch === 0;
} }
export function resource_name(type: UsableResource, id: string) {
const Components = ResourceComponents[type];
return Components.name(id)
}

View File

@@ -1,12 +1,10 @@
import { Page, Section } from "@components/layouts"; import { Page, Section } from "@components/layouts";
import { Box, History } from "lucide-react"; import { Box, History, Key, Tag } from "lucide-react";
import { useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { Card, CardContent } from "@ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { TagsSummary } from "@components/dashboard/tags";
import { ApiKeysSummary } from "@components/dashboard/api-keys";
import { ResourceComponents } from "@components/resources"; import { ResourceComponents } from "@components/resources";
import { OpenAlerts } from "@components/alert"; import { OpenAlerts } from "@components/alert";
import { useUser } from "@lib/hooks"; import { useRead, useUser } from "@lib/hooks";
import { ResourceLink } from "@components/resources/common"; import { ResourceLink } from "@components/resources/common";
import { Fragment } from "react"; import { Fragment } from "react";
@@ -79,3 +77,43 @@ const RecentlyViewed = () => {
</Section> </Section>
); );
}; };
const TagsSummary = () => {
const tags_count = useRead("ListTags", {}).data?.length;
return (
<Link to="/tags" className="w-full">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Tags</CardTitle>
<CardDescription>{tags_count} Total</CardDescription>
</div>
<Tag className="w-4 h-4" />
</div>
</CardHeader>
</Card>
</Link>
);
};
const ApiKeysSummary = () => {
const keys_count = useRead("ListApiKeys", {}).data?.length;
return (
<Link to="/keys" className="w-full">
<Card className="hover:bg-accent/50 transition-colors cursor-pointer">
<CardHeader>
<div className="flex justify-between">
<div>
<CardTitle>Api Keys</CardTitle>
<CardDescription>{keys_count} Total</CardDescription>
</div>
<Key className="w-4 h-4" />
</div>
</CardHeader>
</Card>
</Link>
);
};

View File

@@ -1,11 +1,13 @@
import { Page, Section } from "@components/layouts"; import { Page, Section } from "@components/layouts";
import { ResourceComponents } from "@components/resources";
import { ResourceLink } from "@components/resources/common"; import { ResourceLink } from "@components/resources/common";
import { ConfirmButton } from "@components/util"; import { ConfirmButton } from "@components/util";
import { text_color_class_by_intention } from "@lib/color"; import { text_color_class_by_intention } from "@lib/color";
import { useInvalidate, useRead, useSetTitle, useWrite } from "@lib/hooks"; import { useInvalidate, useRead, useSetTitle, useWrite } from "@lib/hooks";
import { resource_name } from "@lib/utils";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { UsableResource } from "@types"; import { UsableResource } from "@types";
import { DataTable } from "@ui/data-table"; import { DataTable, SortableHeader } from "@ui/data-table";
import { Input } from "@ui/input"; import { Input } from "@ui/input";
import { Label } from "@ui/label"; import { Label } from "@ui/label";
import { import {
@@ -282,6 +284,12 @@ const PermissionsTable = () => {
title="Permissions" title="Permissions"
actions={ actions={
<div className="flex gap-6 items-center"> <div className="flex gap-6 items-center">
<Input
placeholder="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-[300px]"
/>
<div <div
className="flex gap-3 items-center" className="flex gap-3 items-center"
onClick={() => setShowNone(!showNone)} onClick={() => setShowNone(!showNone)}
@@ -289,23 +297,17 @@ const PermissionsTable = () => {
<Label htmlFor="show-none">Show All Resources</Label> <Label htmlFor="show-none">Show All Resources</Label>
<Switch id="show-none" checked={showNone} /> <Switch id="show-none" checked={showNone} />
</div> </div>
<Input
placeholder="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-48"
/>
</div> </div>
} }
> >
<DataTable <DataTable
tableKey="permissions" tableKey="permissions"
data={ data={
permissions permissions?.filter(
?.filter((permission) => (permission) =>
showNone ? true : permission.level !== Types.PermissionLevel.None (showNone
) ? true
.filter((permission) => : permission.level !== Types.PermissionLevel.None) &&
searchSplit.every( searchSplit.every(
(search) => (search) =>
permission.name.toLowerCase().includes(search) || permission.name.toLowerCase().includes(search) ||
@@ -315,11 +317,46 @@ const PermissionsTable = () => {
} }
columns={[ columns={[
{ {
header: "Resource",
accessorKey: "resource_target.type", accessorKey: "resource_target.type",
header: ({ column }) => (
<SortableHeader column={column} title="Resource" />
),
cell: ({ row }) => {
const Components =
ResourceComponents[
row.original.resource_target.type as UsableResource
];
return (
<div className="flex gap-2 items-center">
<Components.Icon />
{row.original.resource_target.type}
</div>
);
},
}, },
{ {
header: "Target", accessorKey: "resource_target",
sortingFn: (a, b) => {
const ra = resource_name(
a.original.resource_target.type as UsableResource,
a.original.resource_target.id
);
const rb = resource_name(
b.original.resource_target.type as UsableResource,
b.original.resource_target.id
);
if (!ra && !rb) return 0;
if (!ra) return -1;
if (!rb) return 1;
if (ra > rb) return 1;
else if (ra < rb) return -1;
else return 0;
},
header: ({ column }) => (
<SortableHeader column={column} title="Target" />
),
cell: ({ cell: ({
row: { row: {
original: { resource_target }, original: { resource_target },
@@ -334,7 +371,10 @@ const PermissionsTable = () => {
}, },
}, },
{ {
header: "Level", accessorKey: "level",
header: ({ column }) => (
<SortableHeader column={column} title="Level" />
),
cell: ({ row: { original: permission } }) => ( cell: ({ row: { original: permission } }) => (
<Select <Select
value={permission.level} value={permission.level}

View File

@@ -17,6 +17,7 @@ export interface RequiredResourceComponents {
/** Name of the resource */ /** Name of the resource */
Name: IdComponent; Name: IdComponent;
name: (id: string) => string | undefined;
/** Icon for the component */ /** Icon for the component */
Icon: OptionalIdComponent; Icon: OptionalIdComponent;