From e1eaf44ae5ad4a2ef70551f18e4b7c529ac0ba4e Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 12 Sep 2024 18:33:47 +0200 Subject: [PATCH] UI: update gitignore --- pkgs/webview-ui/.gitignore | 2 +- pkgs/webview-ui/app/src/api/disk.ts | 31 +++++ pkgs/webview-ui/app/src/api/index.ts | 168 +++++++++++++++++++++++ pkgs/webview-ui/app/src/api/inventory.ts | 40 ++++++ pkgs/webview-ui/app/src/api/wifi.ts | 18 +++ 5 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 pkgs/webview-ui/app/src/api/disk.ts create mode 100644 pkgs/webview-ui/app/src/api/index.ts create mode 100644 pkgs/webview-ui/app/src/api/inventory.ts create mode 100644 pkgs/webview-ui/app/src/api/wifi.ts diff --git a/pkgs/webview-ui/.gitignore b/pkgs/webview-ui/.gitignore index 590601772..9a800f70d 100644 --- a/pkgs/webview-ui/.gitignore +++ b/pkgs/webview-ui/.gitignore @@ -1,3 +1,3 @@ -api +app/api .vite \ No newline at end of file diff --git a/pkgs/webview-ui/app/src/api/disk.ts b/pkgs/webview-ui/app/src/api/disk.ts new file mode 100644 index 000000000..592451ff1 --- /dev/null +++ b/pkgs/webview-ui/app/src/api/disk.ts @@ -0,0 +1,31 @@ +import { get_inventory } from "./inventory"; + +export const instance_name = (machine_name: string) => + `${machine_name}-single-disk` as const; + +export async function set_single_disk_id( + base_path: string, + machine_name: string, + disk_id: string, +) { + const inventory = await get_inventory(base_path); + if (!inventory.services) { + return new Error("No services found in inventory"); + } + if (!inventory.services["single-disk"]) { + inventory.services["single-disk"] = {}; + } + inventory.services["single-disk"][instance_name(machine_name)] = { + meta: { + name: instance_name(machine_name), + }, + roles: { + default: { + machines: [machine_name], + config: { + device: `/dev/disk/by-id/${disk_id}`, + }, + }, + }, + }; +} diff --git a/pkgs/webview-ui/app/src/api/index.ts b/pkgs/webview-ui/app/src/api/index.ts new file mode 100644 index 000000000..9bf104c68 --- /dev/null +++ b/pkgs/webview-ui/app/src/api/index.ts @@ -0,0 +1,168 @@ +import schema from "@/api/API.json" assert { type: "json" }; +import { API } from "@/api/API"; +import { nanoid } from "nanoid"; +import { Schema as Inventory } from "@/api/Inventory"; + +export type OperationNames = keyof API; +export type OperationArgs = API[T]["arguments"]; +export type OperationResponse = API[T]["return"]; + +export type Services = NonNullable; +export type ServiceNames = keyof Services; +export type ClanService = Services[T]; +export type ClanServiceInstance = NonNullable< + Services[T] +>[string]; + +export type SuccessQuery = Extract< + OperationResponse, + { status: "success" } +>; +export type SuccessData = SuccessQuery["data"]; + +export type ErrorQuery = Extract< + OperationResponse, + { status: "error" } +>; +export type ErrorData = ErrorQuery["errors"]; + +export type ClanOperations = { + [K in OperationNames]: (str: string) => void; +}; + +export interface GtkResponse { + result: T; + op_key: string; +} + +declare global { + interface Window { + clan: ClanOperations; + webkit: { + messageHandlers: { + gtk: { + postMessage: (message: { + method: OperationNames; + data: OperationArgs; + }) => 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) => void + >; +}; +const registry: ObserverRegistry = operationNames.reduce( + (acc, opName) => ({ + ...acc, + [opName]: {}, + }), + {} as ObserverRegistry, +); + +function createFunctions( + operationName: K, +): { + dispatch: (args: OperationArgs) => void; + receive: (fn: (response: OperationResponse) => void, id: string) => void; +} { + window.clan[operationName] = (s: string) => { + const f = (response: OperationResponse) => { + // 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) => { + // Send the data to the gtk app + window.webkit.messageHandlers.gtk.postMessage({ + method: operationName, + data: args, + }); + }, + receive: (fn: (response: OperationResponse) => 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) => void; + receive: (fn: (response: OperationResponse) => 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 = ( + method: K, + args: OperationArgs, +) => { + return new Promise>((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 = + (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 }; diff --git a/pkgs/webview-ui/app/src/api/inventory.ts b/pkgs/webview-ui/app/src/api/inventory.ts new file mode 100644 index 000000000..9ad4aa468 --- /dev/null +++ b/pkgs/webview-ui/app/src/api/inventory.ts @@ -0,0 +1,40 @@ +import { callApi, ClanService, ServiceNames, Services } from "."; +import { Schema as Inventory } from "@/api/Inventory"; + +export async function get_inventory(base_path: string) { + const r = await callApi("get_inventory", { + base_path, + }); + if (r.status == "error") { + throw new Error("Failed to get inventory"); + } + const inventory: Inventory = r.data; + return inventory; +} + +export const single_instance_name = ( + machine_name: string, + service_name: T, +) => `${machine_name}_${service_name}_0` as const; + +function get_service(base_path: string, service: T) { + return callApi("get_inventory", { base_path }).then((r) => { + if (r.status == "error") { + return null; + } + const inventory: Inventory = r.data; + + const serviceInstance = inventory.services?.[service]; + return serviceInstance; + }); +} + +export async function get_single_service( + base_path: string, + machine_name: string, + service_name: T, +) { + const instance_key = single_instance_name(machine_name, service_name); + const service = await get_service(base_path, "admin"); + return service?.[instance_key]; +} diff --git a/pkgs/webview-ui/app/src/api/wifi.ts b/pkgs/webview-ui/app/src/api/wifi.ts new file mode 100644 index 000000000..d6ae89785 --- /dev/null +++ b/pkgs/webview-ui/app/src/api/wifi.ts @@ -0,0 +1,18 @@ +import { callApi } from "."; +import { Schema as Inventory } from "@/api/Inventory"; + +export const instance_name = (machine_name: string) => + `${machine_name}_wifi_0` as const; + +export async function get_iwd_service(base_path: string, machine_name: string) { + const r = await callApi("get_inventory", { + base_path, + }); + if (r.status == "error") { + return null; + } + const inventory: Inventory = r.data; + + const instance_key = instance_name(machine_name); + return inventory.services?.iwd?.[instance_key] || null; +}