consolidate Resource components

This commit is contained in:
mbecker20
2024-04-07 16:08:23 -07:00
parent 76471fa694
commit 44784487a0
21 changed files with 289 additions and 225 deletions

View File

@@ -2,17 +2,18 @@ module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
'react-refresh/only-export-components': [
'warn',
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/no-explicit-any": "off",
},
}
};

View File

@@ -1,6 +1,6 @@
import { Section } from "@components/layouts";
import { ResourceComponents } from "@components/resources";
import { ResourceLink } from "@components/util";
import { ResourceLink } from "@components/resources/common";
import {
alert_level_intention,
text_color_class_by_intention,

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useRead, useWrite } from "@lib/hooks";
import { useRead } from "@lib/hooks";
import { Types } from "@monitor/client";
import {
Select,
@@ -11,15 +11,7 @@ import {
import { Button } from "@ui/button";
import { Input } from "@ui/input";
import { Switch } from "@ui/switch";
import {
CheckCircle,
ChevronsUpDown,
MinusCircle,
PlusCircle,
Save,
SearchX,
Trash,
} from "lucide-react";
import { CheckCircle, MinusCircle, PlusCircle, Save } from "lucide-react";
import { ReactNode, useState } from "react";
import { cn } from "@lib/utils";
import {
@@ -30,19 +22,8 @@ import {
DialogTitle,
DialogTrigger,
} from "@ui/dialog";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@ui/command";
import { snake_case_to_upper_space_case } from "@lib/formatting";
import { ActionWithDialog, ConfirmButton } from "@components/util";
import { UsableResource } from "@types";
import { useNavigate } from "react-router-dom";
import { ConfirmButton } from "@components/util";
export const ConfigItem = ({
label,
@@ -175,70 +156,6 @@ export const DoubleInput = <
);
};
type UsableResources = Exclude<Types.ResourceTarget["type"], "System">;
export const ResourceSelector = ({
type,
selected,
onSelect,
disabled,
}: {
type: UsableResources;
selected: string | undefined;
onSelect?: (id: string) => void;
disabled?: boolean;
}) => {
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
const resources = useRead(`List${type}s`, {}).data;
const name = resources?.find((r) => r.id === selected)?.name;
if (!resources) return null;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="secondary" className="flex gap-2" disabled={disabled}>
{name ?? `Select ${type}`}
{!disabled && <ChevronsUpDown className="w-3 h-3" />}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] max-h-[200px] p-0" sideOffset={12}>
<Command>
<CommandInput
placeholder={`Search ${type}s`}
className="h-9"
value={input}
onValueChange={setInput}
/>
<CommandList>
<CommandEmpty className="flex justify-evenly items-center">
{`No ${type}s Found`}
<SearchX className="w-3 h-3" />
</CommandEmpty>
<CommandGroup>
{resources.map((resource) => (
<CommandItem
key={resource.id}
onSelect={() => {
onSelect && onSelect(resource.id);
setOpen(false);
}}
className="flex items-center justify-between cursor-pointer"
>
<div className="p-1">{resource.name}</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export const AccountSelector = ({
id,
type,
@@ -403,34 +320,3 @@ export const SystemCommand = ({
</ConfigItem>
);
};
export const DeleteResource = ({
type,
id,
}: {
type: UsableResource;
id: string;
}) => {
const nav = useNavigate();
const resource = useRead(`Get${type}`, { [type.toLowerCase()]: id } as any).data;
const { mutateAsync, isPending } = useWrite(`Delete${type}`);
if (!resource) return null;
return (
<div className="flex items-center justify-between">
<div className="w-full">Delete {type}</div>
<ActionWithDialog
name={resource.name}
title="Delete"
icon={<Trash className="h-4 w-4" />}
onClick={async () => {
await mutateAsync({ id });
nav(`/${type.toLowerCase()}s`);
}}
disabled={isPending}
loading={isPending}
/>
</div>
);
};

View File

@@ -17,9 +17,8 @@ import { DataTable } from "@ui/data-table";
import { Link } from "react-router-dom";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
import { AlerterConfig } from "./config";
import { CopyResource, ResourceLink } from "@components/util";
import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { DeleteResource } from "@components/config/util";
import { CopyResource, DeleteResource, ResourceLink } from "../common";
const useAlerter = (id?: string) =>
useRead("ListAlerters", {}).data?.find((d) => d.id === id);

View File

@@ -2,7 +2,6 @@ import { Config } from "@components/config";
import {
AccountSelector,
ConfigItem,
ResourceSelector,
SystemCommand,
} from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
@@ -20,6 +19,7 @@ import {
import { Textarea } from "@ui/textarea";
import { MinusCircle, PlusCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { ResourceSelector } from "../common";
export const BuildConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuild", { build: id }).data?.config;

View File

@@ -1,5 +1,5 @@
import { NewResource, Section } from "@components/layouts";
import { ConfirmButton, CopyResource, ResourceLink } from "@components/util";
import { ConfirmButton } from "@components/util";
import { useExecute, useRead, useWrite } from "@lib/hooks";
import { RequiredResourceComponents } from "@types";
import { Input } from "@ui/input";
@@ -11,7 +11,7 @@ import { fill_color_class_by_intention } from "@lib/color";
import { BuildChart } from "./dashboard";
import { BuildTable } from "./table";
import { fmt_version } from "@lib/formatting";
import { DeleteResource } from "@components/config/util";
import { CopyResource, DeleteResource, ResourceLink } from "../common";
const useBuild = (id?: string) =>
useRead("ListBuilds", {}).data?.find((d) => d.id === id);

View File

@@ -1,8 +1,9 @@
import { Config } from "@components/config";
import { InputList, ResourceSelector } from "@components/config/util";
import { InputList } from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { useState } from "react";
import { ResourceSelector } from "../common";
export const BuilderConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuilder", { builder: id }).data?.config;

View File

@@ -18,8 +18,7 @@ import { Cloud, Bot, Factory, AlertTriangle } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { BuilderConfig } from "./config";
import { CopyResource, ResourceLink } from "@components/util";
import { DeleteResource } from "@components/config/util";
import { CopyResource, DeleteResource, ResourceLink } from "../common";
const useBuilder = (id?: string) =>
useRead("ListBuilders", {}).data?.find((d) => d.id === id);

View File

@@ -0,0 +1,253 @@
import { ActionButton, ActionWithDialog, ConfirmButton } from "@components/util";
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
import { UsableResource } from "@types";
import { Button } from "@ui/button";
import { Card, CardContent } from "@ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@ui/command";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@ui/dialog";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import { Textarea } from "@ui/textarea";
import { Check, CheckCircle, ChevronsUpDown, Copy, SearchX, Trash } from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { ResourceComponents } from ".";
import { Input } from "@ui/input";
export const ResourceDescription = ({
type,
id,
}: {
type: UsableResource;
id: string;
}) => {
const [open, setOpen] = useState(false);
const [description, setDescription] = useState<string>();
const { mutate: update_description } = useWrite("UpdateDescription");
const resource = useRead(`Get${type}`, {
[type.toLowerCase()]: id,
} as any).data;
useEffect(
() => setDescription(resource?.description),
[resource?.description]
);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Card>
<CardContent className="text-muted-foreground">
{resource?.description}
</CardContent>
</Card>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Update Description</DialogTitle>
</DialogHeader>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<DialogFooter>
<ConfirmButton
title="Update"
icon={<CheckCircle className="w-4 h-4" />}
onClick={() => {
update_description({
target: { type, id },
description: description!,
});
setOpen(false);
}}
disabled={description !== undefined}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export const ResourceSelector = ({
type,
selected,
onSelect,
disabled,
}: {
type: UsableResource;
selected: string | undefined;
onSelect?: (id: string) => void;
disabled?: boolean;
}) => {
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
const resources = useRead(`List${type}s`, {}).data;
const name = resources?.find((r) => r.id === selected)?.name;
if (!resources) return null;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="secondary" className="flex gap-2" disabled={disabled}>
{name ?? `Select ${type}`}
{!disabled && <ChevronsUpDown className="w-3 h-3" />}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] max-h-[200px] p-0" sideOffset={12}>
<Command>
<CommandInput
placeholder={`Search ${type}s`}
className="h-9"
value={input}
onValueChange={setInput}
/>
<CommandList>
<CommandEmpty className="flex justify-evenly items-center">
{`No ${type}s Found`}
<SearchX className="w-3 h-3" />
</CommandEmpty>
<CommandGroup>
{resources.map((resource) => (
<CommandItem
key={resource.id}
onSelect={() => {
onSelect && onSelect(resource.id);
setOpen(false);
}}
className="flex items-center justify-between cursor-pointer"
>
<div className="p-1">{resource.name}</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export const ResourceLink = ({
type,
id,
}: {
type: UsableResource;
id: string;
}) => {
const Components = ResourceComponents[type];
return (
<Link to={`/${type.toLowerCase()}s/${id}`}>
<Button variant="link" className="flex gap-2 items-center p-0">
<Components.Icon id={id} />
<Components.Name id={id} />
</Button>
</Link>
);
};
export const CopyResource = ({
id,
disabled,
type,
}: {
id: string;
disabled?: boolean;
type: Exclude<UsableResource, "Server">;
}) => {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const nav = useNavigate();
const inv = useInvalidate();
const { mutate } = useWrite(`Copy${type}`, {
onSuccess: (res) => {
inv([`List${type}s`]);
nav(`/${type.toLowerCase()}s/${res._id?.$oid}`);
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<ActionButton
title="Copy"
icon={<Copy className="w-4 h-4" />}
disabled={disabled}
onClick={() => setOpen(true)}
/>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Copy {type}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 my-4">
<p>Provide a name for the newly created {type.toLowerCase()}.</p>
<Input value={name} onChange={(e) => setName(e.target.value)} />
</div>
<DialogFooter>
<ConfirmButton
title="Copy"
icon={<Check className="w-4 h-4" />}
disabled={!name}
onClick={() => {
mutate({ id, name });
setOpen(false);
}}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export const DeleteResource = ({
type,
id,
}: {
type: UsableResource;
id: string;
}) => {
const nav = useNavigate();
const resource = useRead(`Get${type}`, {
[type.toLowerCase()]: id,
} as any).data;
const { mutateAsync, isPending } = useWrite(`Delete${type}`);
if (!resource) return null;
return (
<div className="flex items-center justify-between">
<div className="w-full">Delete {type}</div>
<ActionWithDialog
name={resource.name}
title="Delete"
icon={<Trash className="h-4 w-4" />}
onClick={async () => {
await mutateAsync({ id });
nav(`/${type.toLowerCase()}s`);
}}
disabled={isPending}
loading={isPending}
/>
</div>
);
};

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ConfigItem, ResourceSelector } from "@components/config/util";
import { ConfigItem } from "@components/config/util";
import { ResourceSelector } from "@components/resources/common";
import { fmt_version } from "@lib/formatting";
import { useRead } from "@lib/hooks";
import { Types } from "@monitor/client";

View File

@@ -5,7 +5,6 @@ import {
AccountSelector,
ConfigInput,
ConfigItem,
ResourceSelector,
} from "@components/config/util";
import { ImageConfig } from "./components/image";
import { RestartModeSelector } from "./components/restart";
@@ -20,6 +19,7 @@ import {
TermSignalLabels,
TerminationTimeout,
} from "./components/term-signal";
import { ResourceSelector } from "@components/resources/common";
export const ServerSelector = ({
selected,

View File

@@ -24,8 +24,8 @@ import {
text_color_class_by_intention,
} from "@lib/color";
import { DeploymentTable } from "./table";
import { CopyResource, ResourceLink } from "@components/util";
import { DeploymentsChart } from "./dashboard";
import { CopyResource, ResourceLink } from "../common";
export const useDeployment = (id?: string) =>
useRead("ListDeployments", {}, { refetchInterval: 5000 }).data?.find(

View File

@@ -1,5 +1,4 @@
import { ConfigLayout } from "@components/config";
import { ResourceSelector } from "@components/config/util";
import { ConfirmButton } from "@components/util";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
@@ -40,6 +39,7 @@ import {
Trash2,
} from "lucide-react";
import { useState } from "react";
import { ResourceSelector } from "../common";
export const ProcedureConfig = ({ id }: { id: string }) => {
const procedure = useRead("GetProcedure", { procedure: id }).data;

View File

@@ -1,5 +1,5 @@
import { NewResource, Section } from "@components/layouts";
import { ConfirmButton, CopyResource, ResourceLink } from "@components/util";
import { ConfirmButton } from "@components/util";
import { useExecute, useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types";
@@ -17,7 +17,7 @@ import { useState } from "react";
import { Link } from "react-router-dom";
import { ProcedureConfig } from "./config";
import { ProcedureTable } from "./table";
import { DeleteResource } from "@components/config/util";
import { CopyResource, DeleteResource, ResourceLink } from "../common";
const useProcedure = (id?: string) =>
useRead("ListProcedures", {}).data?.find((d) => d.id === id);

View File

@@ -1,7 +1,6 @@
import { Config } from "@components/config";
import {
ConfigItem,
ResourceSelector,
SystemCommand,
} from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
@@ -14,6 +13,7 @@ import {
SelectValue,
} from "@ui/select";
import { useState } from "react";
import { ResourceSelector } from "../common";
export const RepoConfig = ({ id }: { id: string }) => {
const config = useRead("GetRepo", { repo: id }).data?.config;

View File

@@ -6,12 +6,11 @@ import { DataTable } from "@ui/data-table";
import { AlertTriangle, GitBranch } from "lucide-react";
import { Link } from "react-router-dom";
import { RepoConfig } from "./config";
import { CopyResource, ResourceLink } from "@components/util";
import { useState } from "react";
import { NewResource, Section } from "@components/layouts";
import { Input } from "@ui/input";
import { CloneRepo, PullRepo } from "./actions";
import { DeleteResource } from "@components/config/util";
import { CopyResource, DeleteResource, ResourceLink } from "../common";
const useRepo = (id?: string) =>
useRead("ListRepos", {}).data?.find((d) => d.id === id);

View File

@@ -16,10 +16,9 @@ import { ServerConfig } from "./config";
import { DeploymentTable } from "../deployment/table";
import { ServerTable } from "./table";
import { ServersChart } from "./dashboard";
import { ResourceLink } from "@components/util";
import { Link } from "react-router-dom";
import { Button } from "@ui/button";
import { DeleteResource } from "@components/config/util";
import { DeleteResource, ResourceLink } from "../common";
export const useServer = (id?: string) =>
useRead("ListServers", {}).data?.find((d) => d.id === id);

View File

@@ -19,16 +19,13 @@ import {
} from "@ui/dialog";
import { toast, useToast } from "@ui/use-toast";
import { cn } from "@lib/utils";
import { useInvalidate, useWrite } from "@lib/hooks";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@ui/dropdown-menu";
import { AUTH_TOKEN_STORAGE_KEY } from "@main";
import { UsableResource } from "@types";
import { ResourceComponents } from "./resources";
export const WithLoading = ({
children,
@@ -166,61 +163,6 @@ export const ActionWithDialog = ({
);
};
export const CopyResource = ({
id,
disabled,
type,
}: {
id: string;
disabled?: boolean;
type: Exclude<UsableResource, "Server">;
}) => {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const nav = useNavigate();
const inv = useInvalidate();
const { mutate } = useWrite(`Copy${type}`, {
onSuccess: (res) => {
inv([`List${type}s`]);
nav(`/${type.toLowerCase()}s/${res._id?.$oid}`);
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<ActionButton
title="Copy"
icon={<Copy className="w-4 h-4" />}
disabled={disabled}
onClick={() => setOpen(true)}
/>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Copy {type}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 my-4">
<p>Provide a name for the newly created {type.toLowerCase()}.</p>
<Input value={name} onChange={(e) => setName(e.target.value)} />
</div>
<DialogFooter>
<ConfirmButton
title="Copy"
icon={<Check className="w-4 h-4" />}
disabled={!name}
onClick={() => {
mutate({ id, name });
setOpen(false);
}}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export const ConfirmButton = ({
variant,
size,
@@ -336,21 +278,3 @@ export const CopyButton = ({ content }: { content: string | undefined }) => {
</Button>
);
};
export const ResourceLink = ({
type,
id,
}: {
type: UsableResource;
id: string;
}) => {
const Components = ResourceComponents[type];
return (
<Link to={`/${type.toLowerCase()}s/${id}`}>
<Button variant="link" className="flex gap-2 items-center p-0">
<Components.Icon id={id} />
<Components.Name id={id} />
</Button>
</Link>
);
};

View File

@@ -13,7 +13,7 @@ import { ApiKeysSummary } from "@components/dashboard/api-keys";
import { ResourceComponents } from "@components/resources";
import { OpenAlerts } from "@components/alert";
import { useUser } from "@lib/hooks";
import { ResourceLink } from "@components/util";
import { ResourceLink } from "@components/resources/common";
export const Dashboard = () => {
return (

View File

@@ -1,5 +1,6 @@
import { Page, Section } from "@components/layouts";
import { ConfirmButton, ResourceLink } from "@components/util";
import { ResourceLink } from "@components/resources/common";
import { ConfirmButton } from "@components/util";
import { text_color_class_by_intention } from "@lib/color";
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";

View File

@@ -19,6 +19,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": false,
/* Paths */
"baseUrl": "./src",