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

View File

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

View File

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

View File

@@ -1,6 +1,16 @@
import { useRead, useResourceParamType } from "@lib/hooks";
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 {
DropdownMenu,
DropdownMenuContent,
@@ -10,16 +20,28 @@ import {
DropdownMenuTrigger,
} from "@ui/dropdown-menu";
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 { Omnibar } from "./omnibar";
import { WsStatusIndicator } from "@lib/socket";
import { HeaderUpdates } from "./updates/header";
import { Logout } from "./util";
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 = () => {
const type = useResourceParamType();
return (
<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">
@@ -28,8 +50,8 @@ export const Topbar = () => {
MONITOR
</Link>
<div className="flex gap-2">
<ResourceTypeDropdown />
{type && <ResourcesDropdown />}
<PrimaryDropdown />
<SecondaryDropdown />
</div>
</div>
<div className="flex md:gap-4">
@@ -47,21 +69,22 @@ export const Topbar = () => {
);
};
const ResourceTypeDropdown = () => {
const PrimaryDropdown = () => {
const type = useResourceParamType();
const Components = ResourceComponents[type];
const Components = type && ResourceComponents[type];
const [icon, title] = type
const [icon, title] = Components
? [<Components.Icon />, type + "s"]
: location.pathname === "/tree"
? [<FolderTree className="w-4 h-4" />, "Tree"]
: location.pathname === "/"
? [<Home className="w-4 h-4" />, "Home"]
: location.pathname === "/keys"
? [<Key className="w-4 h-4" />, "Api Keys"]
: location.pathname === "/tags"
? [<Tag className="w-4 h-4" />, "Tags"]
: location.pathname === "/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 (
<DropdownMenu>
@@ -77,23 +100,8 @@ const ResourceTypeDropdown = () => {
<DropdownMenuGroup>
<Link to="/">
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
<Box className="w-4 h-4" />
Dashboard
</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
<Home className="w-4 h-4" />
Home
</DropdownMenuItem>
</Link>
@@ -140,48 +148,118 @@ const ResourceTypeDropdown = () => {
);
};
const ResourcesDropdown = () => {
const type = useResourceParamType();
const id = useParams().id as string;
const list = useRead(`List${type}s`, {}).data;
export type HomeView = "Dashboard" | "Tree" | "Resources";
const selected = list?.find((i) => i.id === id);
const Components = ResourceComponents[type];
export const homeViewAtom = atomWithStorage<HomeView>(
"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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="w-48 justify-between px-3">
<div className="flex items-center gap-2">
<Components.Icon id={selected?.id} />
{selected ? selected.name : `All ${type}s`}
<Icon />
{view}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="bottom">
<DropdownMenuGroup>
<Link to={`/${type.toLowerCase()}s`}>
<DropdownMenuItem className="flex items-center gap-2">
<Components.Icon />
All {type}s
{Object.entries(ICONS).map(([view, Icon]) => (
<DropdownMenuItem
key={view}
className="flex items-center gap-2"
onClick={() => setView(view as HomeView)}
>
<Icon />
{view}
</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>
</DropdownMenuContent>
</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 ==============
/**
* Actually returns UsableResoure | undefined
*/
export const useResourceParamType = () => {
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;
};

View File

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

View File

@@ -37,7 +37,7 @@ export const AllResources = () => {
if (!count) return;
return (
<Section title={type + "s"} actions={<Components.New />}>
<Section key={type} title={type + "s"} actions={<Components.New />}>
<Components.Table />
</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";
export const Resource = () => {
const type = useResourceParamType();
const type = useResourceParamType()!;
const id = useParams().id as string;
usePushRecentlyViewed({ type, id });
@@ -30,9 +30,9 @@ export const Resource = () => {
<Components.Icon id={id} />
<Components.Status id={id} />
</div>
{Components.Info.map((Info) => (
{Components.Info.map((Info, i) => (
<>
| <Info id={id} />
| <Info key={i} id={id} />
</>
))}
</div>
@@ -40,8 +40,8 @@ export const Resource = () => {
}
actions={
<div className="flex gap-4 items-center">
{Components.Actions.map((Action) => (
<Action id={id} />
{Components.Actions.map((Action, i) => (
<Action key={i} id={id} />
))}
</div>
}
@@ -49,7 +49,7 @@ export const Resource = () => {
<ResourceUpdates type={type} id={id} />
{/* <ResourcePermissions type={type} id={id} /> */}
{Object.entries(Components.Page).map(([section, Component]) => (
<Component id={id} key={section} />
<Component key={section} id={id} />
))}
</Page>
);

View File

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