From 4c4f55f309ad865488fe461df5f2886e0b5cde68 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 10 Jul 2024 15:11:45 +0200 Subject: [PATCH] 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 - /> - - + } + />
); };