From 24203666d18b25a6e39c2d239e37aa7f783d26c1 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 11 Jul 2024 16:34:55 +0200 Subject: [PATCH] Webview: add welcome workflow --- pkgs/webview-ui/app/package-lock.json | 52 ++++- pkgs/webview-ui/app/package.json | 2 + pkgs/webview-ui/app/src/App.tsx | 27 ++- pkgs/webview-ui/app/src/Config.tsx | 67 ------- pkgs/webview-ui/app/src/Routes.tsx | 12 ++ .../app/src/components/MachineListItem.tsx | 79 ++++---- pkgs/webview-ui/app/src/layout/header.tsx | 10 +- pkgs/webview-ui/app/src/layout/layout.tsx | 15 +- .../app/src/routes/blockdevices/view.tsx | 20 +- .../app/src/routes/clan/clanDetails.tsx | 182 ++++++------------ pkgs/webview-ui/app/src/routes/clan/view.tsx | 15 +- pkgs/webview-ui/app/src/routes/flash/view.tsx | 23 +-- pkgs/webview-ui/app/src/routes/hosts/view.tsx | 16 +- .../app/src/routes/machines/view.tsx | 139 ++++++------- .../app/src/routes/settings/index.tsx | 98 ++++++++++ .../app/src/routes/welcome/index.tsx | 32 +++ pkgs/webview-ui/flake-module.nix | 2 +- 17 files changed, 412 insertions(+), 379 deletions(-) delete mode 100644 pkgs/webview-ui/app/src/Config.tsx create mode 100644 pkgs/webview-ui/app/src/routes/settings/index.tsx create mode 100644 pkgs/webview-ui/app/src/routes/welcome/index.tsx diff --git a/pkgs/webview-ui/app/package-lock.json b/pkgs/webview-ui/app/package-lock.json index c1bef99a9..e0e9e98f1 100644 --- a/pkgs/webview-ui/app/package-lock.json +++ b/pkgs/webview-ui/app/package-lock.json @@ -10,8 +10,10 @@ "license": "MIT", "dependencies": { "@modular-forms/solid": "^0.21.0", + "@solid-primitives/storage": "^3.7.1", "@tanstack/solid-query": "^5.44.0", "material-icons": "^1.13.12", + "nanoid": "^5.0.7", "solid-js": "^1.8.11", "solid-toast": "^0.5.0" }, @@ -1498,6 +1500,26 @@ "solid-js": "^1.6.12" } }, + "node_modules/@solid-primitives/storage": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-3.7.1.tgz", + "integrity": "sha512-tAmZKQg44RjDjrtWO/5hCOrktQspn/yVV0ySb7yKr7B3CVQlTQtldw3W8UetytJSD9podb9cplvvkq75fgpB1Q==", + "dependencies": { + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "@tauri-apps/plugin-store": "*", + "solid-js": "^1.6.12" + }, + "peerDependenciesMeta": { + "@tauri-apps/plugin-store": { + "optional": true + }, + "solid-start": { + "optional": true + } + } + }, "node_modules/@solid-primitives/styles": { "version": "0.0.111", "resolved": "https://registry.npmjs.org/@solid-primitives/styles/-/styles-0.0.111.tgz", @@ -1515,7 +1537,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz", "integrity": "sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==", - "dev": true, "peerDependencies": { "solid-js": "^1.6.12" } @@ -4177,10 +4198,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", "funding": [ { "type": "github", @@ -4188,10 +4208,10 @@ } ], "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, "node_modules/natural-compare": { @@ -4753,6 +4773,24 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/pkgs/webview-ui/app/package.json b/pkgs/webview-ui/app/package.json index 0d0f87975..c40899c91 100644 --- a/pkgs/webview-ui/app/package.json +++ b/pkgs/webview-ui/app/package.json @@ -39,8 +39,10 @@ }, "dependencies": { "@modular-forms/solid": "^0.21.0", + "@solid-primitives/storage": "^3.7.1", "@tanstack/solid-query": "^5.44.0", "material-icons": "^1.13.12", + "nanoid": "^5.0.7", "solid-js": "^1.8.11", "solid-toast": "^0.5.0" } diff --git a/pkgs/webview-ui/app/src/App.tsx b/pkgs/webview-ui/app/src/App.tsx index 4c15d3627..98061def5 100644 --- a/pkgs/webview-ui/app/src/App.tsx +++ b/pkgs/webview-ui/app/src/App.tsx @@ -1,24 +1,35 @@ import { createSignal, type Component } from "solid-js"; -import { MachineProvider } from "./Config"; import { Layout } from "./layout/layout"; import { Route, Router } from "./Routes"; import { Toaster } from "solid-toast"; +import { effect } from "solid-js/web"; +import { makePersisted } from "@solid-primitives/storage"; // Some global state const [route, setRoute] = createSignal("machines"); export { route, setRoute }; -const [currClanURI, setCurrClanURI] = createSignal(null); -export { currClanURI, setCurrClanURI }; +const [activeURI, setActiveURI] = createSignal(null); +export { activeURI, setActiveURI }; + +const [clanList, setClanList] = makePersisted(createSignal([]), { + name: "clanList", + storage: localStorage, +}); + +export { clanList, setClanList }; const App: Component = () => { + effect(() => { + if (clanList().length === 0) { + setRoute("welcome"); + } + }); return [ , - - - - - , + + + , ]; }; diff --git a/pkgs/webview-ui/app/src/Config.tsx b/pkgs/webview-ui/app/src/Config.tsx deleted file mode 100644 index e73629381..000000000 --- a/pkgs/webview-ui/app/src/Config.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { - createSignal, - createContext, - useContext, - JSXElement, - createEffect, -} from "solid-js"; -import { OperationResponse, pyApi } from "./api"; -import { currClanURI } from "./App"; - -export const makeMachineContext = () => { - const [machines, setMachines] = - createSignal>(); - const [loading, setLoading] = createSignal(false); - - pyApi.list_machines.receive((machines) => { - setLoading(false); - setMachines(machines); - }); - - createEffect(() => { - console.log("The state is now", machines()); - }); - - return [ - { loading, machines }, - { - getMachines: () => { - const clan_dir = currClanURI(); - - if (clan_dir) { - setLoading(true); - pyApi.list_machines.dispatch({ - debug: true, - flake_url: clan_dir, - }); - } - // When the gtk function sends its data the loading state will be set to false - }, - }, - ] as const; - // `as const` forces tuple type inference -}; -type MachineContextType = ReturnType; - -export const MachineContext = createContext([ - { - loading: () => false, - - // eslint-disable-next-line - machines: () => undefined, - }, - { - // eslint-disable-next-line - getMachines: () => {}, - }, -]); - -export const useMachineContext = () => useContext(MachineContext); - -export function MachineProvider(props: { children: JSXElement }) { - return ( - - {props.children} - - ); -} diff --git a/pkgs/webview-ui/app/src/Routes.tsx b/pkgs/webview-ui/app/src/Routes.tsx index 8aa0c07e6..d0c8a27b2 100644 --- a/pkgs/webview-ui/app/src/Routes.tsx +++ b/pkgs/webview-ui/app/src/Routes.tsx @@ -5,6 +5,8 @@ import { CreateClan } from "./routes/clan/view"; import { HostList } from "./routes/hosts/view"; import { BlockDevicesView } from "./routes/blockdevices/view"; import { Flash } from "./routes/flash/view"; +import { Settings } from "./routes/settings"; +import { Welcome } from "./routes/welcome"; export type Route = keyof typeof routes; @@ -39,6 +41,16 @@ export const routes = { label: "Colors", icon: "color_lens", }, + settings: { + child: Settings, + label: "Settings", + icon: "settings", + }, + welcome: { + child: Welcome, + label: "welcome", + icon: "settings", + }, }; interface RouterProps { diff --git a/pkgs/webview-ui/app/src/components/MachineListItem.tsx b/pkgs/webview-ui/app/src/components/MachineListItem.tsx index bb779f6ae..05ee9b61f 100644 --- a/pkgs/webview-ui/app/src/components/MachineListItem.tsx +++ b/pkgs/webview-ui/app/src/components/MachineListItem.tsx @@ -1,6 +1,5 @@ import { Match, Show, Switch, createSignal } from "solid-js"; import { ErrorData, SuccessData, pyApi } from "../api"; -import { currClanURI } from "../App"; type MachineDetails = SuccessData<"list_machines">["data"][string]; @@ -23,51 +22,51 @@ const [deploymentInfo, setDeploymentInfo] = createSignal({}); const [errors, setErrors] = createSignal({}); -pyApi.show_machine_hardware_info.receive((r) => { - const { op_key } = r; - if (r.status === "error") { - console.error(r.errors); - if (op_key) { - setHwInfo((d) => ({ ...d, [op_key]: { system: null } })); - } - return; - } - if (op_key) { - setHwInfo((d) => ({ ...d, [op_key]: r.data })); - } -}); +// pyApi.show_machine_hardware_info.receive((r) => { +// const { op_key } = r; +// if (r.status === "error") { +// console.error(r.errors); +// if (op_key) { +// setHwInfo((d) => ({ ...d, [op_key]: { system: null } })); +// } +// return; +// } +// if (op_key) { +// setHwInfo((d) => ({ ...d, [op_key]: r.data })); +// } +// }); -pyApi.show_machine_deployment_target.receive((r) => { - const { op_key } = r; - if (r.status === "error") { - console.error(r.errors); - if (op_key) { - setDeploymentInfo((d) => ({ ...d, [op_key]: null })); - } - return; - } - if (op_key) { - setDeploymentInfo((d) => ({ ...d, [op_key]: r.data })); - } -}); +// pyApi.show_machine_deployment_target.receive((r) => { +// const { op_key } = r; +// if (r.status === "error") { +// console.error(r.errors); +// if (op_key) { +// setDeploymentInfo((d) => ({ ...d, [op_key]: null })); +// } +// return; +// } +// if (op_key) { +// setDeploymentInfo((d) => ({ ...d, [op_key]: r.data })); +// } +// }); export const MachineListItem = (props: MachineListItemProps) => { const { name, info } = props; - const clan_dir = currClanURI(); - if (clan_dir) { - pyApi.show_machine_hardware_info.dispatch({ - op_key: name, - clan_dir, - machine_name: name, - }); + // const clan_dir = currClanURI(); + // if (clan_dir) { + // pyApi.show_machine_hardware_info.dispatch({ + // op_key: name, + // clan_dir, + // machine_name: name, + // }); - pyApi.show_machine_deployment_target.dispatch({ - op_key: name, - clan_dir, - machine_name: name, - }); - } + // pyApi.show_machine_deployment_target.dispatch({ + // op_key: name, + // clan_dir, + // machine_name: name, + // }); + // } return (
  • diff --git a/pkgs/webview-ui/app/src/layout/header.tsx b/pkgs/webview-ui/app/src/layout/header.tsx index a2b81d56d..badb57a34 100644 --- a/pkgs/webview-ui/app/src/layout/header.tsx +++ b/pkgs/webview-ui/app/src/layout/header.tsx @@ -1,4 +1,4 @@ -import { currClanURI } from "../App"; +import { activeURI, setRoute } from "../App"; export const Header = () => { return ( @@ -14,12 +14,12 @@ export const Header = () => {
    - -
    diff --git a/pkgs/webview-ui/app/src/layout/layout.tsx b/pkgs/webview-ui/app/src/layout/layout.tsx index ab7942626..66bea404d 100644 --- a/pkgs/webview-ui/app/src/layout/layout.tsx +++ b/pkgs/webview-ui/app/src/layout/layout.tsx @@ -1,13 +1,17 @@ -import { Component, JSXElement } from "solid-js"; +import { Component, JSXElement, Show } from "solid-js"; import { Header } from "./header"; import { Sidebar } from "../Sidebar"; import { route, setRoute } from "../App"; +import { effect } from "solid-js/web"; interface LayoutProps { children: JSXElement; } export const Layout: Component = (props) => { + effect(() => { + console.log(route()); + }); return ( <>
    @@ -17,11 +21,16 @@ export const Layout: Component = (props) => { class="drawer-toggle hidden" />
    -
    + +
    + {props.children}
    -
    +
    - } - /> - ); - }} - - - - - {(item) => ( -
    - {item.message} - {item.description} - {item.location} -
    - )} -
    -
    - - ); -}; diff --git a/pkgs/webview-ui/app/src/routes/clan/view.tsx b/pkgs/webview-ui/app/src/routes/clan/view.tsx index 76ca42a0b..2226004c8 100644 --- a/pkgs/webview-ui/app/src/routes/clan/view.tsx +++ b/pkgs/webview-ui/app/src/routes/clan/view.tsx @@ -1,20 +1,9 @@ -import { pyApi } from "@/src/api"; -import { Match, Switch, createEffect, createSignal } from "solid-js"; -import toast from "solid-toast"; -import { ClanDetails, ClanForm } from "./clanDetails"; +import { ClanForm } from "./clanDetails"; export const CreateClan = () => { return (
    - - -
    - } - /> +
    ); }; diff --git a/pkgs/webview-ui/app/src/routes/flash/view.tsx b/pkgs/webview-ui/app/src/routes/flash/view.tsx index 5b4c4578f..db80983f4 100644 --- a/pkgs/webview-ui/app/src/routes/flash/view.tsx +++ b/pkgs/webview-ui/app/src/routes/flash/view.tsx @@ -24,16 +24,17 @@ type BlockDevices = Extract< OperationResponse<"show_block_devices">, { status: "success" } >["data"]["blockdevices"]; + export const Flash = () => { const [formStore, { Form, Field }] = createForm({}); const [devices, setDevices] = createSignal([]); - pyApi.show_block_devices.receive((r) => { - console.log("block devices", r); - if (r.status === "success") { - setDevices(r.data.blockdevices); - } - }); + // pyApi.show_block_devices.receive((r) => { + // console.log("block devices", r); + // if (r.status === "success") { + // setDevices(r.data.blockdevices); + // } + // }); const handleSubmit: SubmitHandler = (values, event) => { // pyApi.open_file.dispatch({ file_request: { mode: "save" } }); @@ -50,11 +51,11 @@ export const Flash = () => { console.log("submit", values); }; - effect(() => { - if (route() === "flash") { - pyApi.show_block_devices.dispatch({}); - } - }); + // effect(() => { + // if (route() === "flash") { + // pyApi.show_block_devices.dispatch({}); + // } + // }); return (
    diff --git a/pkgs/webview-ui/app/src/routes/hosts/view.tsx b/pkgs/webview-ui/app/src/routes/hosts/view.tsx index b12543a4b..68c02e010 100644 --- a/pkgs/webview-ui/app/src/routes/hosts/view.tsx +++ b/pkgs/webview-ui/app/src/routes/hosts/view.tsx @@ -16,15 +16,15 @@ type ServiceModel = Extract< export const HostList: Component = () => { const [services, setServices] = createSignal(); - pyApi.show_mdns.receive((r) => { - const { status } = r; - if (status === "error") return console.error(r.errors); - setServices(r.data.services); - }); + // pyApi.show_mdns.receive((r) => { + // const { status } = r; + // if (status === "error") return console.error(r.errors); + // setServices(r.data.services); + // }); - createEffect(() => { - if (route() === "hosts") pyApi.show_mdns.dispatch({}); - }); + // createEffect(() => { + // if (route() === "hosts") pyApi.show_mdns.dispatch({}); + // }); return (
    diff --git a/pkgs/webview-ui/app/src/routes/machines/view.tsx b/pkgs/webview-ui/app/src/routes/machines/view.tsx index 1298210d0..969033ddc 100644 --- a/pkgs/webview-ui/app/src/routes/machines/view.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/view.tsx @@ -7,117 +7,86 @@ import { createSignal, type Component, } from "solid-js"; -import { useMachineContext } from "../../Config"; -import { route, setCurrClanURI } from "@/src/App"; -import { OperationResponse, pyApi } from "@/src/api"; +import { activeURI, route, setActiveURI } from "@/src/App"; +import { OperationResponse, callApi, pyApi } from "@/src/api"; import toast from "solid-toast"; import { MachineListItem } from "@/src/components/MachineListItem"; -type FilesModel = Extract< - OperationResponse<"get_directory">, - { status: "success" } ->["data"]["files"]; +// type FilesModel = Extract< +// OperationResponse<"get_directory">, +// { status: "success" } +// >["data"]["files"]; -type ServiceModel = Extract< - OperationResponse<"show_mdns">, - { status: "success" } ->["data"]["services"]; +// type ServiceModel = Extract< +// OperationResponse<"show_mdns">, +// { status: "success" } +// >["data"]["services"]; type MachinesModel = Extract< OperationResponse<"list_machines">, { status: "success" } >["data"]; -pyApi.open_file.receive((r) => { - if (r.op_key === "open_clan") { - console.log(r); - if (r.status === "error") return console.error(r.errors); +// pyApi.open_file.receive((r) => { +// if (r.op_key === "open_clan") { +// console.log(r); +// if (r.status === "error") return console.error(r.errors); - if (r.data) { - setCurrClanURI(r.data); - } - } -}); +// if (r.data) { +// setCurrClanURI(r.data); +// } +// } +// }); export const MachineListView: Component = () => { - const [{ machines, loading }, { getMachines }] = useMachineContext(); + // const [files, setFiles] = createSignal([]); - const [files, setFiles] = createSignal([]); - pyApi.get_directory.receive((r) => { - const { status } = r; - if (status === "error") return console.error(r.errors); - setFiles(r.data.files); - }); + // pyApi.get_directory.receive((r) => { + // const { status } = r; + // if (status === "error") return console.error(r.errors); + // setFiles(r.data.files); + // }); - const [services, setServices] = createSignal(); - pyApi.show_mdns.receive((r) => { - const { status } = r; - if (status === "error") return console.error(r.errors); - setServices(r.data.services); - }); + // const [services, setServices] = createSignal(); + // pyApi.show_mdns.receive((r) => { + // const { status } = r; + // if (status === "error") return console.error(r.errors); + // setServices(r.data.services); + // }); - createEffect(() => { - console.log(files()); - }); + const [machines, setMachines] = createSignal({}); + const [loading, setLoading] = createSignal(false); - const [data, setData] = createSignal({}); - createEffect(() => { - if (route() === "machines") getMachines(); - }); - - const unpackedMachines = () => Object.entries(data()); - - createEffect(() => { - const response = machines(); - if (response?.status === "success") { - console.log(response.data); - setData(response.data); - toast.success("Machines loaded"); + const listMachines = async () => { + const uri = activeURI(); + if (!uri) { + return; } - if (response?.status === "error") { - setData({}); - console.error(response.errors); - toast.error("Error loading machines"); - response.errors.forEach((error) => - toast.error( - `${error.message}: ${error.description} From ${error.location}` - ) - ); + setLoading(true); + const response = await callApi("list_machines", { + flake_url: uri, + }); + setLoading(false); + if (response.status === "success") { + setMachines(response.data); } + }; + + createEffect(() => { + if (route() === "machines") listMachines(); }); + const unpackedMachines = () => Object.entries(machines()); + return (
    -
    - -
    -
    - -
    +
    -
    - + {/* {(services) => ( {(service) => ( @@ -163,7 +132,7 @@ export const MachineListView: Component = () => { )} )} - + */} {/* Loading skeleton */} diff --git a/pkgs/webview-ui/app/src/routes/settings/index.tsx b/pkgs/webview-ui/app/src/routes/settings/index.tsx new file mode 100644 index 000000000..2891d2b16 --- /dev/null +++ b/pkgs/webview-ui/app/src/routes/settings/index.tsx @@ -0,0 +1,98 @@ +import { callApi } from "@/src/api"; +import { + SubmitHandler, + createForm, + required, + setValue, +} from "@modular-forms/solid"; +import { activeURI, setClanList, setActiveURI, setRoute } from "@/src/App"; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type SettingsForm = { + base_dir: string | null; +}; + +export const registerClan = async () => { + try { + const loc = await callApi("open_file", { + file_request: { mode: "select_folder" }, + }); + console.log(loc); + if (loc.status === "success" && loc.data) { + // @ts-expect-error: data is a string + setClanList((s) => [...s, loc.data]); + setRoute((r) => { + if (r === "welcome") return "machines"; + return r; + }); + return loc.data; + } + } catch (e) { + // + } +}; + +export const Settings = () => { + const [formStore, { Form, Field }] = createForm({ + initialValues: { + base_dir: activeURI(), + }, + }); + + const handleSubmit: SubmitHandler = async (values, event) => { + // + }; + + return ( +
    + +
    + + {(field, props) => ( + + )} + +
    + +
    + ); +}; diff --git a/pkgs/webview-ui/app/src/routes/welcome/index.tsx b/pkgs/webview-ui/app/src/routes/welcome/index.tsx new file mode 100644 index 000000000..29f90865a --- /dev/null +++ b/pkgs/webview-ui/app/src/routes/welcome/index.tsx @@ -0,0 +1,32 @@ +import { setActiveURI, setRoute } from "@/src/App"; +import { registerClan } from "../settings"; + +export const Welcome = () => { + return ( +
    +
    +
    +

    Welcome to Clan

    +

    Own the services you use.

    +
    + + +
    +
    +
    +
    + ); +}; diff --git a/pkgs/webview-ui/flake-module.nix b/pkgs/webview-ui/flake-module.nix index 69be07bbc..8af1360cd 100644 --- a/pkgs/webview-ui/flake-module.nix +++ b/pkgs/webview-ui/flake-module.nix @@ -16,7 +16,7 @@ npmDeps = pkgs.fetchNpmDeps { src = ./app; - hash = "sha256-3LjcHh+jCuarh9XmS+mOv7xaGgAHxf3L7fWnxxmxUGQ="; + hash = "sha256-U8FwGL0FelUZwa8NjitfsFNDSofUPbp+nHrypeDj2Po="; }; # The prepack script runs the build script, which we'd rather do in the build phase. npmPackFlags = [ "--ignore-scripts" ];