This commit is contained in:
mbecker20
2024-03-29 03:35:22 -07:00
parent f8237217b0
commit 1510ed2147
18 changed files with 362 additions and 277 deletions

View File

@@ -20,7 +20,9 @@ import { Fragment, ReactNode, SetStateAction, useState } from "react";
const keys = <T extends Record<string, unknown>>(obj: T) =>
Object.keys(obj) as Array<keyof T>;
export const ConfigLayout = <T extends Types.Resource<unknown, unknown>["config"]>({
export const ConfigLayout = <
T extends Types.Resource<unknown, unknown>["config"]
>({
config,
children,
onConfirm,
@@ -46,10 +48,12 @@ export const ConfigLayout = <T extends Types.Resource<unknown, unknown>["config"
>
<History className="w-4 h-4" />
</Button>
{Object.keys(config).length ? <ConfirmUpdate
content={JSON.stringify(config, null, 2)}
onConfirm={onConfirm}
/> : null}
{Object.keys(config).length ? (
<ConfirmUpdate
content={JSON.stringify(config, null, 2)}
onConfirm={onConfirm}
/>
) : null}
</div>
}
>
@@ -200,4 +204,4 @@ export const ConfigAgain = <
})}
</>
);
};
};

View File

@@ -19,7 +19,7 @@ import {
SearchX,
} from "lucide-react";
import { ReactNode, useState } from "react";
import { cn } from "@lib/utils";
import { cn, snake_case_to_upper_space_case } from "@lib/utils";
import {
Dialog,
DialogContent,
@@ -52,7 +52,7 @@ export const ConfigItem = ({
className
)}
>
<div className="capitalize"> {label} </div>
<div className="capitalize"> {snake_case_to_upper_space_case(label)} </div>
{children}
</div>
);
@@ -295,7 +295,7 @@ export const InputList = <T extends { [key: string]: unknown }>({
// intent="success"
onClick={() => set({ [field]: [...values, ""] } as Partial<T>)}
>
Add Docker Account
Add {snake_case_to_upper_space_case(field as string).slice(0, -1)}
</Button>
</div>
</ConfigItem>

View File

@@ -94,7 +94,7 @@ export const Page = ({
);
interface SectionProps {
title: string;
title: ReactNode;
children: ReactNode;
icon?: ReactNode;
actions?: ReactNode;
@@ -129,7 +129,7 @@ export const NewResource = ({
return (
<Dialog open={open} onOpenChange={set}>
<DialogTrigger asChild>
<Button className="items-center gap-2">
<Button variant="secondary" className="items-center gap-2">
New {type} <PlusCircle className="w-4 h-4" />
</Button>
</DialogTrigger>

View File

@@ -15,6 +15,9 @@ import { useNavigate } from "react-router-dom";
import { ResourceComponents } from "./resources";
import { UsableResource } from "@types";
import { RESOURCE_TARGETS } from "@lib/utils";
import { DeploymentComponents } from "./resources/deployment";
import { BuildComponents } from "./resources/build";
import { ServerComponents } from "./resources/server";
const ResourceGroup = ({
type,
@@ -26,6 +29,8 @@ const ResourceGroup = ({
const data = useRead(`List${type}s`, {}).data;
const Components = ResourceComponents[type];
if (!data || !data.length) return
return (
<CommandGroup heading={`${type}s`}>
{data?.map(({ id }) => {
@@ -54,9 +59,10 @@ export const Omnibar = () => {
useEffect(() => {
const down = (e: KeyboardEvent) => {
console.log(e);
// 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 === "S") {
e.preventDefault();
set(true);
@@ -90,8 +96,31 @@ export const Omnibar = () => {
<Home className="w-4 h-4" />
Home
</CommandItem>
<CommandItem
className="flex items-center gap-2"
onSelect={() => nav("/deployments")}
>
<DeploymentComponents.Icon />
Deployments
</CommandItem>
<CommandItem
className="flex items-center gap-2"
onSelect={() => nav("/builds")}
>
<BuildComponents.Icon />
Builds
</CommandItem>
<CommandItem
className="flex items-center gap-2"
onSelect={() => nav("/servers")}
>
<ServerComponents.Icon />
Servers
</CommandItem>
</CommandGroup>
<CommandSeparator />
{RESOURCE_TARGETS.map((rt) => (
<Fragment key={rt}>
<ResourceGroup type={rt} key={rt} onSelect={nav} />

View File

@@ -17,7 +17,6 @@ import { Config } from "@components/config";
import { DataTable } from "@ui/data-table";
import { ResourceComponents } from "..";
import { Link } from "react-router-dom";
import { fmt_date_with_minutes } from "@lib/utils";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
const useAlerter = (id?: string) =>
@@ -131,11 +130,6 @@ const AlerterTable = () => {
},
},
{ header: "Tags", accessorFn: ({ tags }) => tags.join(", ") },
{
header: "Created",
accessorFn: ({ created_at }) =>
fmt_date_with_minutes(new Date(created_at)),
},
]}
/>
);

View File

@@ -0,0 +1,105 @@
import { Config } from "@components/config";
import {
AccountSelector,
ConfigItem,
ResourceSelector,
} from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { env_to_text, text_to_env } from "@lib/utils";
import { Types } from "@monitor/client";
import { Textarea } from "@ui/textarea";
import { useEffect, useState } from "react";
export const BuildConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuild", { build: id }).data?.config;
// const orgs = useRead("GetAccounts")
const [update, set] = useState<Partial<Types.BuildConfig>>({});
const { mutate } = useWrite("UpdateBuild");
if (!config) return null;
return (
<Config
config={config}
update={update}
set={set}
onSave={() => mutate({ id, config: update })}
components={{
general: {
general: {
builder_id: (id, set) => (
<div className="flex justify-between items-center border-b pb-4 min-h-[60px]">
<div>Builder</div>
<ResourceSelector
type="Builder"
selected={id}
onSelect={(builder_id) => set({ builder_id })}
/>
</div>
),
},
git: {
repo: true,
branch: true,
github_account: (account, set) => (
<AccountSelector
id={update.builder_id ?? config.builder_id ?? undefined}
type="Builder"
account_type="github"
selected={account}
onSelect={(github_account) => set({ github_account })}
/>
),
},
docker: {
build_path: true,
dockerfile_path: true,
docker_account: (account, set) => (
<AccountSelector
id={update.builder_id ?? config.builder_id ?? undefined}
type="Builder"
account_type="docker"
selected={account}
onSelect={(docker_account) => set({ docker_account })}
/>
),
use_buildx: true,
// docker_organization,
},
},
"Build Args": {
"Build Args": {
build_args: (vars, set) => (
<BuildArgs vars={vars ?? []} set={set} />
),
skip_secret_interp: true,
},
},
}}
/>
);
};
const BuildArgs = ({
vars,
set,
}: {
vars: Types.EnvironmentVar[];
set: (input: Partial<Types.BuildConfig>) => void;
}) => {
const [args, setArgs] = useState(env_to_text(vars));
useEffect(() => {
!!args && set({ build_args: text_to_env(args) });
}, [args, set]);
return (
<ConfigItem label="Build Args" className="flex-col gap-4 items-start">
<Textarea
className="min-h-[300px]"
placeholder="VARIABLE=value"
value={args}
onChange={(e) => setArgs(e.target.value)}
/>
</ConfigItem>
);
};

View File

@@ -1,30 +1,18 @@
import { Config } from "@components/config";
import {
ResourceSelector,
AccountSelector,
ConfigItem,
} from "@components/config/util";
import { NewResource } from "@components/layouts";
import { ConfirmButton } from "@components/util";
import { useExecute, useRead, useWrite } from "@lib/hooks";
import {
env_to_text,
fmt_date_with_minutes,
fmt_version,
text_to_env,
} from "@lib/utils";
import { Types } from "@monitor/client";
import { fmt_date_with_minutes, fmt_version } from "@lib/utils";
import { RequiredResourceComponents } from "@types";
import { DataTable } from "@ui/data-table";
import { Input } from "@ui/input";
import { Ban, Hammer, History, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { ResourceComponents } from "..";
import { BuildChart } from "@components/dashboard/builds-chart";
import { TagsWithBadge, useTagsFilter } from "@components/tags";
import { Textarea } from "@ui/textarea";
import { useToast } from "@ui/use-toast";
import { BuildConfig } from "./config";
const useBuild = (id?: string) =>
useRead("ListBuilds", {}).data?.find((d) => d.id === id);
@@ -50,100 +38,6 @@ const NewBuild = () => {
);
};
export const BuildArgs = ({
vars,
set,
}: {
vars: Types.EnvironmentVar[];
set: (input: Partial<Types.BuildConfig>) => void;
}) => {
const [args, setArgs] = useState(env_to_text(vars));
useEffect(() => {
!!args && set({ build_args: text_to_env(args) });
}, [args, set]);
return (
<ConfigItem label="Build Args" className="flex-col gap-4 items-start">
<Textarea
className="min-h-[300px]"
placeholder="VARIABLE=value"
value={args}
onChange={(e) => setArgs(e.target.value)}
/>
</ConfigItem>
);
};
export const BuildConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuild", { build: id }).data?.config;
// const orgs = useRead("GetAccounts")
const [update, set] = useState<Partial<Types.BuildConfig>>({});
const { mutate } = useWrite("UpdateBuild");
if (!config) return null;
return (
<Config
config={config}
update={update}
set={set}
onSave={() => mutate({ id, config: update })}
components={{
general: {
general: {
builder_id: (id, set) => (
<div className="flex justify-between items-center border-b pb-4 min-h-[60px]">
<div>Builder</div>
<ResourceSelector
type="Builder"
selected={id}
onSelect={(builder_id) => set({ builder_id })}
/>
</div>
),
},
git: {
repo: true,
branch: true,
github_account: (account, set) => (
<AccountSelector
id={update.builder_id ?? config.builder_id ?? undefined}
type="Builder"
account_type="github"
selected={account}
onSelect={(github_account) => set({ github_account })}
/>
),
},
docker: {
build_path: true,
dockerfile_path: true,
docker_account: (account, set) => (
<AccountSelector
id={update.builder_id ?? config.builder_id ?? undefined}
type="Builder"
account_type="docker"
selected={account}
onSelect={(docker_account) => set({ docker_account })}
/>
),
use_buildx: true,
// docker_organization,
},
},
"Build Args": {
"Build Args": {
build_args: (vars, set) => (
<BuildArgs vars={vars ?? []} set={set} />
),
skip_secret_interp: true,
},
},
}}
/>
);
};
const Name = ({ id }: { id: string }) => <>{useBuild(id)?.name}</>;
const Icon = ({ id }: { id: string }) => {
@@ -182,17 +76,6 @@ const BuildTable = () => {
header: "Version",
accessorFn: ({ info }) => fmt_version(info.version),
},
// {
// header: "Deployments",
// cell: ({ row }) => {
// const deps = useRead("ListDeployments", {
// query: { specific: { build_ids: [row.original.id] } },
// })?.data?.map((d) => (
// <Link to={`/deployments/${d.id}`}>{d.name}</Link>
// ));
// return <div className="flex items-center gap-2">{deps}</div>;
// },
// },
{
header: "Last Built",
accessorFn: ({ info: { last_built_at } }) => {
@@ -213,11 +96,6 @@ const BuildTable = () => {
);
},
},
{
header: "Created",
accessorFn: ({ created_at }) =>
fmt_date_with_minutes(new Date(created_at)),
},
]}
/>
);
@@ -244,7 +122,8 @@ export const BuildComponents: RequiredResourceComponents = {
},
Actions: ({ id }) => {
const { toast } = useToast();
const building = useRead("GetBuildActionState", { build: id }).data?.building;
const building = useRead("GetBuildActionState", { build: id }).data
?.building;
const { mutate: run_mutate, isPending: runPending } = useExecute(
"RunBuild",
{

View File

@@ -0,0 +1,90 @@
import { Config } from "@components/config";
import { InputList, ResourceSelector } from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { useState } from "react";
export const BuilderConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuilder", { builder: id }).data?.config;
if (config?.type === "Aws") return <AwsBuilderConfig id={id} />;
if (config?.type === "Server") return <ServerBuilderConfig id={id} />;
};
const AwsBuilderConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuilder", { builder: id }).data?.config;
const [update, set] = useState<Partial<Types.AwsBuilderConfig>>({});
const { mutate } = useWrite("UpdateBuilder");
if (!config) return null;
return (
<Config
config={config.params as Types.AwsBuilderConfig}
update={update}
set={set}
onSave={() => mutate({ id, config: { type: "Aws", params: update } })}
components={{
general: {
general: {
region: true,
instance_type: true,
volume_gb: true,
ami_id: true,
subnet_id: true,
key_pair_name: true,
assign_public_ip: true,
use_public_ip: true,
security_group_ids: (values, set) => (
<InputList field="security_group_ids" values={values} set={set} />
),
docker_accounts: (accounts, set) => (
<InputList
field="docker_accounts"
values={accounts ?? []}
set={set}
/>
),
github_accounts: (accounts, set) => (
<InputList
field="github_accounts"
values={accounts ?? []}
set={set}
/>
),
},
},
}}
/>
);
};
const ServerBuilderConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuilder", { builder: id }).data?.config;
const [update, set] = useState<Partial<Types.ServerBuilderConfig>>({});
const { mutate } = useWrite("UpdateBuilder");
if (!config) return null;
return (
<Config
config={config.params as Types.ServerBuilderConfig}
update={update}
set={set}
onSave={() => mutate({ id, config: { type: "Server", params: update } })}
components={{
general: {
general: {
id: (id, set) => (
<div className="flex justify-between items-center border-b pb-4">
Select Server
<ResourceSelector
type="Server"
selected={id}
onSelect={(id) => set({ id })}
/>
</div>
),
},
},
}}
/>
);
};

View File

@@ -1,9 +1,6 @@
import { Config } from "@components/config";
import { InputList, ResourceSelector } from "@components/config/util";
import { NewResource } from "@components/layouts";
import { useTagsFilter } from "@components/tags";
import { useRead, useWrite } from "@lib/hooks";
import { fmt_date_with_minutes } from "@lib/utils";
import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
@@ -20,89 +17,11 @@ import {
import { Cloud, Bot, Factory } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { BuilderConfig } from "./config";
const useBuilder = (id?: string) =>
useRead("ListBuilders", {}).data?.find((d) => d.id === id);
const AwsBuilderConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuilder", { builder: id }).data?.config;
const [update, set] = useState<Partial<Types.AwsBuilderConfig>>({});
const { mutate } = useWrite("UpdateBuilder");
if (!config) return null;
return (
<Config
config={config.params as Types.AwsBuilderConfig}
update={update}
set={set}
onSave={() => mutate({ id, config: { type: "Aws", params: update } })}
components={{
general: {
general: {
region: true,
instance_type: true,
volume_gb: true,
ami_id: true,
subnet_id: true,
key_pair_name: true,
assign_public_ip: true,
use_public_ip: true,
security_group_ids: (values, set) => (
<InputList field="security_group_ids" values={values} set={set} />
),
docker_accounts: (accounts, set) => (
<InputList
field="docker_accounts"
values={accounts ?? []}
set={set}
/>
),
github_accounts: (accounts, set) => (
<InputList
field="github_accounts"
values={accounts ?? []}
set={set}
/>
),
},
},
}}
/>
);
};
const ServerBuilderConfig = ({ id }: { id: string }) => {
const config = useRead("GetBuilder", { builder: id }).data?.config;
const [update, set] = useState<Partial<Types.ServerBuilderConfig>>({});
const { mutate } = useWrite("UpdateBuilder");
if (!config) return null;
return (
<Config
config={config.params as Types.ServerBuilderConfig}
update={update}
set={set}
onSave={() => mutate({ id, config: { type: "Server", params: update } })}
components={{
general: {
general: {
id: (id, set) => (
<div className="flex justify-between items-center border-b pb-4">
Select Server
<ResourceSelector
type="Server"
selected={id}
onSelect={(id) => set({ id })}
/>
</div>
),
},
},
}}
/>
);
};
const NewBuilder = () => {
const { mutateAsync } = useWrite("CreateBuilder");
const [name, setName] = useState("");
@@ -196,11 +115,6 @@ const BuilderTable = () => {
accessorKey: "info.instance_type",
},
{ header: "Tags", accessorFn: ({ tags }) => tags.join(", ") },
{
header: "Created",
accessorFn: ({ created_at }) =>
fmt_date_with_minutes(new Date(created_at)),
},
]}
/>
);
@@ -223,11 +137,7 @@ export const BuilderComponents: RequiredResourceComponents = {
),
Icon: () => <Factory className="w-4 h-4" />,
Page: {
Config: ({ id }) => {
const config = useRead("GetBuilder", { builder: id }).data?.config;
if (config?.type === "Aws") return <AwsBuilderConfig id={id} />;
if (config?.type === "Server") return <ServerBuilderConfig id={id} />;
},
Config: BuilderConfig,
},
Table: BuilderTable,
Actions: () => null,

View File

@@ -2,7 +2,7 @@ import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types";
import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react";
import { cn, fmt_date_with_minutes } from "@lib/utils";
import { cn } from "@lib/utils";
import { useState } from "react";
import { NewResource, Section } from "@components/layouts";
@@ -109,11 +109,6 @@ export const DeploymentTable = ({
);
},
},
// {
// header: "Description",
// accessorKey: "description",
// },
{
header: "Server",
cell: ({ row }) => {
@@ -163,11 +158,6 @@ export const DeploymentTable = ({
);
},
},
{
header: "Created",
accessorFn: ({ created_at }) =>
fmt_date_with_minutes(new Date(created_at)),
},
]}
/>
);

View File

@@ -10,11 +10,11 @@ import { ProcedureComponents } from "./procedure/index";
export const ResourceComponents: {
[key in UsableResource]: RequiredResourceComponents;
} = {
Alerter: AlerterComponents,
Build: BuildComponents,
Builder: BuilderComponents,
Deployment: DeploymentComponents,
Repo: RepoComponents,
Server: ServerComponents,
Build: BuildComponents,
Repo: RepoComponents,
Procedure: ProcedureComponents,
Builder: BuilderComponents,
Alerter: AlerterComponents,
};

View File

@@ -2,7 +2,6 @@ import { NewResource } from "@components/layouts";
import { TagsWithBadge } from "@components/tags";
import { ConfirmButton } from "@components/util";
import { useExecute, useRead, useWrite } from "@lib/hooks";
import { fmt_date_with_minutes } from "@lib/utils";
import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types";
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
@@ -72,6 +71,10 @@ export const ProcedureComponents: RequiredResourceComponents = {
);
},
},
{
header: "Type",
accessorKey: "info.procedure_type",
},
{
header: "Tags",
cell: ({ row }) => {
@@ -82,11 +85,6 @@ export const ProcedureComponents: RequiredResourceComponents = {
);
},
},
{
header: "Created",
accessorFn: ({ created_at }) =>
fmt_date_with_minutes(new Date(created_at)),
},
]}
/>
);

View File

@@ -1,5 +1,6 @@
import { Config } from "@components/config";
import { AccountSelector, ResourceSelector } from "@components/config/util";
import { TagsWithBadge } from "@components/tags";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { Icon } from "@radix-ui/react-select";
@@ -98,7 +99,16 @@ export const RepoComponents: RequiredResourceComponents = {
);
},
},
{ header: "Tags", accessorFn: ({ tags }) => tags.join(", ") },
{
header: "Tags",
cell: ({ row }) => {
return (
<div className="flex gap-1">
<TagsWithBadge tag_ids={row.original.tags} />
</div>
);
},
},
]}
/>
);

