mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-11 17:44:19 -05:00
fix permissioning endpoint
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 />
|
||||
|
||||
@@ -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" />,
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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" />,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />,
|
||||
|
||||
|
||||
@@ -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" />,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
1
frontend/src/types.d.ts
vendored
1
frontend/src/types.d.ts
vendored
@@ -17,6 +17,7 @@ export interface RequiredResourceComponents {
|
||||
|
||||
/** Name of the resource */
|
||||
Name: IdComponent;
|
||||
name: (id: string) => string | undefined;
|
||||
|
||||
/** Icon for the component */
|
||||
Icon: OptionalIdComponent;
|
||||
|
||||
Reference in New Issue
Block a user