diff --git a/pkgs/clan-app/ui/src/components/Form/index.tsx b/pkgs/clan-app/ui/src/components/Form/index.tsx new file mode 100644 index 000000000..288342117 --- /dev/null +++ b/pkgs/clan-app/ui/src/components/Form/index.tsx @@ -0,0 +1,29 @@ +import { FieldSchema } from "@/src/hooks/queries"; +import { Maybe } from "@modular-forms/solid"; + +export const tooltipText = ( + name: K, + schema: FieldSchema, + staticValue: Maybe = undefined, +): Maybe => { + const entry = schema[name]; + + // return the static value if there is no field schema entry, or the entry + // indicates the field is writeable + if (!(entry && entry.readonly)) { + return staticValue; + } + + const components: string[] = []; + + if (staticValue) { + components.push(staticValue); + } + + components.push(`This field is read-only`); + if (entry.reason) { + components.push(entry.reason); + } + + return components.join(". "); +}; diff --git a/pkgs/clan-app/ui/src/hooks/queries.ts b/pkgs/clan-app/ui/src/hooks/queries.ts index 49b9d97c1..28c6c74cd 100644 --- a/pkgs/clan-app/ui/src/hooks/queries.ts +++ b/pkgs/clan-app/ui/src/hooks/queries.ts @@ -6,10 +6,22 @@ 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 Machine = SuccessData<"get_machine">; export type ListMachines = SuccessData<"list_machines">; export type MachineDetails = SuccessData<"get_machine_details">; +export interface MachineDetail { + machine: Machine; + fieldsSchema: FieldSchema; +} + export type MachinesQueryResult = UseQueryResult; export type ClanListQueryResult = UseQueryResult[]; @@ -35,22 +47,43 @@ export const useMachinesQuery = (clanURI: string) => { export const useMachineQuery = (clanURI: string, machineName: string) => { const client = useApiClient(); - return useQuery(() => ({ + return useQuery(() => ({ queryKey: ["clans", encodeBase64(clanURI), "machine", machineName], queryFn: async () => { - const call = client.fetch("get_machine", { - name: machineName, - flake: { - identifier: clanURI, - }, - }); + const [machineCall, schemaCall] = await Promise.all([ + client.fetch("get_machine", { + name: machineName, + flake: { + identifier: clanURI, + }, + }), + client.fetch("get_machine_fields_schema", { + machine: { + name: machineName, + flake: { + identifier: clanURI, + }, + }, + }), + ]); - const result = await call.result; - if (result.status === "error") { - throw new Error("Error fetching machine: " + result.errors[0].message); + const machine = await machineCall.result; + if (machine.status === "error") { + throw new Error("Error fetching machine: " + machine.errors[0].message); } - return result.data; + const writeSchema = await schemaCall.result; + if (writeSchema.status === "error") { + throw new Error( + "Error fetching machine fields schema: " + + writeSchema.errors[0].message, + ); + } + + return { + machine: machine.data, + fieldsSchema: writeSchema.data, + }; }, })); }; diff --git a/pkgs/clan-app/ui/src/routes/Machine/Machine.tsx b/pkgs/clan-app/ui/src/routes/Machine/Machine.tsx index 7bdf6a5a0..31db91827 100644 --- a/pkgs/clan-app/ui/src/routes/Machine/Machine.tsx +++ b/pkgs/clan-app/ui/src/routes/Machine/Machine.tsx @@ -5,8 +5,9 @@ import { createSignal, Show } from "solid-js"; import { SectionGeneral } from "./SectionGeneral"; import { InstallModal } from "@/src/workflows/Install/install"; import { Button } from "@/src/components/Button/Button"; -import { useMachineQuery } from "@/src/hooks/queries"; +import { Machine as MachineModel, useMachineQuery } from "@/src/hooks/queries"; import { SectionTags } from "@/src/routes/Machine/SectionTags"; +import { callApi } from "@/src/hooks/api"; export const Machine = (props: RouteSectionProps) => { const navigate = useNavigate(); @@ -18,9 +19,38 @@ export const Machine = (props: RouteSectionProps) => { }; const [showInstall, setShowModal] = createSignal(false); + + let container: Node; + const sidebarPane = (machineName: string) => { const machineQuery = useMachineQuery(clanURI, machineName); - const sectionProps = { clanURI, machineName, machineQuery }; + + // we have to update the whole machine model rather than just the sub fields that were changed + // for that reason we pass in this common submit handler to each machine sub section + const onSubmit = async (values: Partial) => { + const call = callApi("set_machine", { + machine: { + name: machineName, + flake: { + identifier: clanURI, + }, + }, + update: { + ...machineQuery.data?.machine, + ...values, + }, + }); + + const result = await call.result; + if (result.status === "error") { + throw new Error(result.errors[0].message); + } + + // refresh the query + await machineQuery.refetch(); + }; + + const sectionProps = { clanURI, machineName, onSubmit, machineQuery }; return ( @@ -30,7 +60,6 @@ export const Machine = (props: RouteSectionProps) => { ); }; - let container: Node; return (