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

View File

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

View File

@@ -21,6 +21,7 @@ export const BuildComponents: RequiredResourceComponents = {
Table: BuildTable,
Name: ({ id }) => <>{useBuild(id)?.name}</>,
name: (id) => useBuild(id)?.name,
Icon: ({ 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) => useBuilder(id)?.name,
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,
Icon: ({ id }) => {
const state = useDeployment(id)?.info.state;

View File

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

View File

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

View File

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

View File

@@ -74,6 +74,7 @@ export const Topbar = () => {
const PrimaryDropdown = () => {
const type = useResourceParamType();
const Components = type && ResourceComponents[type];
console.log(location.pathname.split("/"));
const [icon, title] = Components
? [<Components.Icon />, type + "s"]
@@ -85,7 +86,8 @@ const PrimaryDropdown = () => {
? [<Tag className="w-4 h-4" />, "Tags"]
: location.pathname === "/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"]
: [<FileQuestion className="w-4 h-4" />, "Unknown"];
// : [<Box className="w-4 h-4" />, "Dashboard"];
@@ -177,36 +179,43 @@ const SecondaryDropdown = () => {
const type = useResourceParamType();
if (type) return <ResourcesDropdown type={type} />;
if (location.pathname !== "/") return;
const Icon = ICONS[view];
if (location.pathname === "/") {
const Icon = ICONS[view];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-48 justify-between px-3">
<div className="flex items-center gap-2">
<Icon />
{view}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="bottom">
<DropdownMenuGroup>
{Object.entries(ICONS).map(([view, Icon]) => (
<DropdownMenuItem
key={view}
className="flex items-center gap-2"
onClick={() => setView(view as HomeView)}
>
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-48 justify-between px-3">
<div className="flex items-center gap-2">
<Icon />
{view}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="bottom">
<DropdownMenuGroup>
{Object.entries(ICONS).map(([view, Icon]) => (
<DropdownMenuItem
key={view}
className="flex items-center gap-2"
onClick={() => setView(view as HomeView)}
>
<Icon />
{view}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
const [_, base, user_id] = location.pathname.split("/");
if (base === "users") {
return <UsersDropdown user_id={user_id} />;
}
};
const ResourcesDropdown = ({ type }: { type: UsableResource }) => {
@@ -274,3 +283,83 @@ const ResourcesDropdown = ({ type }: { type: UsableResource }) => {
</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 { UsableResource } from "@types";
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) {
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 { Box, History } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { Card, CardContent } from "@ui/card";
import { TagsSummary } from "@components/dashboard/tags";
import { ApiKeysSummary } from "@components/dashboard/api-keys";
import { Box, History, Key, Tag } from "lucide-react";
import { Link, useNavigate } from "react-router-dom";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { ResourceComponents } from "@components/resources";
import { OpenAlerts } from "@components/alert";
import { useUser } from "@lib/hooks";
import { useRead, useUser } from "@lib/hooks";
import { ResourceLink } from "@components/resources/common";
import { Fragment } from "react";
@@ -79,3 +77,43 @@ const RecentlyViewed = () => {
</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 { ResourceComponents } from "@components/resources";
import { ResourceLink } from "@components/resources/common";
import { ConfirmButton } from "@components/util";
import { text_color_class_by_intention } from "@lib/color";
import { useInvalidate, useRead, useSetTitle, useWrite } from "@lib/hooks";
import { resource_name } from "@lib/utils";
import { Types } from "@monitor/client";
import { UsableResource } from "@types";
import { DataTable } from "@ui/data-table";
import { DataTable, SortableHeader } from "@ui/data-table";
import { Input } from "@ui/input";
import { Label } from "@ui/label";
import {
@@ -282,6 +284,12 @@ const PermissionsTable = () => {
title="Permissions"
actions={
<div className="flex gap-6 items-center">
<Input
placeholder="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-[300px]"
/>
<div
className="flex gap-3 items-center"
onClick={() => setShowNone(!showNone)}
@@ -289,37 +297,66 @@ const PermissionsTable = () => {
<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
tableKey="permissions"
data={
permissions
?.filter((permission) =>
showNone ? true : permission.level !== Types.PermissionLevel.None
)
.filter((permission) =>
permissions?.filter(
(permission) =>
(showNone
? true
: permission.level !== Types.PermissionLevel.None) &&
searchSplit.every(
(search) =>
permission.name.toLowerCase().includes(search) ||
permission.resource_target.type.toLowerCase().includes(search)
)
) ?? []
) ?? []
}
columns={[
{
header: "Resource",
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: ({
row: {
original: { resource_target },
@@ -334,7 +371,10 @@ const PermissionsTable = () => {
},
},
{
header: "Level",
accessorKey: "level",
header: ({ column }) => (
<SortableHeader column={column} title="Level" />
),
cell: ({ row: { original: permission } }) => (
<Select
value={permission.level}

View File

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