UI: include inventory schema and generated types
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -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;
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user