Clan create: migrate to inventory

This commit is contained in:
Johannes Kirschbauer 2024-07-10 15:11:45 +02:00
parent 1a125cc9e7
commit dfec6afd6b
Signed by: hsjobeki
SSH Key Fingerprint: SHA256:vX3utDqig7Ph5L0JPv87ZTPb/w7cMzREKVZzzLFg9qU
9 changed files with 79 additions and 158 deletions

View File

@ -1,6 +1,6 @@
{
"meta": {
"name": "Minimal inventory"
"name": "clan-core"
},
"machines": {
"minimal-inventory-machine": {

View File

@ -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.

View File

@ -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,

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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: {

View File

@ -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<ClanMeta>({
initialValues: meta,
initialValues: {},
});
const handleSubmit: SubmitHandler<ClanMeta> = (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 (
<div class="card card-compact w-96 bg-base-100 shadow-xl">
<Form onSubmit={handleSubmit}>
@ -71,20 +75,6 @@ export const ClanForm = (props: ClanFormProps) => {
)}
</Show>
</figure>
<label class="form-control w-full max-w-xs">
<div class="label">
<span class="label-text">Select icon</span>
</div>
<input
type="file"
class="file-input file-input-bordered w-full max-w-xs"
/>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
</>
)}
</Field>
@ -104,9 +94,8 @@ export const ClanForm = (props: ClanFormProps) => {
<input
{...props}
type="email"
required
placeholder="your.mail@example.com"
placeholder="Clan Name"
class="input w-full max-w-xs"
classList={{ "input-error": !!field.error }}
value={field.value}
@ -205,7 +194,6 @@ export const ClanDetails = (props: ClanDetailsProps) => {
const meta = data();
return (
<ClanForm
directory={directory}
meta={meta}
actions={
<div class="card-actions justify-between">

View File

@ -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<string | null>(null);
createEffect(() => {
console.log(mode());
});
// createEffect(() => {
// console.log(mode());
// });
return (
<div>
<Switch fallback={"invalid"}>
<Match when={mode() === "init"}>
<div class="flex gap-2">
<button class="btn btn-square" onclick={() => setMode("create")}>
<span class="material-icons">add</span>
</button>
<button
class="btn btn-square"
onclick={() => {
pyApi.open_file.dispatch({
file_request: {
mode: "select_folder",
title: "Open Clan",
},
});
pyApi.open_file.receive((r) => {
// There are two error cases to handle
if (r.status !== "success") {
console.error(r.errors);
toast.error("Error opening clan");
return;
}
// User didn't select anything
if (!r.data) {
setMode("init");
return;
}
setClanDir(r.data);
setMode("open");
});
}}
>
<span class="material-icons">folder_open</span>
<ClanForm
actions={
<div class="card-actions justify-end">
<button class="btn btn-primary" type="submit">
Create
</button>
</div>
</Match>
<Match when={mode() === "open"}>
<ClanDetails directory={clanDir() || ""} />
</Match>
<Match when={mode() === "create"}>
<ClanForm
actions={
<div class="card-actions justify-end">
<button
class="btn btn-primary"
// onClick={() => {
// pyApi.open_file.dispatch({
// file_request: { mode: "save" },
// });
// }}
>
Save
</button>
</div>
}
meta={{
name: "New Clan",
description: "nice description",
icon: "select icon",
}}
editable
/>
</Match>
</Switch>
}
/>
</div>
);
};