From dfec6afd6bbf28cff78e36627b923af175a53fad Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 10 Jul 2024 15:11:45 +0200 Subject: [PATCH 1/8] Clan create: migrate to inventory --- inventory.json | 2 +- pkgs/clan-app/README.md | 1 - pkgs/clan-cli/clan_cli/clan/create.py | 24 +++--- pkgs/clan-cli/clan_cli/clan/show.py | 18 ++--- pkgs/clan-cli/clan_cli/clan/update.py | 29 ++----- pkgs/clan-cli/clan_cli/inventory/__init__.py | 13 ++- pkgs/webview-ui/app/src/Routes.tsx | 8 +- .../app/src/routes/clan/clanDetails.tsx | 62 ++++++-------- pkgs/webview-ui/app/src/routes/clan/view.tsx | 80 +++---------------- 9 files changed, 79 insertions(+), 158 deletions(-) diff --git a/inventory.json b/inventory.json index 207630e57..478f177c7 100644 --- a/inventory.json +++ b/inventory.json @@ -1,6 +1,6 @@ { "meta": { - "name": "Minimal inventory" + "name": "clan-core" }, "machines": { "minimal-inventory-machine": { diff --git a/pkgs/clan-app/README.md b/pkgs/clan-app/README.md index 7d69788be..29880562b 100644 --- a/pkgs/clan-app/README.md +++ b/pkgs/clan-app/README.md @@ -28,7 +28,6 @@ Follow the instructions below to set up your development environment and start t This will start the application in debug mode and link it to the web server running at `http://localhost:3000`. - # clan app (old) Provides users with the simple functionality to manage their locally registered clans. diff --git a/pkgs/clan-cli/clan_cli/clan/create.py b/pkgs/clan-cli/clan_cli/clan/create.py index e198ffa0e..afefbdd6f 100644 --- a/pkgs/clan-cli/clan_cli/clan/create.py +++ b/pkgs/clan-cli/clan_cli/clan/create.py @@ -1,12 +1,12 @@ # !/usr/bin/env python3 import argparse -import json import os from dataclasses import dataclass, fields from pathlib import Path from clan_cli.api import API from clan_cli.arg_actions import AppendOptionAction +from clan_cli.inventory import Inventory, InventoryMeta from ..cmd import CmdOut, run from ..errors import ClanError @@ -24,19 +24,12 @@ class CreateClanResponse: flake_update: CmdOut -@dataclass -class ClanMetaInfo: - name: str - description: str | None - icon: str | None - - @dataclass class CreateOptions: directory: Path | str # Metadata for the clan # Metadata can be shown with `clan show` - meta: ClanMetaInfo | None = None + meta: InventoryMeta | None = None # URL to the template to use. Defaults to the "minimal" template template_url: str = minimal_template_url @@ -70,17 +63,18 @@ def create_clan(options: CreateOptions) -> CreateClanResponse: ) out = run(command, cwd=directory) - # Write meta.json file if meta is provided + # Write inventory.json file + inventory = Inventory.load_file(directory) if options.meta is not None: - meta_file = Path(directory / "clan/meta.json") - meta_file.parent.mkdir(parents=True, exist_ok=True) - with open(meta_file, "w") as f: - json.dump(options.meta.__dict__, f) + inventory.meta = options.meta command = nix_shell(["nixpkgs#git"], ["git", "init"]) out = run(command, cwd=directory) cmd_responses["git init"] = out + # Persist also create a commit message for each change + inventory.persist(directory, "Init inventory") + command = nix_shell(["nixpkgs#git"], ["git", "add", "."]) out = run(command, cwd=directory) cmd_responses["git add"] = out @@ -118,7 +112,7 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--meta", - help=f"""Metadata to set for the clan. Available options are: {", ".join([f.name for f in fields(ClanMetaInfo)]) }""", + help=f"""Metadata to set for the clan. Available options are: {", ".join([f.name for f in fields(InventoryMeta)]) }""", nargs=2, metavar=("name", "value"), action=AppendOptionAction, diff --git a/pkgs/clan-cli/clan_cli/clan/show.py b/pkgs/clan-cli/clan_cli/clan/show.py index dbae165b6..670646523 100644 --- a/pkgs/clan-cli/clan_cli/clan/show.py +++ b/pkgs/clan-cli/clan_cli/clan/show.py @@ -5,8 +5,8 @@ from pathlib import Path from urllib.parse import urlparse from clan_cli.api import API -from clan_cli.clan.create import ClanMetaInfo from clan_cli.errors import ClanCmdError, ClanError +from clan_cli.inventory import InventoryMeta from ..cmd import run_no_stdout from ..nix import nix_eval @@ -15,10 +15,10 @@ log = logging.getLogger(__name__) @API.register -def show_clan_meta(uri: str | Path) -> ClanMetaInfo: +def show_clan_meta(uri: str | Path) -> InventoryMeta: cmd = nix_eval( [ - f"{uri}#clanInternals.meta", + f"{uri}#clanInternals.inventory.meta", "--json", ] ) @@ -27,11 +27,11 @@ def show_clan_meta(uri: str | Path) -> ClanMetaInfo: try: proc = run_no_stdout(cmd) res = proc.stdout.strip() - except ClanCmdError: + except ClanCmdError as e: raise ClanError( - "Clan might not have meta attributes", + "Evaluation failed on meta attribute", location=f"show_clan {uri}", - description="Evaluation failed on clanInternals.meta attribute", + description=str(e.cmd), ) clan_meta = json.loads(res) @@ -61,7 +61,7 @@ def show_clan_meta(uri: str | Path) -> ClanMetaInfo: description="Icon path must be a URL or a relative path.", ) - return ClanMetaInfo( + return InventoryMeta( name=clan_meta.get("name"), description=clan_meta.get("description", None), icon=icon_path, @@ -73,8 +73,8 @@ def show_command(args: argparse.Namespace) -> None: meta = show_clan_meta(flake_path) print(f"Name: {meta.name}") - print(f"Description: {meta.description or ''}") - print(f"Icon: {meta.icon or ''}") + print(f"Description: {meta.description or '-'}") + print(f"Icon: {meta.icon or '-'}") def register_parser(parser: argparse.ArgumentParser) -> None: diff --git a/pkgs/clan-cli/clan_cli/clan/update.py b/pkgs/clan-cli/clan_cli/clan/update.py index 4dcc33d77..3f73756ed 100644 --- a/pkgs/clan-cli/clan_cli/clan/update.py +++ b/pkgs/clan-cli/clan_cli/clan/update.py @@ -1,35 +1,20 @@ -import json from dataclasses import dataclass -from pathlib import Path from clan_cli.api import API -from clan_cli.clan.create import ClanMetaInfo -from clan_cli.errors import ClanError +from clan_cli.inventory import Inventory, InventoryMeta @dataclass class UpdateOptions: directory: str - meta: ClanMetaInfo | None = None + meta: InventoryMeta @API.register -def update_clan_meta(options: UpdateOptions) -> ClanMetaInfo: - meta_file = Path(options.directory) / Path("clan/meta.json") - if not meta_file.exists(): - raise ClanError( - "File not found", - description=f"Could not find {meta_file} to update.", - location="update_clan_meta", - ) +def update_clan_meta(options: UpdateOptions) -> InventoryMeta: + inventory = Inventory.load_file(options.directory) + inventory.meta = options.meta - meta_content: dict[str, str] = {} - with open(meta_file) as f: - meta_content = json.load(f) + inventory.persist(options.directory, "Update clan meta") - meta_content = {**meta_content, **options.meta.__dict__} - - with open(meta_file) as f: - json.dump(meta_content, f) - - return ClanMetaInfo(**meta_content) + return inventory.meta diff --git a/pkgs/clan-cli/clan_cli/inventory/__init__.py b/pkgs/clan-cli/clan_cli/inventory/__init__.py index f774fd70a..00fb00972 100644 --- a/pkgs/clan-cli/clan_cli/inventory/__init__.py +++ b/pkgs/clan-cli/clan_cli/inventory/__init__.py @@ -99,14 +99,23 @@ class Service: ) +@dataclass +class InventoryMeta: + name: str + description: str | None = None + icon: str | None = None + + @dataclass class Inventory: + meta: InventoryMeta machines: dict[str, Machine] services: dict[str, dict[str, Service]] @staticmethod def from_dict(d: dict[str, Any]) -> "Inventory": return Inventory( + meta=InventoryMeta(**d.get("meta", {})), machines={ name: Machine.from_dict(machine) for name, machine in d.get("machines", {}).items() @@ -126,7 +135,9 @@ class Inventory: @staticmethod def load_file(flake_dir: str | Path) -> "Inventory": - inventory = Inventory(machines={}, services={}) + inventory = Inventory( + machines={}, services={}, meta=InventoryMeta(name="New Clan") + ) inventory_file = Inventory.get_path(flake_dir) if inventory_file.exists(): with open(inventory_file) as f: diff --git a/pkgs/webview-ui/app/src/Routes.tsx b/pkgs/webview-ui/app/src/Routes.tsx index 1beebdfa8..8aa0c07e6 100644 --- a/pkgs/webview-ui/app/src/Routes.tsx +++ b/pkgs/webview-ui/app/src/Routes.tsx @@ -1,7 +1,7 @@ import { Accessor, For, Match, Switch } from "solid-js"; import { MachineListView } from "./routes/machines/view"; import { colors } from "./routes/colors/view"; -import { clan } from "./routes/clan/view"; +import { CreateClan } from "./routes/clan/view"; import { HostList } from "./routes/hosts/view"; import { BlockDevicesView } from "./routes/blockdevices/view"; import { Flash } from "./routes/flash/view"; @@ -9,9 +9,9 @@ import { Flash } from "./routes/flash/view"; export type Route = keyof typeof routes; export const routes = { - clan: { - child: clan, - label: "Clan", + createClan: { + child: CreateClan, + label: "Create Clan", icon: "groups", }, machines: { diff --git a/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx b/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx index 74eb6ee39..e293adca2 100644 --- a/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx +++ b/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx @@ -8,45 +8,49 @@ import { createEffect, createSignal, } from "solid-js"; -import { - SubmitHandler, - createForm, - email, - required, -} from "@modular-forms/solid"; +import { SubmitHandler, createForm, required } from "@modular-forms/solid"; +import toast from "solid-toast"; +import { effect } from "solid-js/web"; interface ClanDetailsProps { directory: string; } interface ClanFormProps { - directory?: string; - meta: ClanMeta; actions: JSX.Element; - editable?: boolean; } export const ClanForm = (props: ClanFormProps) => { - const { meta, actions, editable = true, directory } = props; + const { actions } = props; const [formStore, { Form, Field }] = createForm({ - initialValues: meta, + initialValues: {}, }); const handleSubmit: SubmitHandler = (values, event) => { - pyApi.open_file.dispatch({ file_request: { mode: "save" } }); - pyApi.open_file.receive((r) => { - if (r.status === "success") { - if (r.data) { - pyApi.create_clan.dispatch({ - options: { directory: r.data, meta: values }, - }); - } + console.log("submit", values); + pyApi.open_file.dispatch({ + file_request: { mode: "save" }, + op_key: "create_clan", + }); + pyApi.open_file.receive((r) => { + if (r.op_key !== "create_clan") { return; } + + if (r.status !== "success") { + toast.error("Failed to create clan"); + return; + } + + if (r.data) { + pyApi.create_clan.dispatch({ + options: { directory: r.data, meta: values }, + }); + } }); - console.log("submit", values); }; + return (
@@ -71,20 +75,6 @@ export const ClanForm = (props: ClanFormProps) => { )} - )} @@ -104,9 +94,8 @@ export const ClanForm = (props: ClanFormProps) => { { const meta = data(); return ( diff --git a/pkgs/webview-ui/app/src/routes/clan/view.tsx b/pkgs/webview-ui/app/src/routes/clan/view.tsx index d1ced9734..049eafd4a 100644 --- a/pkgs/webview-ui/app/src/routes/clan/view.tsx +++ b/pkgs/webview-ui/app/src/routes/clan/view.tsx @@ -3,80 +3,24 @@ import { Match, Switch, createEffect, createSignal } from "solid-js"; import toast from "solid-toast"; import { ClanDetails, ClanForm } from "./clanDetails"; -export const clan = () => { - const [mode, setMode] = createSignal<"init" | "open" | "create">("init"); +export const CreateClan = () => { + // const [mode, setMode] = createSignal<"init" | "open" | "create">("init"); const [clanDir, setClanDir] = createSignal(null); - createEffect(() => { - console.log(mode()); - }); + // createEffect(() => { + // console.log(mode()); + // }); return (
- - -
- -
-
- - - - - - -
- } - meta={{ - name: "New Clan", - description: "nice description", - icon: "select icon", - }} - editable - /> - - + } + />
); }; From d2e94b81882cabf24a30a03fa9e359c3a8e2dba6 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 10 Jul 2024 16:56:37 +0200 Subject: [PATCH 2/8] API: improve json serialization --- pkgs/clan-app/clan_app/views/webview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/clan-app/clan_app/views/webview.py b/pkgs/clan-app/clan_app/views/webview.py index 887742be5..5900fdf3b 100644 --- a/pkgs/clan-app/clan_app/views/webview.py +++ b/pkgs/clan-app/clan_app/views/webview.py @@ -21,7 +21,7 @@ log = logging.getLogger(__name__) def sanitize_string(s: str) -> str: - return s.replace("\\", "\\\\").replace('"', '\\"') + return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") def dataclass_to_dict(obj: Any) -> Any: From 8077053100351d9396f68507466da9d5b3cceb7f Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 10 Jul 2024 16:57:13 +0200 Subject: [PATCH 3/8] Webview: improve error debug abilities --- pkgs/webview-ui/app/src/api.ts | 18 ++++++++++++++++++ pkgs/webview-ui/app/util.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 pkgs/webview-ui/app/util.ts diff --git a/pkgs/webview-ui/app/src/api.ts b/pkgs/webview-ui/app/src/api.ts index fd4d5dec9..a3c342863 100644 --- a/pkgs/webview-ui/app/src/api.ts +++ b/pkgs/webview-ui/app/src/api.ts @@ -94,12 +94,30 @@ type PyApi = { }; }; +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); +} + const deserialize = (fn: (response: T) => void) => (str: string) => { try { fn(JSON.parse(str) as T); } catch (e) { + console.log("Error parsing JSON: ", e); + console.log({ download: () => download("error.json", str) }); console.error(str); alert(`Error parsing JSON: ${e}`); } diff --git a/pkgs/webview-ui/app/util.ts b/pkgs/webview-ui/app/util.ts new file mode 100644 index 000000000..eaeeab027 --- /dev/null +++ b/pkgs/webview-ui/app/util.ts @@ -0,0 +1,33 @@ +export function isValidHostname(value: string | null | undefined) { + if (typeof value !== "string") return false; + + const validHostnameChars = /^[a-zA-Z0-9-.]{1,253}\.?$/g; + + if (!validHostnameChars.test(value)) { + return false; + } + + if (value.endsWith(".")) { + value = value.slice(0, value.length - 1); + } + + if (value.length > 253) { + return false; + } + + const labels = value.split("."); + + const isValid = labels.every(function (label) { + const validLabelChars = /^([a-zA-Z0-9-]+)$/g; + + const validLabel = + validLabelChars.test(label) && + label.length < 64 && + !label.startsWith("-") && + !label.endsWith("-"); + + return validLabel; + }); + + return isValid; +} From 060f020d83a1e9b9644e767540074cf228a61491 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 10 Jul 2024 16:57:50 +0200 Subject: [PATCH 4/8] Webview: add feeback for clan create workflow --- .../app/src/routes/clan/clanDetails.tsx | 48 +++++++++++++++---- pkgs/webview-ui/app/src/routes/clan/view.tsx | 6 --- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx b/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx index e293adca2..3061054d6 100644 --- a/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx +++ b/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx @@ -10,7 +10,8 @@ import { } from "solid-js"; import { SubmitHandler, createForm, required } from "@modular-forms/solid"; import toast from "solid-toast"; -import { effect } from "solid-js/web"; +import { setCurrClanURI, setRoute } from "@/src/App"; +import { isValidHostname } from "@/util"; interface ClanDetailsProps { directory: string; @@ -29,7 +30,10 @@ export const ClanForm = (props: ClanFormProps) => { const handleSubmit: SubmitHandler = (values, event) => { console.log("submit", values); pyApi.open_file.dispatch({ - file_request: { mode: "save" }, + file_request: { + mode: "save", + }, + op_key: "create_clan", }); @@ -37,17 +41,45 @@ export const ClanForm = (props: ClanFormProps) => { if (r.op_key !== "create_clan") { return; } - if (r.status !== "success") { - toast.error("Failed to create clan"); + toast.error("Cannot select clan directory"); + return; + } + const target_dir = r?.data; + if (!target_dir) { + toast.error("Cannot select clan directory"); return; } - if (r.data) { - pyApi.create_clan.dispatch({ - options: { directory: r.data, meta: values }, - }); + if (!isValidHostname(target_dir)) { + toast.error(`Directory name must be valid URI: ${target_dir}`); + return; } + + toast.promise( + new Promise((resolve, reject) => { + pyApi.create_clan.receive((r) => { + if (r.status === "error") { + reject(); + console.error(r.errors); + } + resolve(); + // Navigate to the new clan + setCurrClanURI(target_dir); + setRoute("machines"); + }); + + pyApi.create_clan.dispatch({ + options: { directory: target_dir, meta: values }, + op_key: "create_clan", + }); + }), + { + loading: "Creating clan...", + success: "Clan Successfully Created", + error: "Failed to create clan", + } + ); }); }; diff --git a/pkgs/webview-ui/app/src/routes/clan/view.tsx b/pkgs/webview-ui/app/src/routes/clan/view.tsx index 049eafd4a..76ca42a0b 100644 --- a/pkgs/webview-ui/app/src/routes/clan/view.tsx +++ b/pkgs/webview-ui/app/src/routes/clan/view.tsx @@ -4,12 +4,6 @@ import toast from "solid-toast"; import { ClanDetails, ClanForm } from "./clanDetails"; export const CreateClan = () => { - // const [mode, setMode] = createSignal<"init" | "open" | "create">("init"); - const [clanDir, setClanDir] = createSignal(null); - - // createEffect(() => { - // console.log(mode()); - // }); return (
Date: Wed, 10 Jul 2024 18:08:52 +0200 Subject: [PATCH 5/8] Clan create: add template url field --- pkgs/webview-ui/app/src/index.css | 6 + .../app/src/routes/clan/clanDetails.tsx | 170 ++++++++++-------- 2 files changed, 103 insertions(+), 73 deletions(-) diff --git a/pkgs/webview-ui/app/src/index.css b/pkgs/webview-ui/app/src/index.css index 0fd274ce6..169082015 100644 --- a/pkgs/webview-ui/app/src/index.css +++ b/pkgs/webview-ui/app/src/index.css @@ -3,3 +3,9 @@ @tailwind base; @tailwind components; @tailwind utilities; + + +html { + overflow-x: hidden; + overflow-y: scroll; +} \ No newline at end of file diff --git a/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx b/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx index 3061054d6..bd11871d6 100644 --- a/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx +++ b/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx @@ -8,7 +8,12 @@ import { createEffect, createSignal, } from "solid-js"; -import { SubmitHandler, createForm, required } from "@modular-forms/solid"; +import { + SubmitHandler, + createForm, + required, + custom, +} from "@modular-forms/solid"; import toast from "solid-toast"; import { setCurrClanURI, setRoute } from "@/src/App"; import { isValidHostname } from "@/util"; @@ -21,14 +26,21 @@ interface ClanFormProps { actions: JSX.Element; } +type CreateForm = Meta & { + template_url: string; +}; + export const ClanForm = (props: ClanFormProps) => { const { actions } = props; - const [formStore, { Form, Field }] = createForm({ - initialValues: {}, + const [formStore, { Form, Field }] = createForm({ + initialValues: { + template_url: "git+https://git.clan.lol/clan/clan-core#templates.minimal", + }, }); - const handleSubmit: SubmitHandler = (values, event) => { + const handleSubmit: SubmitHandler = (values, event) => { console.log("submit", values); + const { template_url, ...meta } = values; pyApi.open_file.dispatch({ file_request: { mode: "save", @@ -70,7 +82,7 @@ export const ClanForm = (props: ClanFormProps) => { }); pyApi.create_clan.dispatch({ - options: { directory: target_dir, meta: values }, + options: { directory: target_dir, meta, template_url }, op_key: "create_clan", }); }), @@ -84,7 +96,7 @@ export const ClanForm = (props: ClanFormProps) => { }; return ( -
+
{(field, props) => ( @@ -111,79 +123,92 @@ export const ClanForm = (props: ClanFormProps) => { )}
-
- - {(field, props) => ( -
+ +
+ {field.error && ( + {field.error} + )} +
+ + )} + + + {(field, props) => ( +
+ + +
+ +
+
+ )} +
+ {actions}
); }; -// export const EditMetaFields = (props: MetaFieldsProps) => { -// const { meta, editable, actions, directory } = props; - -// const [editing, setEditing] = createSignal< -// keyof MetaFieldsProps["meta"] | null -// >(null); -// return ( - -// ); -// }; - -type ClanMeta = Extract< +type Meta = Extract< OperationResponse<"show_clan_meta">, { status: "success" } >["data"]; @@ -198,7 +223,7 @@ export const ClanDetails = (props: ClanDetailsProps) => { >["errors"] | null >(null); - const [data, setData] = createSignal(); + const [data, setData] = createSignal(); const loadMeta = () => { pyApi.show_clan_meta.dispatch({ uri: directory }); @@ -226,7 +251,6 @@ export const ClanDetails = (props: ClanDetailsProps) => { const meta = data(); return (
- -
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" ]; From b324e1a4f4695e6baa1b607684fdc1c5c4176353 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Thu, 11 Jul 2024 17:05:57 +0200 Subject: [PATCH 8/8] Fix some type issues --- pkgs/clan-cli/clan_cli/clan/create.py | 17 +++++++++-------- pkgs/clan-cli/tests/test_vars.py | 4 +++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/clan/create.py b/pkgs/clan-cli/clan_cli/clan/create.py index afefbdd6f..29cb94a42 100644 --- a/pkgs/clan-cli/clan_cli/clan/create.py +++ b/pkgs/clan-cli/clan_cli/clan/create.py @@ -63,18 +63,11 @@ def create_clan(options: CreateOptions) -> CreateClanResponse: ) out = run(command, cwd=directory) - # Write inventory.json file - inventory = Inventory.load_file(directory) - if options.meta is not None: - inventory.meta = options.meta - + ## Begin: setup git command = nix_shell(["nixpkgs#git"], ["git", "init"]) out = run(command, cwd=directory) cmd_responses["git init"] = out - # Persist also create a commit message for each change - inventory.persist(directory, "Init inventory") - command = nix_shell(["nixpkgs#git"], ["git", "add", "."]) out = run(command, cwd=directory) cmd_responses["git add"] = out @@ -88,6 +81,14 @@ def create_clan(options: CreateOptions) -> CreateClanResponse: ) out = run(command, cwd=directory) cmd_responses["git config"] = out + ## End: setup git + + # Write inventory.json file + inventory = Inventory.load_file(directory) + if options.meta is not None: + inventory.meta = options.meta + # Persist creates a commit message for each change + inventory.persist(directory, "Init inventory") command = ["nix", "flake", "update"] out = run(command, cwd=directory) diff --git a/pkgs/clan-cli/tests/test_vars.py b/pkgs/clan-cli/tests/test_vars.py index bf41ee272..4abeaff69 100644 --- a/pkgs/clan-cli/tests/test_vars.py +++ b/pkgs/clan-cli/tests/test_vars.py @@ -1,6 +1,8 @@ import os from collections import defaultdict +from collections.abc import Callable from pathlib import Path +from typing import Any import pytest from age_keys import SopsSetup @@ -14,7 +16,7 @@ def def_value() -> defaultdict: # allows defining nested dictionary in a single line -nested_dict = lambda: defaultdict(def_value) +nested_dict: Callable[[], dict[str, Any]] = lambda: defaultdict(def_value) @pytest.mark.impure