forked from github-starred/komodo
refactor config
This commit is contained in:
@@ -1,15 +1,12 @@
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@ui/card";
|
||||
import { StringConfig } from "./components/string";
|
||||
import { ArrayConfig } from "./components/array";
|
||||
import { BooleanConfig } from "./components/boolean";
|
||||
import { NumberConfig } from "./components/number";
|
||||
import { Button } from "@ui/button";
|
||||
import { Input } from "@ui/input";
|
||||
import { Switch } from "@ui/switch";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/card";
|
||||
import { Selector, SearchableSelector, SelectorItem } from "./Selector";
|
||||
import { Tabs, TabsList } from "@ui/tabs";
|
||||
import { TabsContent, TabsTrigger } from "@radix-ui/react-tabs";
|
||||
|
||||
export type ConfigSetter<T> = (
|
||||
update: (curr: Partial<T>) => Partial<T>
|
||||
@@ -42,6 +39,20 @@ type ArrayDefaults<T extends Record<string, unknown>> = {
|
||||
[P in keyof T]?: T[P] extends unknown[] ? T[P][number] : never;
|
||||
};
|
||||
|
||||
function findDeepDisabled<T extends Record<string, unknown>>(
|
||||
field: string,
|
||||
disabled?: boolean | DeepDisabled<T>
|
||||
) {
|
||||
if (disabled === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof disabled === "boolean") {
|
||||
return disabled;
|
||||
} else {
|
||||
return disabled[field];
|
||||
}
|
||||
}
|
||||
|
||||
export function Config<T extends Record<string, unknown>>({
|
||||
config,
|
||||
update,
|
||||
@@ -59,386 +70,118 @@ export function Config<T extends Record<string, unknown>>({
|
||||
arrayDefaults?: ArrayDefaults<T>;
|
||||
disabled?: boolean | DeepDisabled<T>;
|
||||
}) {
|
||||
const config_keys = Object.keys(config);
|
||||
const [show, setShow] = useState(config_keys[0]);
|
||||
|
||||
return (
|
||||
<div className="grid lg:grid-cols-2 gap-4">
|
||||
{Object.entries(config).map(([field, value]) => {
|
||||
const val = update[field] ?? value;
|
||||
const overide = overrides?.[field];
|
||||
if (overide && typeof overide === "function") {
|
||||
return overide(update[field] ?? (value as T[string]), (updt) =>
|
||||
set((curr) => ({
|
||||
...curr,
|
||||
[field]: updt(
|
||||
update[field] ?? (config[field] as Partial<T[string]>)
|
||||
),
|
||||
}))
|
||||
) as ReactNode;
|
||||
}
|
||||
if (typeof val === "string") {
|
||||
return (
|
||||
<StringConfig
|
||||
key={field}
|
||||
field={field}
|
||||
val={val}
|
||||
set={(u) =>
|
||||
set((curr) => ({
|
||||
...curr,
|
||||
[field]: u(val),
|
||||
}))
|
||||
}
|
||||
description={descriptions?.[field] as string | undefined}
|
||||
disabled={findDeepDisabled(field, disabled) as boolean}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (typeof val === "boolean") {
|
||||
return (
|
||||
<BooleanConfig
|
||||
key={field}
|
||||
field={field}
|
||||
val={val}
|
||||
set={(u) => set((curr) => ({ ...curr, [field]: u(val) }))}
|
||||
description={descriptions?.[field] as string | undefined}
|
||||
disabled={findDeepDisabled(field, disabled) as boolean}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (typeof val === "number") {
|
||||
return (
|
||||
<NumberConfig
|
||||
key={field}
|
||||
field={field}
|
||||
val={val}
|
||||
set={(u) => set((curr) => ({ ...curr, [field]: u(val) }))}
|
||||
description={descriptions?.[field] as string | undefined}
|
||||
disabled={findDeepDisabled(field, disabled) as boolean}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const val = (update[field] ? update[field] : value) as unknown[];
|
||||
return (
|
||||
<ArrayConfig
|
||||
key={field}
|
||||
field={field}
|
||||
val={val}
|
||||
set={set}
|
||||
defaultNew={arrayDefaults?.[field] ?? ""}
|
||||
disabled={findDeepDisabled(field, disabled) as boolean}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card key={field}>
|
||||
<CardHeader>
|
||||
<CardTitle>{field.replaceAll("_", " ")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Config
|
||||
config={config[field] as Record<string, unknown>}
|
||||
update={update[field] ?? {}}
|
||||
set={(update) => {
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
{Object.keys(config).map((config) => (
|
||||
<Button
|
||||
key={config}
|
||||
onClick={() => setShow(config)}
|
||||
variant="outline"
|
||||
>
|
||||
{config}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full h-fit">
|
||||
{Object.entries(config).map(([field, value]) => {
|
||||
if (show !== field) return null;
|
||||
const val = update[field] ?? value;
|
||||
const overide = overrides?.[field];
|
||||
if (overide && typeof overide === "function") {
|
||||
return overide(update[field] ?? (value as T[string]), (updt) =>
|
||||
set((curr) => ({
|
||||
...curr,
|
||||
[field]: updt(
|
||||
update[field] ?? (config[field] as Partial<T[string]>)
|
||||
),
|
||||
}))
|
||||
) as ReactNode;
|
||||
}
|
||||
if (typeof val === "string") {
|
||||
return (
|
||||
<StringConfig
|
||||
key={field}
|
||||
field={field}
|
||||
val={val}
|
||||
set={(u) =>
|
||||
set((curr) => ({
|
||||
...curr,
|
||||
[field]: update(
|
||||
curr[field] ?? (config[field] as Partial<T[string]>)
|
||||
),
|
||||
}));
|
||||
}}
|
||||
overrides={
|
||||
overrides?.[field] as Overides<Record<string, unknown>>
|
||||
[field]: u(val),
|
||||
}))
|
||||
}
|
||||
disabled={findDeepDisabled(field, disabled)}
|
||||
description={descriptions?.[field] as string | undefined}
|
||||
disabled={findDeepDisabled(field, disabled) as boolean}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
);
|
||||
}
|
||||
if (typeof val === "boolean") {
|
||||
return (
|
||||
<BooleanConfig
|
||||
key={field}
|
||||
field={field}
|
||||
val={val}
|
||||
set={(u) => set((curr) => ({ ...curr, [field]: u(val) }))}
|
||||
description={descriptions?.[field] as string | undefined}
|
||||
disabled={findDeepDisabled(field, disabled) as boolean}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (typeof val === "number") {
|
||||
return (
|
||||
<NumberConfig
|
||||
key={field}
|
||||
field={field}
|
||||
val={val}
|
||||
set={(u) => set((curr) => ({ ...curr, [field]: u(val) }))}
|
||||
description={descriptions?.[field] as string | undefined}
|
||||
disabled={findDeepDisabled(field, disabled) as boolean}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const val = (update[field] ? update[field] : value) as unknown[];
|
||||
return (
|
||||
<ArrayConfig
|
||||
key={field}
|
||||
field={field}
|
||||
val={val}
|
||||
set={set}
|
||||
defaultNew={arrayDefaults?.[field] ?? ""}
|
||||
disabled={findDeepDisabled(field, disabled) as boolean}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card key={field}>
|
||||
<CardHeader>
|
||||
<CardTitle>{field.replaceAll("_", " ")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Config
|
||||
config={config[field] as Record<string, unknown>}
|
||||
update={update[field] ?? {}}
|
||||
set={(update) => {
|
||||
set((curr) => ({
|
||||
...curr,
|
||||
[field]: update(
|
||||
curr[field] ?? (config[field] as Partial<T[string]>)
|
||||
),
|
||||
}));
|
||||
}}
|
||||
overrides={
|
||||
overrides?.[field] as Overides<Record<string, unknown>>
|
||||
}
|
||||
disabled={findDeepDisabled(field, disabled)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StringConfig({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
field: string;
|
||||
val: string;
|
||||
set: ConfigSetter<string>;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const readable_field = field.replaceAll("_", " ");
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{readable_field}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input
|
||||
id={field}
|
||||
className="w-full"
|
||||
value={val}
|
||||
onChange={(e) => set(() => e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function StringSelectorConfig({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
items,
|
||||
label,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
field: string;
|
||||
val: string;
|
||||
set: ConfigSetter<string>;
|
||||
items: SelectorItem[];
|
||||
label?: (val: string) => string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row justify-between items-center">
|
||||
<div className="flex gap-2">
|
||||
<CardTitle>{field.replaceAll("_", " ")}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</div>
|
||||
<Selector
|
||||
width="180px"
|
||||
value={{
|
||||
value: val,
|
||||
label: label ? label(val) : val,
|
||||
}}
|
||||
items={items}
|
||||
onSelect={({ value }) => set(() => value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchableStringSelectorConfig({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
items,
|
||||
label,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
field: string;
|
||||
val: string;
|
||||
set: ConfigSetter<string>;
|
||||
items: SelectorItem[];
|
||||
label?: (val: string) => string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row justify-between items-center">
|
||||
<div className="flex gap-2">
|
||||
<CardTitle>{field.replaceAll("_", " ")}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</div>
|
||||
<SearchableSelector
|
||||
width="180px"
|
||||
value={{
|
||||
value: val,
|
||||
label: label ? label(val) : val,
|
||||
}}
|
||||
items={items}
|
||||
onSelect={(value) => set(() => value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function BooleanConfig({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
field: string;
|
||||
val: boolean;
|
||||
set: ConfigSetter<boolean>;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const readable_field = field.replaceAll("_", " ");
|
||||
const onChange = (checked: boolean) => set(() => checked);
|
||||
return (
|
||||
<Card
|
||||
className="flex flex-row justify-between items-center cursor-pointer"
|
||||
onClick={() => onChange(!val)}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{readable_field}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<Switch
|
||||
id={field}
|
||||
className="m-6"
|
||||
checked={val}
|
||||
onCheckedChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function NumberConfig({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
disabled,
|
||||
description,
|
||||
}: {
|
||||
field: string;
|
||||
val: number;
|
||||
set: ConfigSetter<number>;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const readable_field = field.replaceAll("_", " ");
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{readable_field}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input
|
||||
className="w-[250px]"
|
||||
type="number"
|
||||
defaultValue={val}
|
||||
onChange={(e) => set(() => e.target.valueAsNumber)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArrayConfig<T, D>({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
defaultNew,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
field: string;
|
||||
val: unknown[];
|
||||
set: ConfigSetter<T>;
|
||||
defaultNew: D;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row justify-between items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<CardTitle>{field.replaceAll("_", " ")}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
set((curr) => ({ ...curr, [field]: [...val, defaultNew] }))
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</CardHeader>
|
||||
{val.length > 0 && (
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{val.map((item, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i.toString()}
|
||||
className="flex flex-row gap-4 justify-between items-center"
|
||||
>
|
||||
{typeof item === "string" ? (
|
||||
<Input
|
||||
value={val[i] as string}
|
||||
onChange={(e) =>
|
||||
set((curr) => {
|
||||
return {
|
||||
...curr,
|
||||
[field]: [
|
||||
...val.slice(0, i),
|
||||
e.target.value,
|
||||
...val.slice(i + 1),
|
||||
],
|
||||
};
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : typeof item === "object" ? (
|
||||
<Config
|
||||
config={item as Record<string, unknown>}
|
||||
update={item as Record<string, unknown>}
|
||||
set={(upd) =>
|
||||
set((curr) => ({
|
||||
...curr,
|
||||
[field]: [
|
||||
...val.slice(0, i),
|
||||
upd(item as Record<string, unknown>),
|
||||
...val.slice(i + 1),
|
||||
],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
onClick={() => {
|
||||
set((curr) => ({
|
||||
...curr,
|
||||
[field]: val.filter((_, index) => i !== index),
|
||||
}));
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function findDeepDisabled<T extends Record<string, unknown>>(
|
||||
field: string,
|
||||
disabled?: boolean | DeepDisabled<T>
|
||||
) {
|
||||
if (disabled === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (typeof disabled === "boolean") {
|
||||
return disabled;
|
||||
} else {
|
||||
return disabled[field];
|
||||
}
|
||||
}
|
||||
|
||||
102
frontend/src/components/config/components/array.tsx
Normal file
102
frontend/src/components/config/components/array.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Button } from "@ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from "@ui/card";
|
||||
import { Input } from "@ui/input";
|
||||
import { Config, ConfigSetter } from "../Config";
|
||||
|
||||
export function ArrayConfig<T, D>({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
defaultNew,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
field: string;
|
||||
val: unknown[];
|
||||
set: ConfigSetter<T>;
|
||||
defaultNew: D;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row justify-between items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<CardTitle>{field.replaceAll("_", " ")}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
set((curr) => ({ ...curr, [field]: [...val, defaultNew] }))
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</CardHeader>
|
||||
{val.length > 0 && (
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{val.map((item, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i.toString()}
|
||||
className="flex flex-row gap-4 justify-between items-center"
|
||||
>
|
||||
{typeof item === "string" ? (
|
||||
<Input
|
||||
value={val[i] as string}
|
||||
onChange={(e) =>
|
||||
set((curr) => {
|
||||
return {
|
||||
...curr,
|
||||
[field]: [
|
||||
...val.slice(0, i),
|
||||
e.target.value,
|
||||
...val.slice(i + 1),
|
||||
],
|
||||
};
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : typeof item === "object" ? (
|
||||
<Config
|
||||
config={item as Record<string, unknown>}
|
||||
update={item as Record<string, unknown>}
|
||||
set={(upd) =>
|
||||
set((curr) => ({
|
||||
...curr,
|
||||
[field]: [
|
||||
...val.slice(0, i),
|
||||
upd(item as Record<string, unknown>),
|
||||
...val.slice(i + 1),
|
||||
],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
onClick={() => {
|
||||
set((curr) => ({
|
||||
...curr,
|
||||
[field]: val.filter((_, index) => i !== index),
|
||||
}));
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
38
frontend/src/components/config/components/boolean.tsx
Normal file
38
frontend/src/components/config/components/boolean.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Switch } from "@ui/switch";
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from "@ui/card";
|
||||
import { ConfigSetter } from "../Config";
|
||||
|
||||
export const BooleanConfig = ({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
field: string;
|
||||
val: boolean;
|
||||
set: ConfigSetter<boolean>;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const readable_field = field.replaceAll("_", " ");
|
||||
const onChange = (checked: boolean) => set(() => checked);
|
||||
return (
|
||||
<Card
|
||||
className="flex flex-row justify-between items-center cursor-pointer"
|
||||
onClick={() => onChange(!val)}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{readable_field}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<Switch
|
||||
id={field}
|
||||
className="m-6"
|
||||
checked={val}
|
||||
onCheckedChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
42
frontend/src/components/config/components/number.tsx
Normal file
42
frontend/src/components/config/components/number.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from "@ui/card";
|
||||
import { Input } from "@ui/input";
|
||||
import { ConfigSetter } from "../Config";
|
||||
|
||||
export function NumberConfig({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
disabled,
|
||||
description,
|
||||
}: {
|
||||
field: string;
|
||||
val: number;
|
||||
set: ConfigSetter<number>;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const readable_field = field.replaceAll("_", " ");
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{readable_field}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input
|
||||
className="w-[250px]"
|
||||
type="number"
|
||||
defaultValue={val}
|
||||
onChange={(e) => set(() => e.target.valueAsNumber)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Card, CardDescription, CardHeader, CardTitle } from "@ui/card";
|
||||
import { ConfigSetter } from "../Config";
|
||||
import { SelectorItem, SearchableSelector } from "../Selector";
|
||||
|
||||
export function SearchableStringSelectorConfig({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
items,
|
||||
label,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
field: string;
|
||||
val: string;
|
||||
set: ConfigSetter<string>;
|
||||
items: SelectorItem[];
|
||||
label?: (val: string) => string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row justify-between items-center">
|
||||
<div className="flex gap-2">
|
||||
<CardTitle>{field.replaceAll("_", " ")}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</div>
|
||||
<SearchableSelector
|
||||
width="180px"
|
||||
value={{
|
||||
value: val,
|
||||
label: label ? label(val) : val,
|
||||
}}
|
||||
items={items}
|
||||
onSelect={(value) => set(() => value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Card, CardHeader, CardTitle, CardDescription } from "@ui/card";
|
||||
import { ConfigSetter } from "../Config";
|
||||
import { SelectorItem, Selector } from "../Selector";
|
||||
|
||||
interface StringSelectorConfigProps {
|
||||
field: string;
|
||||
val: string;
|
||||
set: ConfigSetter<string>;
|
||||
items: SelectorItem[];
|
||||
label?: (val: string) => string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const StringSelectorConfig = ({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
items,
|
||||
label,
|
||||
description,
|
||||
disabled,
|
||||
}: StringSelectorConfigProps) => (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row justify-between items-center">
|
||||
<div className="flex gap-2">
|
||||
<CardTitle>{field.replaceAll("_", " ")}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</div>
|
||||
<Selector
|
||||
width="180px"
|
||||
value={{
|
||||
value: val,
|
||||
label: label ? label(val) : val,
|
||||
}}
|
||||
items={items}
|
||||
onSelect={({ value }) => set(() => value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
42
frontend/src/components/config/components/string.tsx
Normal file
42
frontend/src/components/config/components/string.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from "@ui/card";
|
||||
import { Input } from "@ui/input";
|
||||
import { ConfigSetter } from "../Config";
|
||||
|
||||
export const StringConfig = ({
|
||||
field,
|
||||
val,
|
||||
set,
|
||||
description,
|
||||
disabled,
|
||||
}: {
|
||||
field: string;
|
||||
val: string;
|
||||
set: ConfigSetter<string>;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const readable_field = field.replaceAll("_", " ");
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{readable_field}</CardTitle>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Input
|
||||
id={field}
|
||||
className="w-full"
|
||||
value={val}
|
||||
onChange={(e) => set(() => e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -15,7 +15,7 @@ export const RecentlyViewed = () => {
|
||||
actions=""
|
||||
>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{recents?.map(({ type, id }) => {
|
||||
{recents?.slice(0, 6).map(({ type, id }) => {
|
||||
if (type === "Deployment") return <DeploymentCard key={id} id={id} />;
|
||||
if (type === "Build") return <BuildCard key={id} id={id} />;
|
||||
if (type === "Server") return <ServerCard key={id} id={id} />;
|
||||
|
||||
Reference in New Issue
Block a user