improve topbar navigation

This commit is contained in:
mbecker20
2024-04-01 02:28:51 -07:00
parent 24d2e744a4
commit 8f5570128d
12 changed files with 199 additions and 96 deletions

View File

@@ -30,6 +30,7 @@ import {
SelectValue, SelectValue,
} from "@ui/select"; } from "@ui/select";
import { Switch } from "@ui/switch"; import { Switch } from "@ui/switch";
import { CommandList } from "cmdk";
import { import {
ArrowDown, ArrowDown,
ArrowUp, ArrowUp,
@@ -291,9 +292,8 @@ const ProcedureConfigInner = ({
header: "Delete", header: "Delete",
cell: ({ row: { index } }) => ( cell: ({ row: { index } }) => (
<ConfirmButton <ConfirmButton
title="Delete" title="Delete Row"
icon={<Trash2 className="w-4 h-4" />} icon={<Trash2 className="w-4 h-4" />}
variant="destructive"
onClick={() => onClick={() =>
setConfig({ setConfig({
...config, ...config,
@@ -343,6 +343,7 @@ const ExecutionTypeSelector = ({
value={search} value={search}
onValueChange={setSearch} onValueChange={setSearch}
/> />
<CommandList>
<CommandEmpty className="flex justify-evenly items-center"> <CommandEmpty className="flex justify-evenly items-center">
Empty. Empty.
<SearchX className="w-3 h-3" /> <SearchX className="w-3 h-3" />
@@ -368,6 +369,7 @@ const ExecutionTypeSelector = ({
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
</CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@@ -52,7 +52,10 @@ export const ServerComponents: RequiredResourceComponents = {
}, },
Actions: SERVER_ACTIONS, Actions: SERVER_ACTIONS,
Page: { Page: {
Stats: ({ id }) => <ServerStats server_id={id} />, Stats: ({ id }) => {
const status = useServer(id)?.info.status;
return status === "Ok" && <ServerStats server_id={id} />;
},
Deployments: ({ id }) => { Deployments: ({ id }) => {
const deployments = useRead("ListDeployments", {}).data?.filter( const deployments = useRead("ListDeployments", {}).data?.filter(
(deployment) => deployment.info.server_id === id (deployment) => deployment.info.server_id === id

View File

@@ -54,8 +54,8 @@ export const TagsFilter = () => {
?.filter((tag) => !tags.includes(tag._id!.$oid)) ?.filter((tag) => !tags.includes(tag._id!.$oid))
.map((tag) => ( .map((tag) => (
<DropdownMenuItem <DropdownMenuItem
className="cursor-pointer"
key={tag.name} key={tag.name}
className="cursor-pointer"
onClick={() => setTags([...tags, tag._id!.$oid])} onClick={() => setTags([...tags, tag._id!.$oid])}
> >
{tag.name} {tag.name}

View File

@@ -1,6 +1,16 @@
import { useRead, useResourceParamType } from "@lib/hooks"; import { useRead, useResourceParamType } from "@lib/hooks";
import { ResourceComponents } from "./resources"; import { ResourceComponents } from "./resources";
import { Box, Boxes, FolderTree, Key, Tag, UserCircle2 } from "lucide-react"; import {
Box,
Boxes,
FileQuestion,
FolderTree,
Home,
Key,
SearchX,
Tag,
UserCircle2,
} from "lucide-react";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -10,16 +20,28 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@ui/dropdown-menu"; } from "@ui/dropdown-menu";
import { Button } from "@ui/button"; import { Button } from "@ui/button";
import { Link, useParams } from "react-router-dom"; import { Link, useNavigate, useParams } from "react-router-dom";
import { RESOURCE_TARGETS } from "@lib/utils"; import { RESOURCE_TARGETS } from "@lib/utils";
import { Omnibar } from "./omnibar"; import { Omnibar } from "./omnibar";
import { WsStatusIndicator } from "@lib/socket"; import { WsStatusIndicator } from "@lib/socket";
import { HeaderUpdates } from "./updates/header"; import { HeaderUpdates } from "./updates/header";
import { Logout } from "./util"; import { Logout } from "./util";
import { ThemeToggle } from "@ui/theme"; import { ThemeToggle } from "@ui/theme";
import { UsableResource } from "@types";
import { atomWithStorage } from "jotai/utils";
import { useAtom } from "jotai";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import { useState } from "react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@ui/command";
export const Topbar = () => { export const Topbar = () => {
const type = useResourceParamType();
return ( return (
<div className="sticky top-0 border-b bg-background z-50 w-full"> <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="container flex items-center justify-between py-4 gap-8">
@@ -28,8 +50,8 @@ export const Topbar = () => {
MONITOR MONITOR
</Link> </Link>
<div className="flex gap-2"> <div className="flex gap-2">
<ResourceTypeDropdown /> <PrimaryDropdown />
{type && <ResourcesDropdown />} <SecondaryDropdown />
</div> </div>
</div> </div>
<div className="flex md:gap-4"> <div className="flex md:gap-4">
@@ -47,21 +69,22 @@ export const Topbar = () => {
); );
}; };
const ResourceTypeDropdown = () => { const PrimaryDropdown = () => {
const type = useResourceParamType(); const type = useResourceParamType();
const Components = ResourceComponents[type]; const Components = type && ResourceComponents[type];
const [icon, title] = type const [icon, title] = Components
? [<Components.Icon />, type + "s"] ? [<Components.Icon />, type + "s"]
: location.pathname === "/tree" : location.pathname === "/"
? [<FolderTree className="w-4 h-4" />, "Tree"] ? [<Home className="w-4 h-4" />, "Home"]
: location.pathname === "/keys" : location.pathname === "/keys"
? [<Key className="w-4 h-4" />, "Api Keys"] ? [<Key className="w-4 h-4" />, "Api Keys"]
: location.pathname === "/tags" : location.pathname === "/tags"
? [<Tag className="w-4 h-4" />, "Tags"] ? [<Tag className="w-4 h-4" />, "Tags"]
: location.pathname === "/users" : location.pathname === "/users"
? [<UserCircle2 className="w-4 h-4" />, "Users"] ? [<UserCircle2 className="w-4 h-4" />, "Users"]
: [<Box className="w-4 h-4" />, "Dashboard"]; : [<FileQuestion className="w-4 h-4" />, "Unknown"];
// : [<Box className="w-4 h-4" />, "Dashboard"];
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -77,23 +100,8 @@ const ResourceTypeDropdown = () => {
<DropdownMenuGroup> <DropdownMenuGroup>
<Link to="/"> <Link to="/">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer"> <DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Box className="w-4 h-4" /> <Home className="w-4 h-4" />
Dashboard Home
</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> </DropdownMenuItem>
</Link> </Link>
@@ -140,48 +148,118 @@ const ResourceTypeDropdown = () => {
); );
}; };
const ResourcesDropdown = () => { export type HomeView = "Dashboard" | "Tree" | "Resources";
const type = useResourceParamType();
const id = useParams().id as string;
const list = useRead(`List${type}s`, {}).data;
const selected = list?.find((i) => i.id === id); export const homeViewAtom = atomWithStorage<HomeView>(
const Components = ResourceComponents[type]; "home-view-v1",
"Dashboard"
);
const ICONS = {
Dashboard: () => <Box className="w-4 h-4" />,
Tree: () => <FolderTree className="w-4 h-4" />,
Resources: () => <Boxes className="w-4 h-4" />,
};
const SecondaryDropdown = () => {
const [view, setView] = useAtom(homeViewAtom);
const type = useResourceParamType();
if (type) return <ResourcesDropdown type={type} />;
if (location.pathname !== "/") return;
const Icon = ICONS[view];
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-48 justify-between px-3"> <Button variant="ghost" className="w-48 justify-between px-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Components.Icon id={selected?.id} /> <Icon />
{selected ? selected.name : `All ${type}s`} {view}
</div> </div>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="bottom"> <DropdownMenuContent className="w-48" side="bottom">
<DropdownMenuGroup> <DropdownMenuGroup>
<Link to={`/${type.toLowerCase()}s`}> {Object.entries(ICONS).map(([view, Icon]) => (
<DropdownMenuItem className="flex items-center gap-2"> <DropdownMenuItem
<Components.Icon /> key={view}
All {type}s className="flex items-center gap-2"
onClick={() => setView(view as HomeView)}
>
<Icon />
{view}
</DropdownMenuItem> </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> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );
}; };
const ResourcesDropdown = ({ type }: { type: UsableResource }) => {
const nav = useNavigate();
const id = useParams().id as string;
const list = useRead(`List${type}s`, {}).data;
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
const selected = list?.find((i) => i.id === id);
const Components = ResourceComponents[type];
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">
<Components.Icon id={selected?.id} />
{selected ? selected.name : `All ${type}s`}
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] max-h-[400px] p-0" sideOffset={12}>
<Command>
<CommandInput
placeholder={`Search ${type}s`}
className="h-9"
value={input}
onValueChange={setInput}
/>
<CommandList>
<CommandEmpty className="flex justify-evenly items-center">
{`No ${type}s Found`}
<SearchX className="w-3 h-3" />
</CommandEmpty>
<CommandGroup>
<CommandItem
onSelect={() => {
setOpen(false);
nav(`/${type.toLowerCase()}s`);
}}
>
<Button variant="link" className="flex gap-2 items-center p-0">
<Components.Icon />
All {type}s
</Button>
</CommandItem>
{list?.map((resource) => (
<CommandItem
key={resource.id}
onSelect={() => {
setOpen(false);
nav(`/${type.toLowerCase()}s/${resource.id}`);
}}
>
<Components.Link id={resource.id} />
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};

View File

@@ -143,9 +143,12 @@ export const useAuth = <
// ============== UTILITY ============== // ============== UTILITY ==============
/**
* Actually returns UsableResoure | undefined
*/
export const useResourceParamType = () => { export const useResourceParamType = () => {
const type = useParams().type; const type = useParams().type;
if (!type) return undefined as unknown as UsableResource; if (!type) return undefined;
return (type[0].toUpperCase() + type.slice(1, -1)) as UsableResource; return (type[0].toUpperCase() + type.slice(1, -1)) as UsableResource;
}; };

View File

@@ -11,10 +11,10 @@ export const object_keys = <T extends object>(o: T): (keyof T)[] =>
Object.keys(o) as (keyof T)[]; Object.keys(o) as (keyof T)[];
export const RESOURCE_TARGETS: UsableResource[] = [ export const RESOURCE_TARGETS: UsableResource[] = [
"Procedure",
"Deployment", "Deployment",
"Server", "Server",
"Build", "Build",
"Procedure",
"Repo", "Repo",
"Builder", "Builder",
"Alerter", "Alerter",

View File

@@ -37,7 +37,7 @@ export const AllResources = () => {
if (!count) return; if (!count) return;
return ( return (
<Section title={type + "s"} actions={<Components.New />}> <Section key={type} title={type + "s"} actions={<Components.New />}>
<Components.Table /> <Components.Table />
</Section> </Section>
); );

View File

@@ -0,0 +1,17 @@
import { homeViewAtom } from "@components/topbar";
import { useAtom } from "jotai";
import { Dashboard } from "./dashboard";
import { AllResources } from "./all_resources";
import { Tree } from "./tree";
export const Home = () => {
const [view, _] = useAtom(homeViewAtom);
switch (view) {
case "Dashboard":
return <Dashboard />;
case "Resources":
return <AllResources />;
case "Tree":
return <Tree />;
}
};

View File

@@ -6,7 +6,7 @@ import { usePushRecentlyViewed, useResourceParamType } from "@lib/hooks";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
export const Resource = () => { export const Resource = () => {
const type = useResourceParamType(); const type = useResourceParamType()!;
const id = useParams().id as string; const id = useParams().id as string;
usePushRecentlyViewed({ type, id }); usePushRecentlyViewed({ type, id });
@@ -30,9 +30,9 @@ export const Resource = () => {
<Components.Icon id={id} /> <Components.Icon id={id} />
<Components.Status id={id} /> <Components.Status id={id} />
</div> </div>
{Components.Info.map((Info) => ( {Components.Info.map((Info, i) => (
<> <>
| <Info id={id} /> | <Info key={i} id={id} />
</> </>
))} ))}
</div> </div>
@@ -40,8 +40,8 @@ export const Resource = () => {
} }
actions={ actions={
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
{Components.Actions.map((Action) => ( {Components.Actions.map((Action, i) => (
<Action id={id} /> <Action key={i} id={id} />
))} ))}
</div> </div>
} }
@@ -49,7 +49,7 @@ export const Resource = () => {
<ResourceUpdates type={type} id={id} /> <ResourceUpdates type={type} id={id} />
{/* <ResourcePermissions type={type} id={id} /> */} {/* <ResourcePermissions type={type} id={id} /> */}
{Object.entries(Components.Page).map(([section, Component]) => ( {Object.entries(Components.Page).map(([section, Component]) => (
<Component id={id} key={section} /> <Component key={section} id={id} />
))} ))}
</Page> </Page>
); );

View File

@@ -1,24 +1,24 @@
import { Layout } from "@components/layouts"; import { Layout } from "@components/layouts";
import { useUser } from "@lib/hooks"; import { useUser } from "@lib/hooks";
import { Dashboard } from "@pages/dashboard";
import { Login } from "@pages/login"; import { Login } from "@pages/login";
import { Resource } from "@pages/resource"; import { Resource } from "@pages/resource";
import { Resources } from "@pages/resources"; import { Resources } from "@pages/resources";
import { Keys } from "@pages/keys"; import { Keys } from "@pages/keys";
import { RouterProvider, createBrowserRouter } from "react-router-dom"; import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { Tree } from "@pages/tree"; import { Tree } from "@pages/home/tree";
import { Tags } from "@pages/tags"; import { Tags } from "@pages/tags";
import { ResourceUpdates } from "@pages/resource_update"; import { ResourceUpdates } from "@pages/resource_update";
import { UserPage, UsersPage } from "@pages/users"; import { UserPage, UsersPage } from "@pages/users";
import { AllResources } from "@pages/all_resources"; import { AllResources } from "@pages/home/all_resources";
import { UserDisabled } from "@pages/user_disabled"; import { UserDisabled } from "@pages/user_disabled";
import { Home } from "@pages/home";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: "/", path: "/",
element: <Layout />, element: <Layout />,
children: [ children: [
{ path: "", element: <Dashboard /> }, { path: "", element: <Home /> },
{ path: "keys", element: <Keys /> }, { path: "keys", element: <Keys /> },
{ path: "tags", element: <Tags /> }, { path: "tags", element: <Tags /> },
{ path: "tree", element: <Tree /> }, { path: "tree", element: <Tree /> },