Clan create: migrate to inventory
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Minimal inventory"
|
||||
"name": "clan-core"
|
||||
},
|
||||
"machines": {
|
||||
"minimal-inventory-machine": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user