Files
komodo/frontend/src/components/config/util.tsx
Maxwell Becker ea5506c202 1.16.1 (#143)
* ensure sync state cache is refreshed on sync create / copy

* clean up resources post_create

* show sidebar if element length > 1

* update `run_komodo_command` command

* rename all resources

* refresh repo cache after clone / pull

* improve rename repo log
2024-10-21 23:19:40 -07:00

1186 lines
31 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import {
useCtrlKeyListener,
useInvalidate,
useRead,
useWrite,
} from "@lib/hooks";
import { Types } from "komodo_client";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@ui/select";
import { Button } from "@ui/button";
import { Input } from "@ui/input";
import { Switch } from "@ui/switch";
import {
CheckCircle,
MinusCircle,
Pen,
PlusCircle,
Save,
Search,
SearchX,
SquareArrowOutUpRight,
} from "lucide-react";
import { ReactNode, useState } from "react";
import { cn, env_to_text, filterBySplit } from "@lib/utils";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@ui/dialog";
import { snake_case_to_upper_space_case } from "@lib/formatting";
import { ConfirmButton, ShowHideButton } from "@components/util";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@ui/command";
import { Card, CardContent, CardHeader } from "@ui/card";
import {
soft_text_color_class_by_intention,
text_color_class_by_intention,
} from "@lib/color";
import {
MonacoDiffEditor,
MonacoEditor,
MonacoLanguage,
} from "@components/monaco";
import { useSettingsView } from "@pages/settings";
import { useNavigate } from "react-router-dom";
import { useToast } from "@ui/use-toast";
import { UsableResource } from "@types";
export const ConfigItem = ({
label,
boldLabel,
description,
children,
className,
}: {
label?: ReactNode;
boldLabel?: boolean;
description?: ReactNode;
children: ReactNode;
className?: string;
}) => (
<div
className={cn(
"pb-6 border-b flex flex-col gap-4 first:pt-0 last:border-b-0 last:pb-0",
className
)}
>
{(label || description) && (
<div>
{label && typeof label === "string" && (
<div className={cn("capitalize", boldLabel && "font-bold")}>
{label.split("_").join(" ")}
</div>
)}
{label && typeof label !== "string" && label}
{description && (
<div className="text-sm text-muted-foreground">{description}</div>
)}
</div>
)}
{children}
</div>
);
export const ConfigInput = ({
label,
boldLabel,
value,
description,
disabled,
placeholder,
onChange,
onBlur,
className,
inputLeft,
inputRight,
}: {
label: string;
boldLabel?: boolean;
value: string | number | undefined;
description?: string;
disabled?: boolean;
placeholder?: string;
onChange?: (value: string) => void;
onBlur?: (value: string) => void;
className?: string;
inputLeft?: ReactNode;
inputRight?: ReactNode;
}) => (
<ConfigItem label={label} boldLabel={boldLabel} description={description}>
{inputLeft || inputRight ? (
<div className="flex gap-2 items-center">
{inputLeft}
<Input
className={cn("max-w-[75%] lg:max-w-[400px]", className)}
type={typeof value === "number" ? "number" : undefined}
value={value}
onChange={(e) => onChange && onChange(e.target.value)}
onBlur={(e) => onBlur && onBlur(e.target.value)}
placeholder={placeholder}
disabled={disabled}
/>
{inputRight}
</div>
) : (
<Input
className={cn("max-w-[75%] lg:max-w-[400px]", className)}
type={typeof value === "number" ? "number" : undefined}
value={value}
onChange={(e) => onChange && onChange(e.target.value)}
onBlur={(e) => onBlur && onBlur(e.target.value)}
placeholder={placeholder}
disabled={disabled}
/>
)}
</ConfigItem>
);
export const ConfigSwitch = ({
label,
boldLabel,
value: checked,
description,
disabled,
onChange,
}: {
label: string;
boldLabel?: boolean;
value: boolean | undefined;
description?: string;
disabled: boolean;
onChange: (value: boolean) => void;
}) => (
<ConfigItem
label={label}
description={description}
boldLabel={boldLabel}
className="flex-col"
>
<div
className="py-2 flex flex-row gap-4 items-center text-sm cursor-pointer"
onClick={() => onChange(!checked)}
>
{/* <div
className={cn(
"transition-colors text-muted-foreground",
!checked && soft_text_color_class_by_intention("Critical")
// checked && "text-muted-foreground"
)}
>
DISABLED
</div> */}
<Switch checked={checked} disabled={disabled} />
<div
className={cn(
"transition-colors",
soft_text_color_class_by_intention(checked ? "Good" : "Critical")
// !checked && "text-muted-foreground"
)}
>
{checked ? "ENABLED" : "DISABLED"}
</div>
</div>
</ConfigItem>
);
export const DoubleInput = <
T extends object,
K extends keyof T,
L extends T[K] extends string | number | undefined ? K : never,
R extends T[K] extends string | number | undefined ? K : never,
>({
disabled,
values,
leftval,
leftpl,
rightval,
rightpl,
// addName,
onLeftChange,
onRightChange,
// onAdd,
onRemove,
containerClassName,
inputClassName,
}: {
disabled: boolean;
values: T[] | undefined;
leftval: L;
leftpl: string;
rightval: R;
rightpl: string;
// addName: string;
onLeftChange: (value: T[L], i: number) => void;
onRightChange: (value: T[R], i: number) => void;
// onAdd: () => void;
onRemove: (i: number) => void;
containerClassName?: string;
inputClassName?: string;
}) => {
return (
<div className={cn("flex flex-col gap-4", containerClassName)}>
{values?.map((value, i) => (
<div
className="flex items-center justify-between gap-4 flex-wrap"
key={i}
>
<Input
className={inputClassName}
value={value[leftval] as any}
placeholder={leftpl}
onChange={(e) => onLeftChange(e.target.value as T[L], i)}
disabled={disabled}
/>
:
<Input
className={inputClassName}
value={value[rightval] as any}
placeholder={rightpl}
onChange={(e) => onRightChange(e.target.value as T[R], i)}
disabled={disabled}
/>
{!disabled && (
<Button variant="secondary" onClick={() => onRemove(i)}>
<MinusCircle className="w-4 h-4" />
</Button>
)}
</div>
))}
{/* {!disabled && (
<Button
variant="secondary"
className="flex items-center gap-2 w-[200px] place-self-end"
onClick={onAdd}
>
<PlusCircle className="w-4 h-4" />
Add {addName}
</Button>
)} */}
</div>
);
};
export const ProviderSelector = ({
disabled,
account_type,
selected,
onSelect,
showCustom = true,
}: {
disabled: boolean;
account_type: "git" | "docker";
selected: string | undefined;
onSelect: (provider: string) => void;
showCustom?: boolean;
}) => {
const [db_request, config_request]:
| ["ListGitProviderAccounts", "ListGitProvidersFromConfig"]
| ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"] =
account_type === "git"
? ["ListGitProviderAccounts", "ListGitProvidersFromConfig"]
: ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"];
const db_providers = useRead(db_request, {}).data;
const config_providers = useRead(config_request, {}).data;
const [customMode, setCustomMode] = useState(false);
if (customMode) {
return (
<Input
placeholder="Input custom provider domain"
value={selected}
onChange={(e) => onSelect(e.target.value)}
className="max-w-[75%] lg:max-w-[400px]"
onBlur={() => setCustomMode(false)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setCustomMode(false);
}
}}
autoFocus
/>
);
}
const domains = new Set<string>();
for (const provider of db_providers ?? []) {
domains.add(provider.domain);
}
for (const provider of config_providers ?? []) {
domains.add(provider.domain);
}
const providers = [...domains];
providers.sort();
return (
<Select
value={selected}
onValueChange={(value) => {
if (value === "Custom") {
onSelect("");
setCustomMode(true);
} else {
onSelect(value);
}
}}
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectValue placeholder="Select Provider" />
</SelectTrigger>
<SelectContent>
{providers
?.filter((provider) => provider)
.map((provider) => (
<SelectItem key={provider} value={provider}>
{provider}
</SelectItem>
))}
{providers !== undefined &&
selected &&
!providers.includes(selected) && (
<SelectItem value={selected}>{selected}</SelectItem>
)}
{showCustom && <SelectItem value={"Custom"}>Custom</SelectItem>}
</SelectContent>
</Select>
);
};
export const ProviderSelectorConfig = (params: {
disabled: boolean;
account_type: "git" | "docker";
selected: string | undefined;
onSelect: (id: string) => void;
https?: boolean;
onHttpsSwitch?: () => void;
description?: string;
boldLabel?: boolean;
}) => {
const select =
params.account_type === "git" ? "git provider" : "docker registry";
const label =
params.account_type === "git" ? "Git Provider" : "Image Registry";
return (
<ConfigItem
label={label}
description={params.description ?? `Select ${select} domain`}
boldLabel={params.boldLabel}
>
{params.account_type === "git" ? (
<div className="flex items-center gap-2 w-[75%]">
<Button
variant="outline"
onClick={params.onHttpsSwitch}
className="py-0 px-2"
>
{`http${params.https ? "s" : ""}://`}
</Button>
<ProviderSelector {...params} />
</div>
) : (
<ProviderSelector {...params} />
)}
</ConfigItem>
);
};
export const AccountSelector = ({
disabled,
id,
type,
account_type,
provider,
selected,
onSelect,
placeholder = "Select Account",
}: {
disabled: boolean;
type: "Server" | "Builder" | "None";
id?: string;
account_type: "git" | "docker";
provider: string;
selected: string | undefined;
onSelect: (id: string) => void;
placeholder?: string;
}) => {
const [db_request, config_request]:
| ["ListGitProviderAccounts", "ListGitProvidersFromConfig"]
| ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"] =
account_type === "git"
? ["ListGitProviderAccounts", "ListGitProvidersFromConfig"]
: ["ListDockerRegistryAccounts", "ListDockerRegistriesFromConfig"];
const config_params =
type === "None" ? {} : { target: id ? { type, id } : undefined };
const db_accounts = useRead(db_request, {}).data?.filter(
(account) => account.domain === provider
);
const config_providers = useRead(config_request, config_params).data?.filter(
(_provider) => _provider.domain === provider
);
const _accounts = new Set<string>();
for (const account of db_accounts ?? []) {
if (account.username) {
_accounts.add(account.username);
}
}
for (const provider of config_providers ?? []) {
for (const account of provider.accounts ?? []) {
_accounts.add(account.username);
}
}
const accounts = [..._accounts];
accounts.sort();
return (
<Select
value={selected}
onValueChange={(value) => {
onSelect(value === "Empty" ? "" : value);
}}
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
<SelectItem value={"Empty"}>None</SelectItem>
{accounts
?.filter((account) => account)
.map((account) => (
<SelectItem key={account} value={account}>
{account}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
export const AccountSelectorConfig = (params: {
disabled: boolean;
id?: string;
type: "Server" | "Builder" | "None";
account_type: "git" | "docker";
provider: string;
selected: string | undefined;
onSelect: (id: string) => void;
placeholder?: string;
description?: string;
}) => {
return (
<ConfigItem
label="Account"
description={
params.description ??
"Select the account used to log in to the provider"
}
>
<AccountSelector {...params} />
</ConfigItem>
);
};
export const ConfigList = <T extends { [key: string]: unknown }>(
props: InputListProps<T> & {
label?: string;
addLabel?: string;
boldLabel?: boolean;
description?: ReactNode;
configClassname?: string;
}
) => {
return (
<ConfigItem {...{ ...props, className: props.configClassname }}>
{!props.disabled && (
<Button
variant="secondary"
onClick={() =>
props.set({
[props.field]: [...props.values, ""],
} as Partial<T>)
}
className="flex items-center gap-2 w-[200px]"
>
<PlusCircle className="w-4 h-4" />
{(props.addLabel ?? "Add " + props.label?.endsWith("s"))
? props.label?.slice(0, -1)
: props.label}
</Button>
)}
{props.values.length > 0 && <InputList {...props} />}
</ConfigItem>
);
};
export type InputListProps<T extends { [key: string]: unknown }> = {
field: keyof T;
values: string[];
disabled: boolean;
set: (update: Partial<T>) => void;
placeholder?: string;
className?: string;
};
export const InputList = <T extends { [key: string]: unknown }>({
field,
values,
disabled,
set,
placeholder,
className,
}: InputListProps<T>) => (
<div className="flex w-full">
<div className="flex flex-col gap-4 w-fit">
{values.map((arg, i) => (
<div className="w-full flex gap-4" key={i}>
<Input
placeholder={placeholder}
value={arg}
onChange={(e) => {
values[i] = e.target.value;
set({ [field]: [...values] } as Partial<T>);
}}
disabled={disabled}
className={cn("w-[400px] max-w-full", className)}
/>
{!disabled && (
<Button
variant="secondary"
onClick={() =>
set({
[field]: [...values.filter((_, idx) => idx !== i)],
} as Partial<T>)
}
>
<MinusCircle className="w-4 h-4" />
</Button>
)}
</div>
))}
</div>
</div>
);
interface ConfirmUpdateProps<T> {
previous: T;
content: Partial<T>;
onConfirm: () => Promise<void>;
loading?: boolean;
disabled: boolean;
language?: MonacoLanguage;
file_contents_language?: MonacoLanguage;
}
export function ConfirmUpdate<T>({
previous,
content,
onConfirm,
loading,
disabled,
language,
file_contents_language,
}: ConfirmUpdateProps<T>) {
const [open, set] = useState(false);
useCtrlKeyListener("Enter", () => {
if (open) {
onConfirm();
} else {
set(true);
}
});
return (
<Dialog open={open} onOpenChange={set}>
<DialogTrigger asChild>
<Button
onClick={() => set(true)}
disabled={disabled}
className="flex items-center gap-2"
>
<Save className="w-4 h-4" />
Save
</Button>
</DialogTrigger>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>Confirm Update</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-4 my-4 max-h-[70vh] overflow-auto">
{Object.entries(content).map(([key, val], i) => (
<ConfirmUpdateItem
key={i}
_key={key as any}
val={val as any}
previous={previous}
language={language}
file_contents_language={file_contents_language}
/>
))}
</div>
<DialogFooter>
<ConfirmButton
title="Update"
icon={<CheckCircle className="w-4 h-4" />}
onClick={async () => {
await onConfirm();
set(false);
}}
loading={loading}
/>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function ConfirmUpdateItem<T>({
_key,
val: _val,
previous,
language,
file_contents_language,
}: {
_key: keyof T;
val: T[keyof T];
previous: T;
language?: MonacoLanguage;
file_contents_language?: MonacoLanguage;
}) {
const [show, setShow] = useState(true);
const val =
typeof _val === "string"
? _val
: Array.isArray(_val)
? _val.length > 0 &&
["string", "number", "boolean"].includes(typeof _val[0])
? JSON.stringify(_val)
: JSON.stringify(_val, null, 2)
: JSON.stringify(_val, null, 2);
const prev_val =
typeof previous[_key] === "string"
? previous[_key]
: _key === "environment" ||
_key === "build_args" ||
_key === "secret_args"
? (env_to_text(previous[_key] as any) ?? "") // For backward compat with 1.14
: Array.isArray(previous[_key])
? previous[_key].length > 0 &&
["string", "number", "boolean"].includes(typeof previous[_key][0])
? JSON.stringify(previous[_key])
: JSON.stringify(previous[_key], null, 2)
: JSON.stringify(previous[_key], null, 2);
const showDiff =
val?.includes("\n") ||
prev_val?.includes("\n") ||
Math.max(val?.length ?? 0, prev_val?.length ?? 0) > 30;
return (
<div
className={cn("mr-6 flex flex-col gap-2", val === prev_val && "hidden")}
>
<Card>
<CardHeader className="p-4 flex flex-row justify-between items-center">
<h1 className={text_color_class_by_intention("Neutral")}>
{snake_case_to_upper_space_case(_key as string)}
</h1>
<ShowHideButton show={show} setShow={setShow} />
</CardHeader>
{show && (
<CardContent>
{showDiff ? (
<MonacoDiffEditor
original={prev_val}
modified={val}
language={
language ??
(["environment", "build_args", "secret_args"].includes(
_key as string
)
? "key_value"
: _key === "file_contents"
? file_contents_language
: "json")
}
/>
) : (
<pre style={{ minHeight: 0 }}>
<span className={text_color_class_by_intention("Critical")}>
{prev_val || "None"}
</span>{" "}
<span className="text-muted-foreground">{"->"}</span>{" "}
<span className={text_color_class_by_intention("Good")}>
{val || "None"}
</span>
</pre>
)}
</CardContent>
)}
</Card>
</div>
);
}
export const SystemCommand = ({
value,
disabled,
set,
}: {
value?: Types.SystemCommand;
disabled: boolean;
set: (value: Types.SystemCommand) => void;
}) => {
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<div className="text-muted-foreground">Path:</div>
<Input
placeholder="Command working directory"
value={value?.path}
className="w-[200px] lg:w-[300px]"
onChange={(e) => set({ ...(value || {}), path: e.target.value })}
disabled={disabled}
/>
</div>
<MonacoEditor
value={
value?.command ||
" # Add multiple commands on new lines. Supports comments.\n "
}
language="shell"
onValueChange={(command) => set({ ...(value || {}), command })}
readOnly={disabled}
/>
</div>
);
};
export const AddExtraArgMenu = ({
onSelect,
type,
disabled,
}: {
onSelect: (suggestion: string) => void;
type: "Deployment" | "Build" | "Stack" | "StackBuild";
disabled?: boolean;
}) => {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const suggestions = useRead(`ListCommon${type}ExtraArgs`, {}).data ?? [];
const filtered = filterBySplit(suggestions, search, (item) => item);
if (suggestions.length === 0) {
return (
<Button
variant="secondary"
className="flex items-center gap-2 w-[200px]"
onClick={() => onSelect("")}
disabled={disabled}
>
<PlusCircle className="w-4 h-4" /> Add Extra Arg
</Button>
);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="secondary"
className="flex items-center gap-2 w-[200px]"
disabled={disabled}
>
<PlusCircle className="w-4 h-4" /> Add Extra Arg
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] max-h-[400px] p-0" align="start">
<Command shouldFilter={false}>
<CommandInput
placeholder="Search suggestions"
className="h-9"
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty className="flex justify-evenly items-center">
No Suggestions Found
<SearchX className="w-3 h-3" />
</CommandEmpty>
<CommandGroup>
<CommandItem
onSelect={() => {
onSelect("");
setOpen(false);
}}
className="w-full cursor-pointer"
>
Empty Extra Arg
</CommandItem>
{filtered?.map((suggestion) => (
<CommandItem
key={suggestion}
onSelect={() => {
onSelect(suggestion);
setOpen(false);
}}
className="w-full overflow-hidden overflow-ellipsis cursor-pointer"
>
{suggestion}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
const IMAGE_REGISTRY_DESCRIPTION = "Configure where the built image is pushed.";
export const ImageRegistryConfig = ({
registry,
setRegistry,
disabled,
resource_id,
}: {
registry: Types.ImageRegistryConfig | undefined;
setRegistry: (registry: Types.ImageRegistryConfig) => void;
disabled: boolean;
// For builds, its builder id. For servers, its server id.
resource_id?: string;
}) => {
// This is the only way to get organizations for now
const config_provider = useRead("ListDockerRegistriesFromConfig", {
target: resource_id ? { type: "Builder", id: resource_id } : undefined,
}).data?.find((provider) => {
return provider.domain === registry?.domain;
});
const organizations = config_provider?.organizations ?? [];
return (
<>
<ConfigItem
label="Image Registry"
boldLabel
description={IMAGE_REGISTRY_DESCRIPTION}
>
<div className="flex items-center justify-stretch gap-4">
<ProviderSelector
disabled={disabled}
account_type="docker"
selected={registry?.domain}
onSelect={(domain) =>
setRegistry({
...registry,
domain,
})
}
showCustom={false}
/>
</div>
</ConfigItem>
{organizations.length > 0 && (
<ConfigItem
label="Organization"
description="Push the build under an organization namespace, rather than the account namespace."
>
<OrganizationSelector
organizations={organizations}
selected={registry?.organization!}
set={(organization) =>
setRegistry({
...registry,
organization,
})
}
disabled={disabled}
/>
</ConfigItem>
)}
{registry && (
<ConfigItem
label="Account"
description="Select the account used to authenticate against the registry."
>
<AccountSelector
id={resource_id}
type="Builder"
account_type="docker"
provider={registry.domain!}
selected={registry.account}
onSelect={(account) =>
setRegistry({
...registry,
account,
})
}
disabled={disabled}
/>
</ConfigItem>
)}
</>
);
};
const OrganizationSelector = ({
organizations,
selected,
set,
disabled,
}: {
organizations: string[];
selected: string;
set: (org: string) => void;
disabled: boolean;
}) => {
const [customMode, setCustomMode] = useState(false);
if (customMode || organizations.length === 0) {
return (
<Input
placeholder="Input custom organization name"
value={selected}
onChange={(e) => set(e.target.value)}
className="max-w-[75%] lg:max-w-[400px]"
onBlur={() => setCustomMode(false)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setCustomMode(false);
}
}}
autoFocus
/>
);
}
const orgs =
selected === "" || organizations.includes(selected)
? organizations
: [...organizations, selected];
orgs.sort();
return (
<Select
value={selected}
onValueChange={(organization) => {
if (organization === "Custom") {
set("");
setCustomMode(true);
} else if (organization === "Empty") {
set("");
} else {
set(organization);
}
}}
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectValue placeholder="Select Organization" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"Empty"}>None</SelectItem>
{orgs
?.filter((org) => org)
.map((org) => (
<SelectItem key={org} value={org}>
{org}
</SelectItem>
))}
<SelectItem value={"Custom"}>Custom</SelectItem>
</SelectContent>
</Select>
);
};
export const SecretSelector = ({
keys,
onSelect,
type,
disabled,
align = "start",
side = "right",
}: {
keys: string[];
onSelect: (key: string) => void;
type: "Variable" | "Secret";
disabled: boolean;
align?: "start" | "center" | "end";
side?: "bottom" | "right";
}) => {
const nav = useNavigate();
const [_, setSettingsView] = useSettingsView();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const filtered = filterBySplit(keys, search, (item) => item).sort((a, b) => {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
} else {
return 0;
}
});
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="secondary" className="flex gap-2" disabled={disabled}>
<Search className="w-4 h-4" />
<div>{type}s</div>
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[300px] max-h-[300px] p-0"
align={align}
side={side}
>
<Command shouldFilter={false}>
<CommandInput
placeholder={`Search ${type}s`}
className="h-9"
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty className="flex justify-evenly items-center pt-2">
{`No ${type}s Found`}
<SearchX className="w-3 h-3" />
</CommandEmpty>
<CommandGroup>
{filtered.map((key) => (
<CommandItem
key={key}
onSelect={() => {
onSelect(key);
setOpen(false);
}}
className="flex items-center justify-between cursor-pointer"
>
<div className="p-1">{key}</div>
</CommandItem>
))}
<CommandItem
onSelect={() => {
setOpen(false);
setSettingsView("Variables");
nav("/settings");
}}
className="flex items-center justify-between cursor-pointer"
>
<div className="p-1">All</div>
<SquareArrowOutUpRight className="w-4 h-4" />
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export const PermissionLevelSelector = ({
level,
onSelect,
}: {
level: Types.PermissionLevel;
onSelect: (level: Types.PermissionLevel) => void;
}) => {
return (
<Select
value={level}
onValueChange={(value) => onSelect(value as Types.PermissionLevel)}
>
<SelectTrigger className="w-32 capitalize">
<SelectValue />
</SelectTrigger>
<SelectContent className="w-32">
{Object.keys(Types.PermissionLevel).map((permission) => (
<SelectItem
value={permission}
key={permission}
className="capitalize"
>
{permission}
</SelectItem>
))}
</SelectContent>
</Select>
);
};
export const RenameResource = ({
type,
id,
}: {
type: UsableResource;
id: string;
}) => {
const invalidate = useInvalidate();
const { toast } = useToast();
const { mutate, isPending } = useWrite(`Rename${type}`, {
onSuccess: () => {
invalidate([`List${type}s`]);
toast({ title: `${type} Renamed` });
set("");
},
});
const [name, set] = useState("");
return (
<div className="flex items-center justify-between">
<div className="w-full">
Rename{" "}
{type === "ServerTemplate"
? "Template"
: type === "ResourceSync"
? "Sync"
: type}
</div>
<div className="flex gap-4 w-full justify-end">
<Input
value={name}
onChange={(e) => set(e.target.value)}
className="w-96"
placeholder="Enter new name"
/>
<ConfirmButton
title="Rename"
icon={<Pen className="w-4 h-4" />}
disabled={!name || isPending}
loading={isPending}
onClick={() => mutate({ id, name })}
/>
</div>
</div>
);
};