more responsive

This commit is contained in:
mbecker20
2024-07-04 14:41:40 -07:00
parent d4a1891c70
commit b1e4b55ba1
3 changed files with 61 additions and 346 deletions

View File

@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",

View File

@@ -7,9 +7,7 @@ import {
Boxes,
FileQuestion,
FolderTree,
Home,
Keyboard,
SearchX,
Settings,
User,
Users,
@@ -23,32 +21,16 @@ import {
DropdownMenuTrigger,
} from "@ui/dropdown-menu";
import { Button } from "@ui/button";
import { Link, useNavigate, useParams } from "react-router-dom";
import {
RESOURCE_TARGETS,
filterBySplit,
usableResourcePath,
} from "@lib/utils";
import { Link } from "react-router-dom";
import { RESOURCE_TARGETS, usableResourcePath } from "@lib/utils";
import { OmniSearch, OmniDialog } from "./omnibar";
import { WsStatusIndicator } from "@lib/socket";
import { TopbarUpdates } from "./updates/topbar";
import { Logout, UserAvatar } from "./util";
import { Logout } from "./util";
import { ThemeToggle } from "@ui/theme";
import { UsableResource } from "@types";
import { useAtom } from "jotai";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import { ReactNode, useState } from "react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@ui/command";
import { ResourceLink } from "./resources/common";
import { HomeView, homeViewAtom } from "@main";
import { Types } from "@monitor/client";
import {
Dialog,
DialogContent,
@@ -65,26 +47,18 @@ export const Topbar = () => {
return (
<div className="sticky top-0 h-[70px] border-b z-50 w-full bg-card text-card-foreground shadow flex items-center">
<div className="w-full p-4 grid grid-cols-2 lg:grid-cols-3">
<div className="flex items-center gap-4 justify-self-start w-fit">
<div className="flex items-center justify-self-start w-fit gap-0 md:gap-4">
<Link
to={"/"}
// className="flex gap-3 items-start text-2xl tracking-widest lg:mx-2"
className="flex gap-3 items-center text-2xl tracking-widest lg:mx-2"
to="/"
className="flex gap-3 items-center text-2xl tracking-widest md:mx-2"
>
{/* <img
src="/monitor-lizard.png"
className="w-9 h-7 dark:invert hidden lg:block"
/> */}
<img
src="/monitor-circle.png"
className="w-[28px] dark:invert hidden lg:block"
className="w-[28px] dark:invert"
/>
MONITOR
<div className="hidden md:block">MONITOR</div>
</Link>
<div className="flex gap-2">
<PrimaryDropdown />
<SecondaryDropdown />
</div>
<MobileDropdown />
</div>
<OmniSearch
setOpen={setOmniOpen}
@@ -113,9 +87,10 @@ export const Topbar = () => {
);
};
const PrimaryDropdown = () => {
const MobileDropdown = () => {
const type = useResourceParamType();
const Components = type && ResourceComponents[type];
const [view, setView] = useAtom<HomeView>(homeViewAtom);
const [icon, title] = Components
? [
@@ -126,8 +101,12 @@ const PrimaryDropdown = () => {
? "Sync"
: type) + "s",
]
: location.pathname === "/"
? [<Home className="w-4 h-4" />, "Home"]
: location.pathname === "/" && view === "Dashboard"
? [<Box className="w-4 h-4" />, "Dashboard"]
: location.pathname === "/" && view === "Resources"
? [<Boxes className="w-4 h-4" />, "Resources"]
: location.pathname === "/" && view === "Tree"
? [<FolderTree className="w-4 h-4" />, "Tree"]
: location.pathname === "/settings"
? [<Settings className="w-4 h-4" />, "Settings"]
: location.pathname === "/alerts"
@@ -139,7 +118,6 @@ const PrimaryDropdown = () => {
: location.pathname.split("/")[1] === "users"
? [<User className="w-4 h-4" />, "Users"]
: [<FileQuestion className="w-4 h-4" />, "Unknown"];
// : [<Box className="w-4 h-4" />, "Dashboard"];
return (
<DropdownMenu>
@@ -155,16 +133,34 @@ const PrimaryDropdown = () => {
<DropdownMenuContent className="w-36" side="bottom" align="start">
<DropdownMenuGroup>
<DropdownLinkItem
label="Home"
icon={<Home className="w-4 h-4" />}
label="Dashboard"
icon={<Box className="w-4 h-4" />}
to="/"
onClick={() => setView("Dashboard")}
/>
<DropdownLinkItem
label="Resources"
icon={<Boxes className="w-4 h-4" />}
to="/"
onClick={() => setView("Resources")}
/>
<DropdownLinkItem
label="Tree"
icon={<FolderTree className="w-4 h-4" />}
to="/"
onClick={() => setView("Tree")}
/>
<DropdownMenuSeparator />
{RESOURCE_TARGETS.map((type) => {
const RTIcon = ResourceComponents[type].Icon;
const name = type === "ServerTemplate" ? "Template" : type;
const name =
type === "ServerTemplate"
? "Template"
: type === "ResourceSync"
? "Sync"
: type;
return (
<DropdownLinkItem
key={type}
@@ -206,13 +202,15 @@ const DropdownLinkItem = ({
label,
icon,
to,
onClick,
}: {
label: string;
icon: ReactNode;
to: string;
onClick?: () => void;
}) => {
return (
<Link to={to}>
<Link to={to} onClick={onClick}>
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
{icon}
{label}
@@ -221,296 +219,11 @@ const DropdownLinkItem = ({
);
};
const ICONS = {
Dashboard: () => <Box className="w-4 h-4" />,
Resources: () => <Boxes className="w-4 h-4" />,
Tree: () => <FolderTree className="w-4 h-4" />,
};
const SecondaryDropdown = () => {
const [view, setView] = useAtom(homeViewAtom);
const type = useResourceParamType();
if (type) return <ResourcesDropdown type={type} />;
if (location.pathname === "/") {
const Icon = ICONS[view];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="hidden sm:flex lg:hidden justify-start items-center gap-2 w-48 px-3"
>
<Icon />
{view}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48" side="bottom" align="start">
<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, id] = location.pathname.split("/");
if (base === "users") {
return <UsersDropdown user_id={id} />;
} else if (base === "user-groups") {
return <UserGroupDropdown group_id={id} />;
}
};
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 [search, setSearch] = useState("");
const selected = list?.find((i) => i.id === id);
const Components = ResourceComponents[type];
const filtered = filterBySplit(
list as Types.ResourceListItem<unknown>[],
search,
(item) => item.name
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="hidden sm:flex lg:hidden justify-start items-center gap-2 w-48 px-3"
>
<Components.Icon id={selected?.id} />
{selected ? selected.name : `All ${type}s`}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] max-h-[400px] p-0" align="start">
<Command shouldFilter={false}>
<CommandInput
placeholder={`Search ${type}s`}
className="h-9"
value={search}
onValueChange={setSearch}
/>
<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(`/${usableResourcePath(type)}`);
}}
>
<Button variant="link" className="flex gap-2 items-center p-0">
<Components.Icon />
All {type}s
</Button>
</CommandItem>
{filtered?.map((resource) => (
<CommandItem
key={resource.id}
onSelect={() => {
setOpen(false);
nav(`/${usableResourcePath(type)}/${resource.id}`);
}}
>
<ResourceLink type={type} id={resource.id} />
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
const UserGroupDropdown = ({ group_id }: { group_id: string | undefined }) => {
const nav = useNavigate();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const groups = useRead("ListUserGroups", {}).data ?? [];
const selected = group_id
? groups?.find((user) => user._id?.$oid === group_id)
: undefined;
const filtered = filterBySplit(
groups as Types.UserGroup[],
search,
(item) => item.name
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="hidden sm:flex lg:hidden justify-start items-center gap-2 w-48 px-3"
>
<Users className="w-4 h-4" />
{selected ? selected.name : "All User Groups"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] max-h-[400px] p-0" sideOffset={12}>
<Command shouldFilter={false}>
<CommandInput
placeholder="Search User Groups"
className="h-9"
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty className="flex justify-evenly items-center">
No User Groups 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">
<User className="w-4" />
All User Groups
</Button>
</CommandItem>
{filtered?.map((group) => (
<CommandItem
key={group.name}
onSelect={() => {
setOpen(false);
nav(`/user-groups/${group._id?.$oid}`);
}}
>
<Button
variant="link"
className="flex gap-2 items-center p-0"
>
<Users className="w-4 h-4" />
{group.name}
</Button>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
const UsersDropdown = ({ user_id }: { user_id: string | undefined }) => {
const nav = useNavigate();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const users = useRead("ListUsers", {}).data ?? [];
const selected = user_id
? users?.find((user) => user._id?.$oid === user_id)
: undefined;
const avatar = (selected?.config.data as { avatar?: string })?.avatar;
const filtered = filterBySplit(
users as Types.User[],
search,
(item) => item.username
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="hidden sm:flex lg:hidden justify-start items-center gap-2 w-48 px-3"
>
<UserAvatar avatar={avatar} />
{selected ? selected.username : "All Users"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] max-h-[400px] p-0" sideOffset={12}>
<Command shouldFilter={false}>
<CommandInput
placeholder="Search Users"
className="h-9"
value={search}
onValueChange={setSearch}
/>
<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">
<User className="w-4" />
All Users
</Button>
</CommandItem>
{filtered?.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 KeyboardShortcuts = () => {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" className="items-center gap-2">
<Button variant="ghost" className="hidden md:flex items-center gap-2">
<Keyboard className="w-4 h-4" />
</Button>
</DialogTrigger>

View File

@@ -48,23 +48,25 @@ const ResourceRow = ({ type }: { type: UsableResource }) => {
return (
<div className="flex gap-4">
<Components.Dashboard />
<div className="py-2">
<Separator orientation="vertical" />
</div>
<div className="flex flex-col gap-4 w-full pb-1">
<div className="flex gap-2 items-center text-muted-foreground">
<History className="w-4 h-4" />
<h3>Recent {type}s</h3>
<div className="hidden lg:flex gap-4 ">
<div className="py-2">
<Separator orientation="vertical" />
</div>
<div className="grid grid-rows-2 grid-cols-2 2xl:grid-cols-3 gap-4 w-full h-full">
{ids.map((id, i) => (
<RecentCard
key={type + id}
type={type}
id={id}
className={i > 3 && "hidden 2xl:block"}
/>
))}
<div className="flex flex-col gap-4 w-full pb-1">
<div className="flex gap-2 items-center text-muted-foreground">
<History className="w-4 h-4" />
<h3>Recent {type}s</h3>
</div>
<div className="grid grid-rows-2 grid-cols-2 2xl:grid-cols-3 gap-4 w-full h-full">
{ids.map((id, i) => (
<RecentCard
key={type + id}
type={type}
id={id}
className={i > 3 && "hidden 2xl:block"}
/>
))}
</div>
</div>
</div>
</div>