forked from github-starred/komodo
split sidebar home
This commit is contained in:
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user