View File

@@ -1,5 +1,5 @@
import { useInvalidate, useRead, useWrite } from "@lib/hooks";
import { cn, fmt_date_with_minutes } from "@lib/utils";
import { cn } from "@lib/utils";
import { Types } from "@monitor/client";
import { RequiredResourceComponents } from "@types";
import {
@@ -205,11 +205,6 @@ const ServerTable = () => {
);
},
},
{
header: "Created",
accessorFn: ({ created_at }) =>
fmt_date_with_minutes(new Date(created_at)),
},
]}
/>
);
@@ -224,7 +219,9 @@ export const ServerComponents: RequiredResourceComponents = {
Page: {
Stats: ({ id }) => <ServerStats server_id={id} />,
Deployments: ({ id }) => {
const deployments = useRead("ListDeployments", {}).data?.filter(deployment => deployment.info.server_id === id);
const deployments = useRead("ListDeployments", {}).data?.filter(
(deployment) => deployment.info.server_id === id
);
return (
<Section title="Deployments" icon={<Rocket className="w-4 h-4" />}>
<DeploymentTable deployments={deployments} />

View File

@@ -9,6 +9,7 @@ import {
import { Button } from "../ui/button";
import {
Box,
Boxes,
Check,
Copy,
FolderTree,
@@ -45,6 +46,7 @@ import {
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@ui/dropdown-menu";
import { ResourceComponents } from "./resources";
@@ -357,12 +359,24 @@ export const ResourceTypeDropdown = () => {
Dashboard
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<Link to="/resources">
<DropdownMenuItem className="flex items-center gap-2">
<Boxes className="w-4 h-4" />
Resources
</DropdownMenuItem>
</Link>
<Link to="/tree">
<DropdownMenuItem className="flex items-center gap-2">
<FolderTree className="w-4 h-4" />
Tree
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
{RESOURCE_TARGETS.map((rt) => {
const RTIcon = ResourceComponents[rt].Icon;
return (
@@ -374,12 +388,18 @@ export const ResourceTypeDropdown = () => {
</Link>
);
})}
<DropdownMenuSeparator />
<Link to="/tags">
<DropdownMenuItem className="flex items-center gap-2">
<Tag className="w-4 h-4" />
Tags
</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<Link to="/keys">
<DropdownMenuItem className="flex items-center gap-2">
<Box className="w-4 h-4" />

View File

@@ -11,23 +11,25 @@ export const keys = <T extends object>(o: T): (keyof T)[] =>
Object.keys(o) as (keyof T)[];
export const RESOURCE_TARGETS: UsableResource[] = [
"Alerter",
"Build",
"Builder",
"Deployment",
"Repo",
"Server",
"Procedure",
"Deployment",
"Server",
"Build",
"Repo",
"Builder",
"Alerter",
];
export const fmt_date = (d: Date) => {
return `${d.getDate()}/${d.getMonth() + 1} @ ${d.getHours()}:${d.getMinutes()}`;
return `${d.getDate()}/${
d.getMonth() + 1
} @ ${d.getHours()}:${d.getMinutes()}`;
};
export const fmt_date_with_minutes = (d: Date) => {
// return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`;
return d.toLocaleString()
}
return d.toLocaleString();
};
export const fmt_version = (version: Types.Version | undefined) => {
if (!version) return "...";
@@ -80,3 +82,11 @@ export function text_to_env(env: string): Types.EnvironmentVar[] {
})
.map(([variable, value]) => ({ variable, value }));
}
/// list_all_items => List All Items
export function snake_case_to_upper_space_case(snake: string) {
return snake
.split("_")
.map((item) => item[0].toUpperCase() + item.slice(1))
.join(" ");
}

View File

@@ -0,0 +1,47 @@
import { Page, Section } from "@components/layouts";
import { ResourceComponents } from "@components/resources";
import { TagsFilter, useTagsFilter } from "@components/tags";
import { useRead } from "@lib/hooks";
import { UsableResource } from "@types";
import { Input } from "@ui/input";
import { useState } from "react";
export const AllResources = () => {
const [search, setSearch] = useState("");
const tags = useTagsFilter();
return (
<Page
title="Resources"
actions={
<div className="grid gap-4 justify-items-end">
<div className="flex gap-4">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="search..."
className="w-96"
/>
</div>
<TagsFilter />
</div>
}
>
{Object.entries(ResourceComponents).map(([type, Components]) => {
const count = useRead(
`List${type as UsableResource}s`,
{}
).data?.filter((resource) =>
tags.every((tag) => resource.tags.includes(tag))
).length;
if (!count) return;
return (
<Section title={type + "s"} actions={<Components.New />}>
<Components.Table />
</Section>
);
})}
</Page>
);
};

View File

@@ -10,6 +10,7 @@ import { Tree } from "@pages/tree";
import { Tags } from "@pages/tags";
import { ResourceUpdates } from "@pages/resource_update";
import { UsersPage } from "@pages/users";
import { AllResources } from "@pages/all_resources";
const router = createBrowserRouter([
{
@@ -17,16 +18,17 @@ const router = createBrowserRouter([
element: <Layout />,
children: [
{ path: "", element: <Dashboard /> },
{ path: "tree", element: <Tree /> },
{ path: "users", element: <UsersPage /> },
{ path: "keys", element: <Keys /> },
{ path: "tags", element: <Tags /> },
{ path: "users", element: <UsersPage /> },
{ path: "tree", element: <Tree /> },
{ path: "resources", element: <AllResources /> },
{
path: ":type",
children: [
{ path: "", element: <Resources /> },
{ path: ":id", element: <Resource /> },
{ path: ":id/updates", element: <ResourceUpdates /> }
{ path: ":id/updates", element: <ResourceUpdates /> },
],
},
],