mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-11 17:44:19 -05:00
more responsive
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user