forked from github-starred/komodo
add a god damn sidebar
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
|
||||
<title>Monitor</title>
|
||||
</head>
|
||||
<body class="min-h-screen pb-12">
|
||||
<body class="min-h-screen">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@radix-ui/react-popover": "1.0.7",
|
||||
"@radix-ui/react-progress": "1.0.3",
|
||||
"@radix-ui/react-select": "2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "1.0.2",
|
||||
"@radix-ui/react-switch": "1.0.3",
|
||||
"@radix-ui/react-tabs": "1.0.4",
|
||||
|
||||
@@ -144,7 +144,7 @@ export const Config = <T,>({
|
||||
<CardTitle>{snake_case_to_upper_space_case(k)}</CardTitle>
|
||||
</CardHeader>
|
||||
)}
|
||||
<CardContent className="flex flex-col gap-4 mt-4">
|
||||
<CardContent className="flex flex-col gap-2 mt-4">
|
||||
<ConfigAgain
|
||||
config={config}
|
||||
update={update}
|
||||
|
||||
@@ -227,7 +227,10 @@ export const InputList = <T extends { [key: string]: unknown }>({
|
||||
disabled: boolean;
|
||||
set: (update: Partial<T>) => void;
|
||||
}) => (
|
||||
<ConfigItem label={field as string} className="items-start">
|
||||
<ConfigItem
|
||||
label={field as string}
|
||||
className={values.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-4 w-full max-w-[400px]">
|
||||
{values.map((arg, i) => (
|
||||
<div className="w-full flex gap-4" key={i}>
|
||||
@@ -242,7 +245,7 @@ export const InputList = <T extends { [key: string]: unknown }>({
|
||||
/>
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
set({
|
||||
[field]: [...values.filter((_, idx) => idx !== i)],
|
||||
@@ -257,7 +260,7 @@ export const InputList = <T extends { [key: string]: unknown }>({
|
||||
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={() => set({ [field]: [...values, ""] } as Partial<T>)}
|
||||
>
|
||||
Add {snake_case_to_upper_space_case(field as string).slice(0, -1)}
|
||||
|
||||
@@ -16,12 +16,20 @@ import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@ui/card";
|
||||
import { ResourceTags } from "./tags";
|
||||
import { Topbar } from "./topbar";
|
||||
import { usableResourcePath } from "@lib/utils";
|
||||
import { Sidebar } from "./sidebar";
|
||||
|
||||
export const Layout = () => {
|
||||
return (
|
||||
<>
|
||||
<Topbar />
|
||||
<div className="flex gap-2">
|
||||
<Sidebar />
|
||||
<div className="w-full h-[calc(100vh-70px)] overflow-y-auto">
|
||||
<div className="pb-24">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -213,7 +213,10 @@ const ExtraArgs = ({
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<ConfigItem label="Extra Args" className="items-start">
|
||||
<ConfigItem
|
||||
label="Extra Args"
|
||||
className={args.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-4 w-full max-w-[400px]">
|
||||
{args.map((arg, i) => (
|
||||
<div className="w-full flex gap-4" key={i}>
|
||||
|
||||
@@ -284,7 +284,10 @@ export const LabelsConfig = ({
|
||||
set: (input: Partial<Types.DeploymentConfig | Types.BuildConfig>) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<ConfigItem label="Labels" className="items-start">
|
||||
<ConfigItem
|
||||
label="Labels"
|
||||
className={labels.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<DoubleInput
|
||||
disabled={disabled}
|
||||
values={labels}
|
||||
|
||||
@@ -25,7 +25,10 @@ export const ExtraArgs = ({
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<ConfigItem label="Extra Args" className="items-start">
|
||||
<ConfigItem
|
||||
label="Extra Args"
|
||||
className={args.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-4 w-full max-w-[400px]">
|
||||
{args.map((arg, i) => (
|
||||
<div className="w-full flex gap-4" key={i}>
|
||||
|
||||
@@ -10,7 +10,10 @@ export const PortsConfig = ({
|
||||
set: (input: Partial<Types.DeploymentConfig>) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<ConfigItem label="Ports" className="items-start">
|
||||
<ConfigItem
|
||||
label="Ports"
|
||||
className={ports.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<DoubleInput
|
||||
disabled={disabled}
|
||||
values={ports}
|
||||
|
||||
@@ -10,7 +10,10 @@ export const VolumesConfig = ({
|
||||
set: (input: Partial<Types.DeploymentConfig>) => void;
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<ConfigItem label="Volumes" className="items-start">
|
||||
<ConfigItem
|
||||
label="Volumes"
|
||||
className={volumes.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<DoubleInput
|
||||
disabled={disabled}
|
||||
inputClassName="w-[300px] max-w-full"
|
||||
|
||||
@@ -66,7 +66,10 @@ export const AwsServerTemplateConfig = ({ id }: { id: string }) => {
|
||||
),
|
||||
volumes: (volumes, set) => {
|
||||
return (
|
||||
<ConfigItem label="EBS Volumes" className="items-start">
|
||||
<ConfigItem
|
||||
label="EBS Volumes"
|
||||
className={volumes.length > 0 ? "items-start" : undefined}
|
||||
>
|
||||
<div className="flex flex-col gap-4 w-full max-w-[400px]">
|
||||
{volumes.map((_, index) => (
|
||||
<div
|
||||
@@ -81,7 +84,7 @@ export const AwsServerTemplateConfig = ({ id }: { id: string }) => {
|
||||
/>
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
set({
|
||||
@@ -96,7 +99,7 @@ export const AwsServerTemplateConfig = ({ id }: { id: string }) => {
|
||||
))}
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
set({
|
||||
volumes: [...volumes, newVolume(volumes.length)],
|
||||
|
||||
93
frontend/src/components/sidebar.tsx
Normal file
93
frontend/src/components/sidebar.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
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 { Link, useLocation } from "react-router-dom";
|
||||
import { ResourceComponents } from "./resources";
|
||||
import { Separator } from "@ui/separator";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const Sidebar = () => {
|
||||
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" />} />
|
||||
|
||||
<Separator />
|
||||
|
||||
{RESOURCE_TARGETS.map((type) => {
|
||||
const RTIcon = ResourceComponents[type].Icon;
|
||||
const name = type === "ServerTemplate" ? "Template" : type;
|
||||
return (
|
||||
<SidebarLink
|
||||
key={type}
|
||||
label={`${name}s`}
|
||||
to={`/${usableResourcePath(type)}`}
|
||||
icon={<RTIcon />}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Separator />
|
||||
|
||||
<SidebarLink
|
||||
label="Alerts"
|
||||
to="/alerts"
|
||||
icon={<AlertTriangle className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Updates"
|
||||
to="/updates"
|
||||
icon={<Bell className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Tags"
|
||||
to="/tags"
|
||||
icon={<Tag className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<SidebarLink
|
||||
label="Api Keys"
|
||||
to="/keys"
|
||||
icon={<Box className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Users"
|
||||
to="/users"
|
||||
icon={<UserCircle2 className="w-4 h-4" />}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarLink = ({
|
||||
to,
|
||||
icon,
|
||||
label,
|
||||
}: {
|
||||
to: string;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
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"
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRead, useResourceParamType } from "@lib/hooks";
|
||||
import { useRead, useResourceParamType, useUser } from "@lib/hooks";
|
||||
import { ResourceComponents } from "./resources";
|
||||
import {
|
||||
AlertTriangle,
|
||||
@@ -34,7 +34,7 @@ import { UsableResource } from "@types";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { useAtom } from "jotai";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
|
||||
import { useState } from "react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -47,10 +47,10 @@ import { ResourceLink } from "./resources/common";
|
||||
|
||||
export const Topbar = () => {
|
||||
return (
|
||||
<div className="sticky top-0 border-b z-50 w-full bg-card text-card-foreground shadow">
|
||||
<div className="sticky top-0 h-[70px] border-b z-50 w-full bg-card text-card-foreground shadow flex items-center">
|
||||
<div className="container flex items-center justify-between py-4 gap-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to={"/"} className="text-2xl tracking-widest">
|
||||
<Link to={"/"} className="text-2xl tracking-widest mx-6">
|
||||
MONITOR
|
||||
</Link>
|
||||
<div className="flex gap-2">
|
||||
@@ -74,6 +74,8 @@ export const Topbar = () => {
|
||||
};
|
||||
|
||||
const PrimaryDropdown = () => {
|
||||
const user = useUser().data;
|
||||
|
||||
const type = useResourceParamType();
|
||||
const Components = type && ResourceComponents[type];
|
||||
|
||||
@@ -102,7 +104,7 @@ const PrimaryDropdown = () => {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<DropdownMenuTrigger asChild className="lg:hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex justify-start items-center gap-2 w-36 px-3"
|
||||
@@ -113,12 +115,11 @@ const PrimaryDropdown = () => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-36" side="bottom">
|
||||
<DropdownMenuGroup>
|
||||
<Link to="/">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<Home className="w-4 h-4" />
|
||||
Home
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
label="Home"
|
||||
icon={<Home className="w-4 h-4" />}
|
||||
to="/"
|
||||
/>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -126,58 +127,75 @@ const PrimaryDropdown = () => {
|
||||
const RTIcon = ResourceComponents[type].Icon;
|
||||
const name = type === "ServerTemplate" ? "Template" : type;
|
||||
return (
|
||||
<Link key={type} to={`/${usableResourcePath(type)}`}>
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<RTIcon />
|
||||
{name}s
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
key={type}
|
||||
label={`${name}s`}
|
||||
icon={<RTIcon />}
|
||||
to={`/${usableResourcePath(type)}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<Link to="/alerts">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
Alerts
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
label="Alerts"
|
||||
icon={<AlertTriangle className="w-4 h-4" />}
|
||||
to="/alerts"
|
||||
/>
|
||||
|
||||
<Link to="/updates">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<Bell className="w-4 h-4" />
|
||||
Updates
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
label="Updates"
|
||||
icon={<Bell className="w-4 h-4" />}
|
||||
to="/updates"
|
||||
/>
|
||||
|
||||
<Link to="/tags">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<Tag className="w-4 h-4" />
|
||||
Tags
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
label="Tags"
|
||||
icon={<Tag className="w-4 h-4" />}
|
||||
to="/tags"
|
||||
/>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<Link to="/keys">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<Box className="w-4 h-4" />
|
||||
Api Keys
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link to="/users">
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
<UserCircle2 className="w-4 h-4" />
|
||||
Users
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownLinkItem
|
||||
label="Api Keys"
|
||||
icon={<Box className="w-4 h-4" />}
|
||||
to="/keys"
|
||||
/>
|
||||
|
||||
{user?.admin && (
|
||||
<DropdownLinkItem
|
||||
label="Users"
|
||||
icon={<UserCircle2 className="w-4 h-4" />}
|
||||
to="/users"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownLinkItem = ({
|
||||
label,
|
||||
icon,
|
||||
to,
|
||||
}: {
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
to: string;
|
||||
}) => {
|
||||
return (
|
||||
<Link to={to}>
|
||||
<DropdownMenuItem className="flex items-center gap-2 cursor-pointer">
|
||||
{icon}
|
||||
{label}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export type HomeView = "Dashboard" | "Tree" | "Resources";
|
||||
|
||||
export const homeViewAtom = atomWithStorage<HomeView>(
|
||||
@@ -260,7 +278,7 @@ const ResourcesDropdown = ({ type }: { type: UsableResource }) => {
|
||||
{selected ? selected.name : `All ${type}s`}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] max-h-[400px] p-0" sideOffset={12}>
|
||||
<PopoverContent className="w-[300px] max-h-[400px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder={`Search ${type}s`}
|
||||
|
||||
@@ -14,11 +14,11 @@ export const object_keys = <T extends object>(o: T): (keyof T)[] =>
|
||||
Object.keys(o) as (keyof T)[];
|
||||
|
||||
export const RESOURCE_TARGETS: UsableResource[] = [
|
||||
"Deployment",
|
||||
"Server",
|
||||
"Deployment",
|
||||
"Build",
|
||||
"Procedure",
|
||||
"Repo",
|
||||
"Procedure",
|
||||
"Builder",
|
||||
"Alerter",
|
||||
"ServerTemplate"
|
||||
|
||||
29
frontend/src/ui/separator.tsx
Normal file
29
frontend/src/ui/separator.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
@@ -13,7 +13,7 @@ module.exports = {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
"2xl": "w-full",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
|
||||
@@ -825,6 +825,14 @@
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.5"
|
||||
|
||||
"@radix-ui/react-separator@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.0.3.tgz#be5a931a543d5726336b112f465f58585c04c8aa"
|
||||
integrity sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-slot@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab"
|
||||
|
||||
Reference in New Issue
Block a user