diff --git a/pkgs/clan-app/ui/src/components/Form/MachineTags.tsx b/pkgs/clan-app/ui/src/components/Form/MachineTags.tsx index 9d272ac34..34a510233 100644 --- a/pkgs/clan-app/ui/src/components/Form/MachineTags.tsx +++ b/pkgs/clan-app/ui/src/components/Form/MachineTags.tsx @@ -11,7 +11,7 @@ import { Label } from "@/src/components/Form/Label"; import { Orienter } from "@/src/components/Form/Orienter"; import { CollectionNode } from "@kobalte/core"; -interface MachineTag { +export interface MachineTag { value: string; disabled?: boolean; new?: boolean; @@ -24,11 +24,10 @@ export type MachineTagsProps = FieldProps & { disabled?: boolean; required?: boolean; defaultValue?: string[]; + defaultOptions?: string[]; + readonlyOptions?: string[]; }; -// tags which are applied to all machines and cannot be removed -const staticOptions = [{ value: "all", disabled: true }]; - const uniqueOptions = (options: MachineTag[]) => { const record: Record = {}; options.forEach((option) => { @@ -39,13 +38,8 @@ const uniqueOptions = (options: MachineTag[]) => { return Object.values(record); }; -const sortedOptions = (options: MachineTag[]) => { - return options.sort((a, b) => { - if (a.new && !b.new) return -1; - if (a.disabled && !b.disabled) return -1; - return a.value.localeCompare(b.value); - }); -}; +const sortedOptions = (options: MachineTag[]) => + options.sort((a, b) => a.value.localeCompare(b.value)); const sortedAndUniqueOptions = (options: MachineTag[]) => sortedOptions(uniqueOptions(options)); @@ -72,9 +66,15 @@ export const MachineTags = (props: MachineTagsProps) => { (props.defaultValue || []).map((value) => ({ value })), ); - // todo this should be the superset of tags used across the entire clan and be passed in via a prop + // convert default options string[] into MachineTag[] const [availableOptions, setAvailableOptions] = createSignal( - sortedAndUniqueOptions([...staticOptions, ...defaultValue]), + sortedAndUniqueOptions([ + ...(props.readonlyOptions || []).map((value) => ({ + value, + disabled: true, + })), + ...(props.defaultOptions || []).map((value) => ({ value })), + ]), ); const onKeyDown = (event: KeyboardEvent) => { diff --git a/pkgs/clan-app/ui/src/components/Form/index.tsx b/pkgs/clan-app/ui/src/components/Form/index.tsx index 288342117..479373408 100644 --- a/pkgs/clan-app/ui/src/components/Form/index.tsx +++ b/pkgs/clan-app/ui/src/components/Form/index.tsx @@ -1,9 +1,9 @@ -import { FieldSchema } from "@/src/hooks/queries"; +import { SuccessData } from "@/src/hooks/api"; import { Maybe } from "@modular-forms/solid"; -export const tooltipText = ( - name: K, - schema: FieldSchema, +export const tooltipText = ( + name: string, + schema: SuccessData<"get_machine_fields_schema">, staticValue: Maybe = undefined, ): Maybe => { const entry = schema[name]; diff --git a/pkgs/clan-app/ui/src/hooks/queries.ts b/pkgs/clan-app/ui/src/hooks/queries.ts index 28c6c74cd..c6eb86f29 100644 --- a/pkgs/clan-app/ui/src/hooks/queries.ts +++ b/pkgs/clan-app/ui/src/hooks/queries.ts @@ -6,20 +6,15 @@ import { useApiClient } from "./ApiClient"; export type ClanDetails = SuccessData<"get_clan_details">; export type ClanDetailsWithURI = ClanDetails & { uri: string }; -export type FieldSchema = { - [K in keyof T]: { - readonly: boolean; - reason?: string; - }; -}; - +export type Tags = SuccessData<"list_tags">; export type Machine = SuccessData<"get_machine">; export type ListMachines = SuccessData<"list_machines">; export type MachineDetails = SuccessData<"get_machine_details">; export interface MachineDetail { + tags: Tags; machine: Machine; - fieldsSchema: FieldSchema; + fieldsSchema: SuccessData<"get_machine_fields_schema">; } export type MachinesQueryResult = UseQueryResult; @@ -50,7 +45,12 @@ export const useMachineQuery = (clanURI: string, machineName: string) => { return useQuery(() => ({ queryKey: ["clans", encodeBase64(clanURI), "machine", machineName], queryFn: async () => { - const [machineCall, schemaCall] = await Promise.all([ + const [tagsCall, machineCall, schemaCall] = await Promise.all([ + client.fetch("list_tags", { + flake: { + identifier: clanURI, + }, + }), client.fetch("get_machine", { name: machineName, flake: { @@ -67,6 +67,11 @@ export const useMachineQuery = (clanURI: string, machineName: string) => { }), ]); + const tags = await tagsCall.result; + if (tags.status === "error") { + throw new Error("Error fetching tags: " + tags.errors[0].message); + } + const machine = await machineCall.result; if (machine.status === "error") { throw new Error("Error fetching machine: " + machine.errors[0].message); @@ -81,6 +86,7 @@ export const useMachineQuery = (clanURI: string, machineName: string) => { } return { + tags: tags.data, machine: machine.data, fieldsSchema: writeSchema.data, }; diff --git a/pkgs/clan-app/ui/src/routes/Machine/SectionTags.tsx b/pkgs/clan-app/ui/src/routes/Machine/SectionTags.tsx index dcb20454d..b56591e30 100644 --- a/pkgs/clan-app/ui/src/routes/Machine/SectionTags.tsx +++ b/pkgs/clan-app/ui/src/routes/Machine/SectionTags.tsx @@ -30,6 +30,26 @@ export const SectionTags = (props: SectionTags) => { return pick(machineQuery.data.machine, ["tags"]) satisfies FormValues; }; + const options = () => { + if (!machineQuery.isSuccess) { + return [[], []]; + } + + // these are static values or values which have been configured in nix and + // cannot be modified in the UI + const readonlyOptions = + machineQuery.data.fieldsSchema.tags?.readonly_members || []; + + // filter out the read-only options from the superset of clan-wide options + const readonlySet = new Set(readonlyOptions); + + const defaultOptions = (machineQuery.data.tags.options || []).filter( + (tag) => !readonlySet.has(tag), + ); + + return [defaultOptions, readonlyOptions]; + }; + return ( { readOnly={!editing} orientation="horizontal" defaultValue={field.value} + defaultOptions={options()[0]} + readonlyOptions={options()[1]} input={input} /> )}