forked from github-starred/komodo
split sidebar home
This commit is contained in:
@@ -3,11 +3,10 @@ import {
|
||||
alert_level_intention,
|
||||
text_color_class_by_intention,
|
||||
} from "@lib/color";
|
||||
import { useRead } from "@lib/hooks";
|
||||
import { useRead, atomWithStorage } from "@lib/hooks";
|
||||
import { Types } from "@monitor/client";
|
||||
import { Button } from "@ui/button";
|
||||
import { useAtom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { AlertsTable } from "./table";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@ui/button";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Link, Outlet } from "react-router-dom";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { Link, Outlet, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -19,6 +19,21 @@ import { usableResourcePath } from "@lib/utils";
|
||||
import { Sidebar } from "./sidebar";
|
||||
|
||||
export const Layout = () => {
|
||||
const nav = useNavigate();
|
||||
useEffect(() => {
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
// This will ignore Shift + S if it is sent from input / textarea
|
||||
const target = e.target as any;
|
||||
if (target.matches("input") || target.matches("textarea")) return;
|
||||
|
||||
if (e.shiftKey && e.key === "H") {
|
||||
e.preventDefault();
|
||||
nav("/");
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", keydown);
|
||||
return () => document.removeEventListener("keydown", keydown);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Topbar />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { atomWithStorage } from "@lib/hooks";
|
||||
import { Types } from "@monitor/client";
|
||||
import { useAtom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
const statsGranularityAtom = atomWithStorage(
|
||||
"stats-granularity-v0",
|
||||
|
||||
@@ -1,17 +1,49 @@
|
||||
import { RESOURCE_TARGETS, cn, usableResourcePath } from "@lib/utils";
|
||||
import { Button } from "@ui/button";
|
||||
import { Card, CardContent } from "@ui/card";
|
||||
import { AlertTriangle, Bell, Box, Home, Tag, UserCircle2 } from "lucide-react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Bell,
|
||||
Box,
|
||||
Boxes,
|
||||
FolderTree,
|
||||
Tag,
|
||||
UserCircle2,
|
||||
} from "lucide-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { ResourceComponents } from "./resources";
|
||||
import { Separator } from "@ui/separator";
|
||||
import { ReactNode } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { homeViewAtom } from "@main";
|
||||
|
||||
export const Sidebar = () => {
|
||||
const [view, setView] = useAtom(homeViewAtom);
|
||||
console.log(view);
|
||||
return (
|
||||
<Card className="h-fit m-4 hidden lg:flex">
|
||||
<CardContent className="h-fit grid gap-2 px-6 py-4">
|
||||
<SidebarLink label="Home" to="/" icon={<Home className="w-4 h-4" />} />
|
||||
<SidebarLink
|
||||
label="Dashboard"
|
||||
to="/"
|
||||
icon={<Box className="w-4 h-4" />}
|
||||
onClick={() => setView("Dashboard")}
|
||||
highlighted={view === "Dashboard"}
|
||||
/>
|
||||
<SidebarLink
|
||||
label="Resources"
|
||||
to="/"
|
||||
icon={<Boxes className="w-4 h-4" />}
|
||||
onClick={() => setView("Resources")}
|
||||
highlighted={view === "Resources"}
|
||||
/>
|
||||
<SidebarLink
|
||||
label="Tree"
|
||||
to="/"
|
||||
icon={<FolderTree className="w-4 h-4" />}
|
||||
onClick={() => setView("Tree")}
|
||||
highlighted={view === "Tree"}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -70,20 +102,27 @@ const SidebarLink = ({
|
||||
to,
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
highlighted,
|
||||
}: {
|
||||
to: string;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
highlighted?: boolean;
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const hl =
|
||||
"/" + location.pathname.split("/")[1] === to && (highlighted ?? true);
|
||||
return (
|
||||
<Link to={to} className="w-full">
|
||||
<Button
|
||||
variant="link"
|
||||
className={cn(
|
||||
"flex justify-start items-center gap-2 w-full hover:bg-accent",
|
||||
"/" + location.pathname.split("/")[1] === to && "bg-accent"
|
||||
hl && "bg-accent"
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
|
||||
@@ -31,7 +31,6 @@ import { TopbarUpdates } from "./updates/topbar";
|
||||
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 { ReactNode, useEffect, useState } from "react";
|
||||
@@ -44,11 +43,12 @@ import {
|
||||
CommandList,
|
||||
} from "@ui/command";
|
||||
import { ResourceLink } from "./resources/common";
|
||||
import { HomeView, homeViewAtom } from "@main";
|
||||
|
||||
export const Topbar = () => {
|
||||
const [omniOpen, setOmniOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
// This will ignore Shift + S if it is sent from input / textarea
|
||||
const target = e.target as any;
|
||||
if (target.matches("input") || target.matches("textarea")) return;
|
||||
@@ -58,8 +58,8 @@ export const Topbar = () => {
|
||||
setOmniOpen(true);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
document.addEventListener("keydown", keydown);
|
||||
return () => document.removeEventListener("keydown", keydown);
|
||||
});
|
||||
return (
|
||||
<div className="sticky top-0 h-[70px] border-b z-50 w-full bg-card text-card-foreground shadow flex items-center">
|
||||
@@ -209,17 +209,10 @@ const DropdownLinkItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type HomeView = "Dashboard" | "Tree" | "Resources";
|
||||
|
||||
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" />,
|
||||
Tree: () => <FolderTree className="w-4 h-4" />,
|
||||
};
|
||||
|
||||
const SecondaryDropdown = () => {
|
||||
@@ -236,7 +229,7 @@ const SecondaryDropdown = () => {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hidden sm:flex justify-start items-center gap-2 w-48 px-3"
|
||||
className="hidden sm:flex lg:hidden justify-start items-center gap-2 w-48 px-3"
|
||||
>
|
||||
<Icon />
|
||||
{view}
|
||||
|
||||
@@ -15,8 +15,7 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { UsableResource } from "@types";
|
||||
import { useToast } from "@ui/use-toast";
|
||||
import { useAtom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
@@ -175,6 +174,19 @@ export const useSetTitle = (more?: string) => {
|
||||
}, [title]);
|
||||
};
|
||||
|
||||
export const atomWithStorage = <T>(key: string, init: T) => {
|
||||
const stored = localStorage.getItem(key);
|
||||
const inner = atom(stored ? JSON.parse(stored) : init);
|
||||
|
||||
return atom(
|
||||
(get) => get(inner),
|
||||
(_, set, newValue) => {
|
||||
set(inner, newValue);
|
||||
localStorage.setItem(key, JSON.stringify(newValue));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const tagsAtom = atomWithStorage<string[]>("tags-v0", []);
|
||||
|
||||
export const useTagsFilter = () => {
|
||||
@@ -219,15 +231,17 @@ export const useCheckResourceExists = () => {
|
||||
|
||||
export const useFilterResources = <Info>(
|
||||
resources?: Types.ResourceListItem<Info>[],
|
||||
search?: string,
|
||||
search?: string
|
||||
) => {
|
||||
const tags = useTagsFilter();
|
||||
const searchSplit = search?.split(" ") || [];
|
||||
return resources?.filter(
|
||||
return (
|
||||
resources?.filter(
|
||||
(resource) =>
|
||||
tags.every((tag) => resource.tags.includes(tag)) &&
|
||||
(searchSplit.length > 0
|
||||
? searchSplit.every((search) => resource.name.includes(search))
|
||||
: true)
|
||||
) ?? [];
|
||||
) ?? []
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,7 @@ export const RESOURCE_TARGETS: UsableResource[] = [
|
||||
"Procedure",
|
||||
"Builder",
|
||||
"Alerter",
|
||||
"ServerTemplate"
|
||||
"ServerTemplate",
|
||||
];
|
||||
|
||||
export function env_to_text(envVars: Types.EnvironmentVar[] | undefined) {
|
||||
@@ -95,18 +95,18 @@ export const convertTsMsToLocalUnixTsInSecs = (ts: number) =>
|
||||
ts / 1000 - tzOffset;
|
||||
|
||||
export const usableResourcePath = (resource: UsableResource) => {
|
||||
if (resource === "ServerTemplate") return "server-templates"
|
||||
return `${resource.toLowerCase()}s`
|
||||
}
|
||||
if (resource === "ServerTemplate") return "server-templates";
|
||||
return `${resource.toLowerCase()}s`;
|
||||
};
|
||||
|
||||
export const sanitizeOnlySpan = (log: string) => {
|
||||
return sanitizeHtml(log, {
|
||||
allowedTags: ["span"],
|
||||
allowedAttributes: {
|
||||
"span": ["class"]
|
||||
span: ["class"],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const convert = new Convert();
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Router } from "@router";
|
||||
import { WebsocketProvider } from "@lib/socket";
|
||||
import { Toaster } from "@ui/toaster";
|
||||
import { atomWithStorage } from "@lib/hooks";
|
||||
|
||||
export const AUTH_TOKEN_STORAGE_KEY = "monitor-auth-token";
|
||||
|
||||
@@ -18,6 +19,13 @@ const query_client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
export type HomeView = "Dashboard" | "Tree" | "Resources";
|
||||
|
||||
export const homeViewAtom = atomWithStorage<HomeView>(
|
||||
"home-view-v1",
|
||||
"Dashboard"
|
||||
);
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
// <React.StrictMode>
|
||||
<QueryClientProvider client={query_client}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { homeViewAtom } from "@components/topbar";
|
||||
import { homeViewAtom } from "@main";
|
||||
import { useAtom } from "jotai";
|
||||
import { Dashboard } from "./dashboard";
|
||||
import { AllResources } from "./all_resources";
|
||||
|
||||
@@ -45,7 +45,7 @@ const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user