split sidebar home

This commit is contained in:
mbecker20
2024-05-12 12:59:45 -07:00
parent 1ff21d2986
commit 1ba288be79
10 changed files with 108 additions and 40 deletions

View File

@@ -3,11 +3,10 @@ import {
alert_level_intention, alert_level_intention,
text_color_class_by_intention, text_color_class_by_intention,
} from "@lib/color"; } from "@lib/color";
import { useRead } from "@lib/hooks"; import { useRead, atomWithStorage } from "@lib/hooks";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { Button } from "@ui/button"; import { Button } from "@ui/button";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { AlertsTable } from "./table"; import { AlertsTable } from "./table";

View File

@@ -1,7 +1,7 @@
import { Button } from "@ui/button"; import { Button } from "@ui/button";
import { PlusCircle } from "lucide-react"; import { PlusCircle } from "lucide-react";
import { ReactNode, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { Link, Outlet } from "react-router-dom"; import { Link, Outlet, useNavigate } from "react-router-dom";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -19,6 +19,21 @@ import { usableResourcePath } from "@lib/utils";
import { Sidebar } from "./sidebar"; import { Sidebar } from "./sidebar";
export const Layout = () => { 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 ( return (
<> <>
<Topbar /> <Topbar />

View File

@@ -1,6 +1,6 @@
import { atomWithStorage } from "@lib/hooks";
import { Types } from "@monitor/client"; import { Types } from "@monitor/client";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
const statsGranularityAtom = atomWithStorage( const statsGranularityAtom = atomWithStorage(
"stats-granularity-v0", "stats-granularity-v0",

View File

@@ -1,17 +1,49 @@
import { RESOURCE_TARGETS, cn, usableResourcePath } from "@lib/utils"; import { RESOURCE_TARGETS, cn, usableResourcePath } from "@lib/utils";
import { Button } from "@ui/button"; import { Button } from "@ui/button";
import { Card, CardContent } from "@ui/card"; 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 { Link, useLocation } from "react-router-dom";
import { ResourceComponents } from "./resources"; import { ResourceComponents } from "./resources";
import { Separator } from "@ui/separator"; import { Separator } from "@ui/separator";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { useAtom } from "jotai";
import { homeViewAtom } from "@main";
export const Sidebar = () => { export const Sidebar = () => {
const [view, setView] = useAtom(homeViewAtom);
console.log(view);
return ( return (
<Card className="h-fit m-4 hidden lg:flex"> <Card className="h-fit m-4 hidden lg:flex">
<CardContent className="h-fit grid gap-2 px-6 py-4"> <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 /> <Separator />
@@ -70,20 +102,27 @@ const SidebarLink = ({
to, to,
icon, icon,
label, label,
onClick,
highlighted,
}: { }: {
to: string; to: string;
icon: ReactNode; icon: ReactNode;
label: string; label: string;
onClick?: () => void;
highlighted?: boolean;
}) => { }) => {
const location = useLocation(); const location = useLocation();
const hl =
"/" + location.pathname.split("/")[1] === to && (highlighted ?? true);
return ( return (
<Link to={to} className="w-full"> <Link to={to} className="w-full">
<Button <Button
variant="link" variant="link"
className={cn( className={cn(
"flex justify-start items-center gap-2 w-full hover:bg-accent", "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} {icon}
{label} {label}

View File

@@ -31,7 +31,6 @@ import { TopbarUpdates } from "./updates/topbar";
import { Logout } from "./util"; import { Logout } from "./util";
import { ThemeToggle } from "@ui/theme"; import { ThemeToggle } from "@ui/theme";
import { UsableResource } from "@types"; import { UsableResource } from "@types";
import { atomWithStorage } from "jotai/utils";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
@@ -44,11 +43,12 @@ import {
CommandList, CommandList,
} from "@ui/command"; } from "@ui/command";
import { ResourceLink } from "./resources/common"; import { ResourceLink } from "./resources/common";
import { HomeView, homeViewAtom } from "@main";
export const Topbar = () => { export const Topbar = () => {
const [omniOpen, setOmniOpen] = useState(false); const [omniOpen, setOmniOpen] = useState(false);
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const keydown = (e: KeyboardEvent) => {
// This will ignore Shift + S if it is sent from input / textarea // This will ignore Shift + S if it is sent from input / textarea
const target = e.target as any; const target = e.target as any;
if (target.matches("input") || target.matches("textarea")) return; if (target.matches("input") || target.matches("textarea")) return;
@@ -58,8 +58,8 @@ export const Topbar = () => {
setOmniOpen(true); setOmniOpen(true);
} }
}; };
document.addEventListener("keydown", down); document.addEventListener("keydown", keydown);
return () => document.removeEventListener("keydown", down); return () => document.removeEventListener("keydown", keydown);
}); });
return ( 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="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 = { const ICONS = {
Dashboard: () => <Box className="w-4 h-4" />, Dashboard: () => <Box className="w-4 h-4" />,
Tree: () => <FolderTree className="w-4 h-4" />,
Resources: () => <Boxes className="w-4 h-4" />, Resources: () => <Boxes className="w-4 h-4" />,
Tree: () => <FolderTree className="w-4 h-4" />,
}; };
const SecondaryDropdown = () => { const SecondaryDropdown = () => {
@@ -236,7 +229,7 @@ const SecondaryDropdown = () => {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="ghost" 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 /> <Icon />
{view} {view}

View File

@@ -15,8 +15,7 @@ import {
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { UsableResource } from "@types"; import { UsableResource } from "@types";
import { useToast } from "@ui/use-toast"; import { useToast } from "@ui/use-toast";
import { useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { useEffect } from "react"; import { useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
@@ -175,6 +174,19 @@ export const useSetTitle = (more?: string) => {
}, [title]); }, [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 tagsAtom = atomWithStorage<string[]>("tags-v0", []);
export const useTagsFilter = () => { export const useTagsFilter = () => {
@@ -219,15 +231,17 @@ export const useCheckResourceExists = () => {
export const useFilterResources = <Info>( export const useFilterResources = <Info>(
resources?: Types.ResourceListItem<Info>[], resources?: Types.ResourceListItem<Info>[],
search?: string, search?: string
) => { ) => {
const tags = useTagsFilter(); const tags = useTagsFilter();
const searchSplit = search?.split(" ") || []; const searchSplit = search?.split(" ") || [];
return resources?.filter( return (
(resource) => resources?.filter(
tags.every((tag) => resource.tags.includes(tag)) && (resource) =>
(searchSplit.length > 0 tags.every((tag) => resource.tags.includes(tag)) &&
? searchSplit.every((search) => resource.name.includes(search)) (searchSplit.length > 0
: true) ? searchSplit.every((search) => resource.name.includes(search))
) ?? []; : true)
) ?? []
);
}; };

View File

@@ -21,7 +21,7 @@ export const RESOURCE_TARGETS: UsableResource[] = [
"Procedure", "Procedure",
"Builder", "Builder",
"Alerter", "Alerter",
"ServerTemplate" "ServerTemplate",
]; ];
export function env_to_text(envVars: Types.EnvironmentVar[] | undefined) { export function env_to_text(envVars: Types.EnvironmentVar[] | undefined) {
@@ -95,18 +95,18 @@ export const convertTsMsToLocalUnixTsInSecs = (ts: number) =>
ts / 1000 - tzOffset; ts / 1000 - tzOffset;
export const usableResourcePath = (resource: UsableResource) => { export const usableResourcePath = (resource: UsableResource) => {
if (resource === "ServerTemplate") return "server-templates" if (resource === "ServerTemplate") return "server-templates";
return `${resource.toLowerCase()}s` return `${resource.toLowerCase()}s`;
} };
export const sanitizeOnlySpan = (log: string) => { export const sanitizeOnlySpan = (log: string) => {
return sanitizeHtml(log, { return sanitizeHtml(log, {
allowedTags: ["span"], allowedTags: ["span"],
allowedAttributes: { allowedAttributes: {
"span": ["class"] span: ["class"],
}, },
}); });
} };
const convert = new Convert(); const convert = new Convert();
/** /**
@@ -123,4 +123,4 @@ export const logToHtml = (log: string) => {
allowedAttributes: sanitizeHtml.defaults.allowedAttributes, allowedAttributes: sanitizeHtml.defaults.allowedAttributes,
}); });
return convert.toHtml(sanitized); return convert.toHtml(sanitized);
}; };

View File

@@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Router } from "@router"; import { Router } from "@router";
import { WebsocketProvider } from "@lib/socket"; import { WebsocketProvider } from "@lib/socket";
import { Toaster } from "@ui/toaster"; import { Toaster } from "@ui/toaster";
import { atomWithStorage } from "@lib/hooks";
export const AUTH_TOKEN_STORAGE_KEY = "monitor-auth-token"; export const AUTH_TOKEN_STORAGE_KEY = "monitor-auth-token";
@@ -18,6 +19,13 @@ const query_client = new QueryClient({
defaultOptions: { queries: { retry: false } }, 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( ReactDOM.createRoot(document.getElementById("root")!).render(
// <React.StrictMode> // <React.StrictMode>
<QueryClientProvider client={query_client}> <QueryClientProvider client={query_client}>

View File

@@ -1,4 +1,4 @@
import { homeViewAtom } from "@components/topbar"; import { homeViewAtom } from "@main";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { Dashboard } from "./dashboard"; import { Dashboard } from "./dashboard";
import { AllResources } from "./all_resources"; import { AllResources } from "./all_resources";

View File

@@ -45,7 +45,7 @@ const CardDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<p <div
ref={ref} ref={ref}
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}