UI: include inventory schema and generated types

This commit is contained in:
Johannes Kirschbauer
2024-09-12 18:22:46 +02:00
parent e3d85fc2b8
commit 406c88907d
4 changed files with 55 additions and 208 deletions

View File

@@ -97,6 +97,8 @@
mkdir -p $out mkdir -p $out
python api.py > $out/API.json python api.py > $out/API.json
${self'.packages.json2ts}/bin/json2ts --input $out/API.json > $out/API.ts ${self'.packages.json2ts}/bin/json2ts --input $out/API.json > $out/API.ts
${self'.packages.json2ts}/bin/json2ts --input ${self'.packages.inventory-schema}/schema.json > $out/Inventory.ts
cp ${self'.packages.inventory-schema}/schema.json $out/inventory-schema.json
''; '';
}; };
json2ts = pkgs.buildNpmPackage { json2ts = pkgs.buildNpmPackage {

View File

@@ -1,160 +0,0 @@
import schema from "@/api/API.json" assert { type: "json" };
import { API } from "@/api/API";
import { nanoid } from "nanoid";
export type OperationNames = keyof API;
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
export type OperationResponse<T extends OperationNames> = API[T]["return"];
export type SuccessQuery<T extends OperationNames> = Extract<
OperationResponse<T>,
{ status: "success" }
>;
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
export type ErrorQuery<T extends OperationNames> = Extract<
OperationResponse<T>,
{ status: "error" }
>;
export type ErrorData<T extends OperationNames> = ErrorQuery<T>["errors"];
export type ClanOperations = {
[K in OperationNames]: (str: string) => void;
};
export interface GtkResponse<T> {
result: T;
op_key: string;
}
declare global {
interface Window {
clan: ClanOperations;
webkit: {
messageHandlers: {
gtk: {
postMessage: (message: {
method: OperationNames;
data: OperationArgs<OperationNames>;
}) => void;
};
};
};
}
}
// Make sure window.webkit is defined although the type is not correctly filled yet.
window.clan = {} as ClanOperations;
const operations = schema.properties;
const operationNames = Object.keys(operations) as OperationNames[];
type ObserverRegistry = {
[K in OperationNames]: Record<
string,
(response: OperationResponse<K>) => void
>;
};
const registry: ObserverRegistry = operationNames.reduce(
(acc, opName) => ({
...acc,
[opName]: {},
}),
{} as ObserverRegistry,
);
function createFunctions<K extends OperationNames>(
operationName: K,
): {
dispatch: (args: OperationArgs<K>) => void;
receive: (fn: (response: OperationResponse<K>) => void, id: string) => void;
} {
window.clan[operationName] = (s: string) => {
const f = (response: OperationResponse<K>) => {
// Get the correct receiver function for the op_key
const receiver = registry[operationName][response.op_key];
if (receiver) {
receiver(response);
}
};
deserialize(f)(s);
};
return {
dispatch: (args: OperationArgs<K>) => {
// Send the data to the gtk app
window.webkit.messageHandlers.gtk.postMessage({
method: operationName,
data: args,
});
},
receive: (fn: (response: OperationResponse<K>) => void, id: string) => {
// @ts-expect-error: This should work although typescript doesn't let us write
registry[operationName][id] = fn;
},
};
}
type PyApi = {
[K in OperationNames]: {
dispatch: (args: OperationArgs<K>) => void;
receive: (fn: (response: OperationResponse<K>) => void, id: string) => void;
};
};
function download(filename: string, text: string) {
const element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
);
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
export const callApi = <K extends OperationNames>(
method: K,
args: OperationArgs<K>,
) => {
return new Promise<OperationResponse<K>>((resolve) => {
const id = nanoid();
pyApi[method].receive((response) => {
console.log(method, "Received response: ", { response });
resolve(response);
}, id);
pyApi[method].dispatch({ ...args, op_key: id });
});
};
const deserialize =
<T>(fn: (response: T) => void) =>
(str: string) => {
try {
const r = JSON.parse(str) as T;
fn(r);
} catch (e) {
console.log("Error parsing JSON: ", e);
window.localStorage.setItem("error", str);
console.error(str);
console.error("See localStorage 'error'");
alert(`Error parsing JSON: ${e}`);
}
};
// Create the API object
const pyApi: PyApi = {} as PyApi;
operationNames.forEach((opName) => {
const name = opName as OperationNames;
// @ts-expect-error - TODO: Fix this. Typescript is not recognizing the receive function correctly
pyApi[name] = createFunctions(name);
});
export { pyApi };

View File

@@ -1,4 +1,9 @@
import { callApi, SuccessQuery } from "@/src/api"; import {
callApi,
ClanService,
ClanServiceInstance,
SuccessQuery,
} from "@/src/api";
import { BackButton } from "@/src/components/BackButton"; import { BackButton } from "@/src/components/BackButton";
import { useParams } from "@solidjs/router"; import { useParams } from "@solidjs/router";
import { import {
@@ -19,6 +24,7 @@ import {
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/components/TextInput";
import toast from "solid-toast"; import toast from "solid-toast";
import { get_single_service } from "@/src/api/inventory";
interface AdminModuleFormProps { interface AdminModuleFormProps {
admin: AdminData; admin: AdminData;
@@ -184,19 +190,20 @@ const AdminModuleForm = (props: AdminModuleFormProps) => {
const handleSubmit = async (values: AdminSettings) => { const handleSubmit = async (values: AdminSettings) => {
console.log("submitting", values, getValues(formStore)); console.log("submitting", values, getValues(formStore));
const r = await callApi("set_admin_service", { // const r = await callApi("set_admin_service", {
base_url: props.base_url, // base_url: props.base_url,
allowed_keys: values.allowedKeys.reduce( // allowed_keys: values.allowedKeys.reduce(
(acc, curr) => ({ ...acc, [curr.name]: curr.value }), // (acc, curr) => ({ ...acc, [curr.name]: curr.value }),
{}, // {}
), // ),
}); // });
if (r.status === "success") { // if (r.status === "success") {
toast.success("Successfully updated admin settings"); // toast.success("Successfully updated admin settings");
} // }
if (r.status === "error") { // if (r.status === "error") {
toast.error(`Failed to update admin settings: ${r.errors[0].message}`); // toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
} toast.error(`Failed to update admin settings: feature disabled`);
// }
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [props.base_url, "get_admin_service"], queryKey: [props.base_url, "get_admin_service"],
}); });
@@ -329,7 +336,7 @@ const AdminModuleForm = (props: AdminModuleFormProps) => {
}; };
type GeneralData = SuccessQuery<"show_clan_meta">["data"]; type GeneralData = SuccessQuery<"show_clan_meta">["data"];
type AdminData = SuccessQuery<"get_admin_service">["data"]; type AdminData = ClanServiceInstance<"admin">;
export const ClanDetails = () => { export const ClanDetails = () => {
const params = useParams(); const params = useParams();
@@ -347,11 +354,9 @@ export const ClanDetails = () => {
const adminQuery = createQuery(() => ({ const adminQuery = createQuery(() => ({
queryKey: [clan_dir, "get_admin_service"], queryKey: [clan_dir, "get_admin_service"],
queryFn: async () => { queryFn: async () => {
const result = await callApi("get_admin_service", { const result = await get_single_service(clan_dir, "", "admin");
base_url: clan_dir, if (!result) throw new Error("Failed to fetch data");
}); return result || null;
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data || null;
}, },
})); }));

View File

@@ -1,4 +1,12 @@
import { callApi, SuccessData, SuccessQuery } from "@/src/api"; import {
callApi,
ClanService,
Services,
SuccessData,
SuccessQuery,
} from "@/src/api";
import { set_single_disk_id } from "@/src/api/disk";
import { get_iwd_service } from "@/src/api/wifi";
import { activeURI } from "@/src/App"; import { activeURI } from "@/src/App";
import { BackButton } from "@/src/components/BackButton"; import { BackButton } from "@/src/components/BackButton";
import { FileInput } from "@/src/components/FileInput"; import { FileInput } from "@/src/components/FileInput";
@@ -117,17 +125,12 @@ const InstallMachine = (props: InstallMachineProps) => {
return; return;
} }
const r = await callApi("set_single_disk_uuid", { const r = await set_single_disk_id(curr_uri, props.name, disk_id);
base_path: curr_uri, if (!r) {
machine_name: props.name,
disk_uuid: disk_id,
});
if (r.status === "error") {
toast.error("Failed to set disk");
}
if (r.status === "success") {
toast.success("Disk set successfully"); toast.success("Disk set successfully");
setConfirmDisk(true); setConfirmDisk(true);
} else {
toast.error("Failed to set disk");
} }
}; };
@@ -600,7 +603,7 @@ const MachineForm = (props: MachineDetailsProps) => {
); );
}; };
type WifiData = SuccessData<"get_iwd_service">; type WifiData = ClanService<"iwd">;
export const MachineDetails = () => { export const MachineDetails = () => {
const params = useParams(); const params = useParams();
@@ -629,12 +632,9 @@ export const MachineDetails = () => {
queryFn: async () => { queryFn: async () => {
const curr = activeURI(); const curr = activeURI();
if (curr) { if (curr) {
const result = await callApi("get_iwd_service", { const result = await get_iwd_service(curr, params.id);
base_url: curr, if (!result) throw new Error("Failed to fetch data");
machine_name: params.id, return Object.entries(result?.config?.networks || {}).map(
});
if (result.status === "error") throw new Error("Failed to fetch data");
return Object.entries(result.data?.config?.networks || {}).map(
([name, value]) => ({ name, ssid: value.ssid }), ([name, value]) => ({ name, ssid: value.ssid }),
); );
} }
@@ -728,17 +728,17 @@ function WifiModule(props: MachineWifiProps) {
); );
console.log("submitting", values, networks); console.log("submitting", values, networks);
const r = await callApi("set_iwd_service_for_machine", { // const r = await callApi("set_iwd_service_for_machine", {
base_url: props.base_url, // base_url: props.base_url,
machine_name: props.machine_name, // machine_name: props.machine_name,
networks: networks, // networks: networks,
}); // });
if (r.status === "error") { // if (r.status === "error") {
toast.error("Failed to set wifi"); toast.error("Failed to set wifi. Feature disabled temporarily");
} // }
if (r.status === "success") { // if (r.status === "success") {
toast.success("Wifi set successfully"); // toast.success("Wifi set successfully");
} // }
}; };
return ( return (