Merge pull request 'Clan create: migrate to inventory' (#1732) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": "Minimal inventory"
|
"name": "clan-core"
|
||||||
},
|
},
|
||||||
"machines": {
|
"machines": {
|
||||||
"minimal-inventory-machine": {
|
"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`.
|
This will start the application in debug mode and link it to the web server running at `http://localhost:3000`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# clan app (old)
|
# clan app (old)
|
||||||
|
|
||||||
Provides users with the simple functionality to manage their locally registered clans.
|
Provides users with the simple functionality to manage their locally registered clans.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def sanitize_string(s: str) -> str:
|
def sanitize_string(s: str) -> str:
|
||||||
return s.replace("\\", "\\\\").replace('"', '\\"')
|
return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||||
|
|
||||||
|
|
||||||
def dataclass_to_dict(obj: Any) -> Any:
|
def dataclass_to_dict(obj: Any) -> Any:
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# !/usr/bin/env python3
|
# !/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass, fields
|
from dataclasses import dataclass, fields
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
from clan_cli.arg_actions import AppendOptionAction
|
from clan_cli.arg_actions import AppendOptionAction
|
||||||
|
from clan_cli.inventory import Inventory, InventoryMeta
|
||||||
|
|
||||||
from ..cmd import CmdOut, run
|
from ..cmd import CmdOut, run
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
@@ -24,19 +24,12 @@ class CreateClanResponse:
|
|||||||
flake_update: CmdOut
|
flake_update: CmdOut
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ClanMetaInfo:
|
|
||||||
name: str
|
|
||||||
description: str | None
|
|
||||||
icon: str | None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CreateOptions:
|
class CreateOptions:
|
||||||
directory: Path | str
|
directory: Path | str
|
||||||
# Metadata for the clan
|
# Metadata for the clan
|
||||||
# Metadata can be shown with `clan show`
|
# 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
|
# URL to the template to use. Defaults to the "minimal" template
|
||||||
template_url: str = minimal_template_url
|
template_url: str = minimal_template_url
|
||||||
|
|
||||||
@@ -70,13 +63,7 @@ def create_clan(options: CreateOptions) -> CreateClanResponse:
|
|||||||
)
|
)
|
||||||
out = run(command, cwd=directory)
|
out = run(command, cwd=directory)
|
||||||
|
|
||||||
# Write meta.json file if meta is provided
|
## Begin: setup git
|
||||||
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)
|
|
||||||
|
|
||||||
command = nix_shell(["nixpkgs#git"], ["git", "init"])
|
command = nix_shell(["nixpkgs#git"], ["git", "init"])
|
||||||
out = run(command, cwd=directory)
|
out = run(command, cwd=directory)
|
||||||
cmd_responses["git init"] = out
|
cmd_responses["git init"] = out
|
||||||
@@ -94,6 +81,14 @@ def create_clan(options: CreateOptions) -> CreateClanResponse:
|
|||||||
)
|
)
|
||||||
out = run(command, cwd=directory)
|
out = run(command, cwd=directory)
|
||||||
cmd_responses["git config"] = out
|
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"]
|
command = ["nix", "flake", "update"]
|
||||||
out = run(command, cwd=directory)
|
out = run(command, cwd=directory)
|
||||||
@@ -118,7 +113,7 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--meta",
|
"--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,
|
nargs=2,
|
||||||
metavar=("name", "value"),
|
metavar=("name", "value"),
|
||||||
action=AppendOptionAction,
|
action=AppendOptionAction,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from pathlib import Path
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
from clan_cli.clan.create import ClanMetaInfo
|
|
||||||
from clan_cli.errors import ClanCmdError, ClanError
|
from clan_cli.errors import ClanCmdError, ClanError
|
||||||
|
from clan_cli.inventory import InventoryMeta
|
||||||
|
|
||||||
from ..cmd import run_no_stdout
|
from ..cmd import run_no_stdout
|
||||||
from ..nix import nix_eval
|
from ..nix import nix_eval
|
||||||
@@ -15,10 +15,10 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def show_clan_meta(uri: str | Path) -> ClanMetaInfo:
|
def show_clan_meta(uri: str | Path) -> InventoryMeta:
|
||||||
cmd = nix_eval(
|
cmd = nix_eval(
|
||||||
[
|
[
|
||||||
f"{uri}#clanInternals.meta",
|
f"{uri}#clanInternals.inventory.meta",
|
||||||
"--json",
|
"--json",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -27,11 +27,11 @@ def show_clan_meta(uri: str | Path) -> ClanMetaInfo:
|
|||||||
try:
|
try:
|
||||||
proc = run_no_stdout(cmd)
|
proc = run_no_stdout(cmd)
|
||||||
res = proc.stdout.strip()
|
res = proc.stdout.strip()
|
||||||
except ClanCmdError:
|
except ClanCmdError as e:
|
||||||
raise ClanError(
|
raise ClanError(
|
||||||
"Clan might not have meta attributes",
|
"Evaluation failed on meta attribute",
|
||||||
location=f"show_clan {uri}",
|
location=f"show_clan {uri}",
|
||||||
description="Evaluation failed on clanInternals.meta attribute",
|
description=str(e.cmd),
|
||||||
)
|
)
|
||||||
|
|
||||||
clan_meta = json.loads(res)
|
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.",
|
description="Icon path must be a URL or a relative path.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return ClanMetaInfo(
|
return InventoryMeta(
|
||||||
name=clan_meta.get("name"),
|
name=clan_meta.get("name"),
|
||||||
description=clan_meta.get("description", None),
|
description=clan_meta.get("description", None),
|
||||||
icon=icon_path,
|
icon=icon_path,
|
||||||
@@ -73,8 +73,8 @@ def show_command(args: argparse.Namespace) -> None:
|
|||||||
meta = show_clan_meta(flake_path)
|
meta = show_clan_meta(flake_path)
|
||||||
|
|
||||||
print(f"Name: {meta.name}")
|
print(f"Name: {meta.name}")
|
||||||
print(f"Description: {meta.description or ''}")
|
print(f"Description: {meta.description or '-'}")
|
||||||
print(f"Icon: {meta.icon or ''}")
|
print(f"Icon: {meta.icon or '-'}")
|
||||||
|
|
||||||
|
|
||||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
|||||||
@@ -1,35 +1,20 @@
|
|||||||
import json
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
from clan_cli.clan.create import ClanMetaInfo
|
from clan_cli.inventory import Inventory, InventoryMeta
|
||||||
from clan_cli.errors import ClanError
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UpdateOptions:
|
class UpdateOptions:
|
||||||
directory: str
|
directory: str
|
||||||
meta: ClanMetaInfo | None = None
|
meta: InventoryMeta
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def update_clan_meta(options: UpdateOptions) -> ClanMetaInfo:
|
def update_clan_meta(options: UpdateOptions) -> InventoryMeta:
|
||||||
meta_file = Path(options.directory) / Path("clan/meta.json")
|
inventory = Inventory.load_file(options.directory)
|
||||||
if not meta_file.exists():
|
inventory.meta = options.meta
|
||||||
raise ClanError(
|
|
||||||
"File not found",
|
|
||||||
description=f"Could not find {meta_file} to update.",
|
|
||||||
location="update_clan_meta",
|
|
||||||
)
|
|
||||||
|
|
||||||
meta_content: dict[str, str] = {}
|
inventory.persist(options.directory, "Update clan meta")
|
||||||
with open(meta_file) as f:
|
|
||||||
meta_content = json.load(f)
|
|
||||||
|
|
||||||
meta_content = {**meta_content, **options.meta.__dict__}
|
return inventory.meta
|
||||||
|
|
||||||
with open(meta_file) as f:
|
|
||||||
json.dump(meta_content, f)
|
|
||||||
|
|
||||||
return ClanMetaInfo(**meta_content)
|
|
||||||
|
|||||||
@@ -99,14 +99,23 @@ class Service:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InventoryMeta:
|
||||||
|
name: str
|
||||||
|
description: str | None = None
|
||||||
|
icon: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Inventory:
|
class Inventory:
|
||||||
|
meta: InventoryMeta
|
||||||
machines: dict[str, Machine]
|
machines: dict[str, Machine]
|
||||||
services: dict[str, dict[str, Service]]
|
services: dict[str, dict[str, Service]]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_dict(d: dict[str, Any]) -> "Inventory":
|
def from_dict(d: dict[str, Any]) -> "Inventory":
|
||||||
return Inventory(
|
return Inventory(
|
||||||
|
meta=InventoryMeta(**d.get("meta", {})),
|
||||||
machines={
|
machines={
|
||||||
name: Machine.from_dict(machine)
|
name: Machine.from_dict(machine)
|
||||||
for name, machine in d.get("machines", {}).items()
|
for name, machine in d.get("machines", {}).items()
|
||||||
@@ -126,7 +135,9 @@ class Inventory:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_file(flake_dir: str | Path) -> "Inventory":
|
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)
|
inventory_file = Inventory.get_path(flake_dir)
|
||||||
if inventory_file.exists():
|
if inventory_file.exists():
|
||||||
with open(inventory_file) as f:
|
with open(inventory_file) as f:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from age_keys import SopsSetup
|
from age_keys import SopsSetup
|
||||||
@@ -14,7 +16,7 @@ def def_value() -> defaultdict:
|
|||||||
|
|
||||||
|
|
||||||
# allows defining nested dictionary in a single line
|
# 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
|
@pytest.mark.impure
|
||||||
|
|||||||
52
pkgs/webview-ui/app/package-lock.json
generated
52
pkgs/webview-ui/app/package-lock.json
generated
@@ -10,8 +10,10 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modular-forms/solid": "^0.21.0",
|
"@modular-forms/solid": "^0.21.0",
|
||||||
|
"@solid-primitives/storage": "^3.7.1",
|
||||||
"@tanstack/solid-query": "^5.44.0",
|
"@tanstack/solid-query": "^5.44.0",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
"solid-js": "^1.8.11",
|
"solid-js": "^1.8.11",
|
||||||
"solid-toast": "^0.5.0"
|
"solid-toast": "^0.5.0"
|
||||||
},
|
},
|
||||||
@@ -1498,6 +1500,26 @@
|
|||||||
"solid-js": "^1.6.12"
|
"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": {
|
"node_modules/@solid-primitives/styles": {
|
||||||
"version": "0.0.111",
|
"version": "0.0.111",
|
||||||
"resolved": "https://registry.npmjs.org/@solid-primitives/styles/-/styles-0.0.111.tgz",
|
"resolved": "https://registry.npmjs.org/@solid-primitives/styles/-/styles-0.0.111.tgz",
|
||||||
@@ -1515,7 +1537,6 @@
|
|||||||
"version": "6.2.3",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz",
|
||||||
"integrity": "sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==",
|
"integrity": "sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==",
|
||||||
"dev": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"solid-js": "^1.6.12"
|
"solid-js": "^1.6.12"
|
||||||
}
|
}
|
||||||
@@ -4177,10 +4198,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4188,10 +4208,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^18 || >=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
@@ -4753,6 +4773,24 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|||||||
@@ -39,8 +39,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modular-forms/solid": "^0.21.0",
|
"@modular-forms/solid": "^0.21.0",
|
||||||
|
"@solid-primitives/storage": "^3.7.1",
|
||||||
"@tanstack/solid-query": "^5.44.0",
|
"@tanstack/solid-query": "^5.44.0",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
"solid-js": "^1.8.11",
|
"solid-js": "^1.8.11",
|
||||||
"solid-toast": "^0.5.0"
|
"solid-toast": "^0.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,35 @@
|
|||||||
import { createSignal, type Component } from "solid-js";
|
import { createSignal, type Component } from "solid-js";
|
||||||
import { MachineProvider } from "./Config";
|
|
||||||
import { Layout } from "./layout/layout";
|
import { Layout } from "./layout/layout";
|
||||||
import { Route, Router } from "./Routes";
|
import { Route, Router } from "./Routes";
|
||||||
import { Toaster } from "solid-toast";
|
import { Toaster } from "solid-toast";
|
||||||
|
import { effect } from "solid-js/web";
|
||||||
|
import { makePersisted } from "@solid-primitives/storage";
|
||||||
|
|
||||||
// Some global state
|
// Some global state
|
||||||
const [route, setRoute] = createSignal<Route>("machines");
|
const [route, setRoute] = createSignal<Route>("machines");
|
||||||
export { route, setRoute };
|
export { route, setRoute };
|
||||||
|
|
||||||
const [currClanURI, setCurrClanURI] = createSignal<string | null>(null);
|
const [activeURI, setActiveURI] = createSignal<string | null>(null);
|
||||||
export { currClanURI, setCurrClanURI };
|
export { activeURI, setActiveURI };
|
||||||
|
|
||||||
|
const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
|
||||||
|
name: "clanList",
|
||||||
|
storage: localStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { clanList, setClanList };
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
|
effect(() => {
|
||||||
|
if (clanList().length === 0) {
|
||||||
|
setRoute("welcome");
|
||||||
|
}
|
||||||
|
});
|
||||||
return [
|
return [
|
||||||
<Toaster position="top-right" />,
|
<Toaster position="top-right" />,
|
||||||
<MachineProvider>
|
<Layout>
|
||||||
<Layout>
|
<Router route={route} />
|
||||||
<Router route={route} />
|
</Layout>,
|
||||||
</Layout>
|
|
||||||
</MachineProvider>,
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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<OperationResponse<"list_machines">>();
|
|
||||||
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<typeof makeMachineContext>;
|
|
||||||
|
|
||||||
export const MachineContext = createContext<MachineContextType>([
|
|
||||||
{
|
|
||||||
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 (
|
|
||||||
<MachineContext.Provider value={makeMachineContext()}>
|
|
||||||
{props.children}
|
|
||||||
</MachineContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import { Accessor, For, Match, Switch } from "solid-js";
|
import { Accessor, For, Match, Switch } from "solid-js";
|
||||||
import { MachineListView } from "./routes/machines/view";
|
import { MachineListView } from "./routes/machines/view";
|
||||||
import { colors } from "./routes/colors/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 { HostList } from "./routes/hosts/view";
|
||||||
import { BlockDevicesView } from "./routes/blockdevices/view";
|
import { BlockDevicesView } from "./routes/blockdevices/view";
|
||||||
import { Flash } from "./routes/flash/view";
|
import { Flash } from "./routes/flash/view";
|
||||||
|
import { Settings } from "./routes/settings";
|
||||||
|
import { Welcome } from "./routes/welcome";
|
||||||
|
|
||||||
export type Route = keyof typeof routes;
|
export type Route = keyof typeof routes;
|
||||||
|
|
||||||
export const routes = {
|
export const routes = {
|
||||||
clan: {
|
createClan: {
|
||||||
child: clan,
|
child: CreateClan,
|
||||||
label: "Clan",
|
label: "Create Clan",
|
||||||
icon: "groups",
|
icon: "groups",
|
||||||
},
|
},
|
||||||
machines: {
|
machines: {
|
||||||
@@ -39,6 +41,16 @@ export const routes = {
|
|||||||
label: "Colors",
|
label: "Colors",
|
||||||
icon: "color_lens",
|
icon: "color_lens",
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
child: Settings,
|
||||||
|
label: "Settings",
|
||||||
|
icon: "settings",
|
||||||
|
},
|
||||||
|
welcome: {
|
||||||
|
child: Welcome,
|
||||||
|
label: "welcome",
|
||||||
|
icon: "settings",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RouterProps {
|
interface RouterProps {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FromSchema } from "json-schema-to-ts";
|
import { FromSchema } from "json-schema-to-ts";
|
||||||
import { schema } from "@/api";
|
import { schema } from "@/api";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
export type API = FromSchema<typeof schema>;
|
export type API = FromSchema<typeof schema>;
|
||||||
|
|
||||||
@@ -41,47 +42,44 @@ const operations = schema.properties;
|
|||||||
const operationNames = Object.keys(operations) as OperationNames[];
|
const operationNames = Object.keys(operations) as OperationNames[];
|
||||||
|
|
||||||
type ObserverRegistry = {
|
type ObserverRegistry = {
|
||||||
[K in OperationNames]: ((response: OperationResponse<K>) => void)[];
|
[K in OperationNames]: Record<
|
||||||
|
string,
|
||||||
|
(response: OperationResponse<K>) => void
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
const obs: ObserverRegistry = operationNames.reduce(
|
const registry: ObserverRegistry = operationNames.reduce(
|
||||||
(acc, opName) => ({
|
(acc, opName) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[opName]: [],
|
[opName]: {},
|
||||||
}),
|
}),
|
||||||
{} as ObserverRegistry
|
{} as ObserverRegistry
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ReceiveOptions {
|
|
||||||
/**
|
|
||||||
* Calls only the registered function that has the same key as used with dispatch
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fnKey: string;
|
|
||||||
}
|
|
||||||
function createFunctions<K extends OperationNames>(
|
function createFunctions<K extends OperationNames>(
|
||||||
operationName: K
|
operationName: K
|
||||||
): {
|
): {
|
||||||
dispatch: (args: OperationArgs<K>) => void;
|
dispatch: (args: OperationArgs<K>) => void;
|
||||||
receive: (fn: (response: OperationResponse<K>) => void) => void;
|
receive: (fn: (response: OperationResponse<K>) => void, id: string) => void;
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
dispatch: (args: OperationArgs<K>) => {
|
dispatch: (args: OperationArgs<K>) => {
|
||||||
// console.log(
|
|
||||||
// `Operation: ${String(operationName)}, Arguments: ${JSON.stringify(args)}`
|
|
||||||
// );
|
|
||||||
// Send the data to the gtk app
|
// Send the data to the gtk app
|
||||||
window.webkit.messageHandlers.gtk.postMessage({
|
window.webkit.messageHandlers.gtk.postMessage({
|
||||||
method: operationName,
|
method: operationName,
|
||||||
data: args,
|
data: args,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
receive: (
|
receive: (fn: (response: OperationResponse<K>) => void, id: string) => {
|
||||||
fn: (response: OperationResponse<K>) => void
|
// @ts-expect-error: This should work although typescript doesn't let us write
|
||||||
// options?: ReceiveOptions
|
registry[operationName][id] = fn;
|
||||||
) => {
|
|
||||||
obs[operationName].push(fn);
|
|
||||||
window.clan[operationName] = (s: string) => {
|
window.clan[operationName] = (s: string) => {
|
||||||
obs[operationName].forEach((f) => deserialize(f)(s));
|
const f = (response: OperationResponse<K>) => {
|
||||||
|
if (response.op_key === id) {
|
||||||
|
registry[operationName][id](response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deserialize(f)(s);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -90,16 +88,51 @@ function createFunctions<K extends OperationNames>(
|
|||||||
type PyApi = {
|
type PyApi = {
|
||||||
[K in OperationNames]: {
|
[K in OperationNames]: {
|
||||||
dispatch: (args: OperationArgs<K>) => void;
|
dispatch: (args: OperationArgs<K>) => void;
|
||||||
receive: (fn: (response: OperationResponse<K>) => void) => void;
|
receive: (fn: (response: OperationResponse<K>) => void, id: string) => void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const callApi = <K extends OperationNames>(
|
||||||
|
method: K,
|
||||||
|
args: OperationArgs<K>
|
||||||
|
) => {
|
||||||
|
return new Promise<OperationResponse<K>>((resolve, reject) => {
|
||||||
|
const id = nanoid();
|
||||||
|
pyApi[method].receive((response) => {
|
||||||
|
if (response.status === "error") {
|
||||||
|
reject(response);
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
}, id);
|
||||||
|
|
||||||
|
pyApi[method].dispatch({ ...args, op_key: id });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const deserialize =
|
const deserialize =
|
||||||
<T>(fn: (response: T) => void) =>
|
<T>(fn: (response: T) => void) =>
|
||||||
(str: string) => {
|
(str: string) => {
|
||||||
try {
|
try {
|
||||||
fn(JSON.parse(str) as T);
|
fn(JSON.parse(str) as T);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log("Error parsing JSON: ", e);
|
||||||
|
console.log({ download: () => download("error.json", str) });
|
||||||
console.error(str);
|
console.error(str);
|
||||||
alert(`Error parsing JSON: ${e}`);
|
alert(`Error parsing JSON: ${e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Match, Show, Switch, createSignal } from "solid-js";
|
import { Match, Show, Switch, createSignal } from "solid-js";
|
||||||
import { ErrorData, SuccessData, pyApi } from "../api";
|
import { ErrorData, SuccessData, pyApi } from "../api";
|
||||||
import { currClanURI } from "../App";
|
|
||||||
|
|
||||||
type MachineDetails = SuccessData<"list_machines">["data"][string];
|
type MachineDetails = SuccessData<"list_machines">["data"][string];
|
||||||
|
|
||||||
@@ -23,51 +22,51 @@ const [deploymentInfo, setDeploymentInfo] = createSignal<DeploymentInfo>({});
|
|||||||
|
|
||||||
const [errors, setErrors] = createSignal<MachineErrors>({});
|
const [errors, setErrors] = createSignal<MachineErrors>({});
|
||||||
|
|
||||||
pyApi.show_machine_hardware_info.receive((r) => {
|
// pyApi.show_machine_hardware_info.receive((r) => {
|
||||||
const { op_key } = r;
|
// const { op_key } = r;
|
||||||
if (r.status === "error") {
|
// if (r.status === "error") {
|
||||||
console.error(r.errors);
|
// console.error(r.errors);
|
||||||
if (op_key) {
|
// if (op_key) {
|
||||||
setHwInfo((d) => ({ ...d, [op_key]: { system: null } }));
|
// setHwInfo((d) => ({ ...d, [op_key]: { system: null } }));
|
||||||
}
|
// }
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
if (op_key) {
|
// if (op_key) {
|
||||||
setHwInfo((d) => ({ ...d, [op_key]: r.data }));
|
// setHwInfo((d) => ({ ...d, [op_key]: r.data }));
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
pyApi.show_machine_deployment_target.receive((r) => {
|
// pyApi.show_machine_deployment_target.receive((r) => {
|
||||||
const { op_key } = r;
|
// const { op_key } = r;
|
||||||
if (r.status === "error") {
|
// if (r.status === "error") {
|
||||||
console.error(r.errors);
|
// console.error(r.errors);
|
||||||
if (op_key) {
|
// if (op_key) {
|
||||||
setDeploymentInfo((d) => ({ ...d, [op_key]: null }));
|
// setDeploymentInfo((d) => ({ ...d, [op_key]: null }));
|
||||||
}
|
// }
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
if (op_key) {
|
// if (op_key) {
|
||||||
setDeploymentInfo((d) => ({ ...d, [op_key]: r.data }));
|
// setDeploymentInfo((d) => ({ ...d, [op_key]: r.data }));
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
export const MachineListItem = (props: MachineListItemProps) => {
|
export const MachineListItem = (props: MachineListItemProps) => {
|
||||||
const { name, info } = props;
|
const { name, info } = props;
|
||||||
|
|
||||||
const clan_dir = currClanURI();
|
// const clan_dir = currClanURI();
|
||||||
if (clan_dir) {
|
// if (clan_dir) {
|
||||||
pyApi.show_machine_hardware_info.dispatch({
|
// pyApi.show_machine_hardware_info.dispatch({
|
||||||
op_key: name,
|
// op_key: name,
|
||||||
clan_dir,
|
// clan_dir,
|
||||||
machine_name: name,
|
// machine_name: name,
|
||||||
});
|
// });
|
||||||
|
|
||||||
pyApi.show_machine_deployment_target.dispatch({
|
// pyApi.show_machine_deployment_target.dispatch({
|
||||||
op_key: name,
|
// op_key: name,
|
||||||
clan_dir,
|
// clan_dir,
|
||||||
machine_name: name,
|
// machine_name: name,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -3,3 +3,9 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { currClanURI } from "../App";
|
import { activeURI, setRoute } from "../App";
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
return (
|
return (
|
||||||
@@ -14,12 +14,12 @@ export const Header = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<a class="text-xl">{currClanURI() || "Clan"}</a>
|
<a class="text-xl">{activeURI()}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
<span class="tooltip tooltip-bottom" data-tip="Account">
|
<span class="tooltip tooltip-bottom" data-tip="Settings">
|
||||||
<button class="btn btn-square btn-ghost">
|
<button class="link" onClick={() => setRoute("settings")}>
|
||||||
<span class="material-icons">account_circle</span>
|
<span class="material-icons">settings</span>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { Component, JSXElement } from "solid-js";
|
import { Component, JSXElement, Show } from "solid-js";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
import { Sidebar } from "../Sidebar";
|
import { Sidebar } from "../Sidebar";
|
||||||
import { route, setRoute } from "../App";
|
import { route, setRoute } from "../App";
|
||||||
|
import { effect } from "solid-js/web";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: JSXElement;
|
children: JSXElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Layout: Component<LayoutProps> = (props) => {
|
export const Layout: Component<LayoutProps> = (props) => {
|
||||||
|
effect(() => {
|
||||||
|
console.log(route());
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="drawer bg-base-100 lg:drawer-open">
|
<div class="drawer bg-base-100 lg:drawer-open">
|
||||||
@@ -17,11 +21,16 @@ export const Layout: Component<LayoutProps> = (props) => {
|
|||||||
class="drawer-toggle hidden"
|
class="drawer-toggle hidden"
|
||||||
/>
|
/>
|
||||||
<div class="drawer-content">
|
<div class="drawer-content">
|
||||||
<Header />
|
<Show when={route() !== "welcome"}>
|
||||||
|
<Header />
|
||||||
|
</Show>
|
||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<div class="drawer-side z-40">
|
<div
|
||||||
|
class="drawer-side z-40"
|
||||||
|
classList={{ "!hidden": route() === "welcome" }}
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
for="toplevel-drawer"
|
for="toplevel-drawer"
|
||||||
aria-label="close sidebar"
|
aria-label="close sidebar"
|
||||||
|
|||||||
@@ -8,24 +8,24 @@ type DevicesModel = Extract<
|
|||||||
>["data"]["blockdevices"];
|
>["data"]["blockdevices"];
|
||||||
|
|
||||||
export const BlockDevicesView: Component = () => {
|
export const BlockDevicesView: Component = () => {
|
||||||
const [devices, setServices] = createSignal<DevicesModel>();
|
const [devices, setDevices] = createSignal<DevicesModel>();
|
||||||
|
|
||||||
pyApi.show_block_devices.receive((r) => {
|
// pyApi.show_block_devices.receive((r) => {
|
||||||
const { status } = r;
|
// const { status } = r;
|
||||||
if (status === "error") return console.error(r.errors);
|
// if (status === "error") return console.error(r.errors);
|
||||||
setServices(r.data.blockdevices);
|
// setServices(r.data.blockdevices);
|
||||||
});
|
// });
|
||||||
|
|
||||||
createEffect(() => {
|
// createEffect(() => {
|
||||||
if (route() === "blockdevices") pyApi.show_block_devices.dispatch({});
|
// if (route() === "blockdevices") pyApi.show_block_devices.dispatch({});
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="tooltip tooltip-bottom" data-tip="Refresh">
|
<div class="tooltip tooltip-bottom" data-tip="Refresh">
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost"
|
||||||
onClick={() => pyApi.show_block_devices.dispatch({})}
|
// onClick={() => pyApi.show_block_devices.dispatch({})}
|
||||||
>
|
>
|
||||||
<span class="material-icons ">refresh</span>
|
<span class="material-icons ">refresh</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,45 +11,91 @@ import {
|
|||||||
import {
|
import {
|
||||||
SubmitHandler,
|
SubmitHandler,
|
||||||
createForm,
|
createForm,
|
||||||
email,
|
|
||||||
required,
|
required,
|
||||||
|
custom,
|
||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
|
import toast from "solid-toast";
|
||||||
|
import { setActiveURI, setRoute } from "@/src/App";
|
||||||
|
|
||||||
interface ClanDetailsProps {
|
interface ClanDetailsProps {
|
||||||
directory: string;
|
directory: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClanFormProps {
|
type CreateForm = Meta & {
|
||||||
directory?: string;
|
template_url: string;
|
||||||
meta: ClanMeta;
|
};
|
||||||
actions: JSX.Element;
|
|
||||||
editable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ClanForm = (props: ClanFormProps) => {
|
export const ClanForm = () => {
|
||||||
const { meta, actions, editable = true, directory } = props;
|
const [formStore, { Form, Field }] = createForm<CreateForm>({
|
||||||
const [formStore, { Form, Field }] = createForm<ClanMeta>({
|
initialValues: {
|
||||||
initialValues: meta,
|
template_url: "git+https://git.clan.lol/clan/clan-core#templates.minimal",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit: SubmitHandler<ClanMeta> = (values, event) => {
|
const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => {
|
||||||
pyApi.open_file.dispatch({ file_request: { mode: "save" } });
|
const { template_url, ...meta } = values;
|
||||||
pyApi.open_file.receive((r) => {
|
pyApi.open_file.dispatch({
|
||||||
if (r.status === "success") {
|
file_request: {
|
||||||
if (r.data) {
|
mode: "save",
|
||||||
pyApi.create_clan.dispatch({
|
},
|
||||||
options: { directory: r.data, meta: values },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
op_key: "create_clan",
|
||||||
}
|
|
||||||
});
|
});
|
||||||
console.log("submit", values);
|
|
||||||
|
// await new Promise<void>((done) => {
|
||||||
|
// pyApi.open_file.receive((r) => {
|
||||||
|
// if (r.op_key !== "create_clan") {
|
||||||
|
// done();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (r.status !== "success") {
|
||||||
|
// toast.error("Cannot select clan directory");
|
||||||
|
// done();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const target_dir = r?.data;
|
||||||
|
// if (!target_dir) {
|
||||||
|
// toast.error("Cannot select clan directory");
|
||||||
|
// done();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// console.log({ formStore });
|
||||||
|
|
||||||
|
// toast.promise(
|
||||||
|
// new Promise<void>((resolve, reject) => {
|
||||||
|
// pyApi.create_clan.receive((r) => {
|
||||||
|
// done();
|
||||||
|
// if (r.status === "error") {
|
||||||
|
// reject();
|
||||||
|
// console.error(r.errors);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// resolve();
|
||||||
|
|
||||||
|
// // Navigate to the new clan
|
||||||
|
// setCurrClanURI(target_dir);
|
||||||
|
// setRoute("machines");
|
||||||
|
// });
|
||||||
|
|
||||||
|
// pyApi.create_clan.dispatch({
|
||||||
|
// options: { directory: target_dir, meta, template_url },
|
||||||
|
// op_key: "create_clan",
|
||||||
|
// });
|
||||||
|
// }),
|
||||||
|
// {
|
||||||
|
// loading: "Creating clan...",
|
||||||
|
// success: "Clan Successfully Created",
|
||||||
|
// error: "Failed to create clan",
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="card card-compact w-96 bg-base-100 shadow-xl">
|
<div class="card card-normal">
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit} shouldActive>
|
||||||
<Field name="icon">
|
<Field name="icon">
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<>
|
||||||
@@ -71,13 +117,29 @@ export const ClanForm = (props: ClanFormProps) => {
|
|||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</figure>
|
</figure>
|
||||||
<label class="form-control w-full max-w-xs">
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<div class="card-body">
|
||||||
|
<Field
|
||||||
|
name="name"
|
||||||
|
validate={[required("Please enter a unique name for the clan.")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<label class="form-control w-full">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Select icon</span>
|
<span class="label-text block after:ml-0.5 after:text-primary after:content-['*']">
|
||||||
|
Name
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
{...props}
|
||||||
class="file-input file-input-bordered w-full max-w-xs"
|
required
|
||||||
|
placeholder="Clan Name"
|
||||||
|
class="input input-bordered"
|
||||||
|
classList={{ "input-error": !!field.error }}
|
||||||
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
{field.error && (
|
{field.error && (
|
||||||
@@ -85,154 +147,76 @@ export const ClanForm = (props: ClanFormProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</>
|
)}
|
||||||
)}
|
</Field>
|
||||||
</Field>
|
<Field name="description">
|
||||||
<div class="card-body">
|
{(field, props) => (
|
||||||
<div class="card-body">
|
<label class="form-control w-full">
|
||||||
<Field
|
<div class="label">
|
||||||
name="name"
|
<span class="label-text">Description</span>
|
||||||
validate={[required("Please enter a unique name for the clan.")]}
|
</div>
|
||||||
>
|
|
||||||
{(field, props) => (
|
|
||||||
<label class="form-control w-full max-w-xs">
|
|
||||||
<div class="label">
|
|
||||||
<span class="label-text block after:ml-0.5 after:text-primary after:content-['*']">
|
|
||||||
Name
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
{...props}
|
{...props}
|
||||||
type="email"
|
required
|
||||||
required
|
type="text"
|
||||||
placeholder="your.mail@example.com"
|
placeholder="Some words about your clan"
|
||||||
class="input w-full max-w-xs"
|
class="input input-bordered"
|
||||||
classList={{ "input-error": !!field.error }}
|
classList={{ "input-error": !!field.error }}
|
||||||
value={field.value}
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
{field.error && (
|
{field.error && (
|
||||||
<span class="label-text-alt">{field.error}</span>
|
<span class="label-text-alt">{field.error}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="description">
|
<Field name="template_url" validate={[required("This is required")]}>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<label class="form-control w-full max-w-xs">
|
<div class="collapse collapse-arrow" tabindex="0">
|
||||||
<div class="label">
|
<input type="checkbox" />
|
||||||
<span class="label-text">Description</span>
|
<div class="collapse-title link font-medium ">Advanced</div>
|
||||||
</div>
|
<div class="collapse-content">
|
||||||
|
<label class="form-control w-full">
|
||||||
<input
|
<div class="label ">
|
||||||
{...props}
|
<span class="label-text after:ml-0.5 after:text-primary after:content-['*']">
|
||||||
required
|
Template to use
|
||||||
type="text"
|
</span>
|
||||||
placeholder="Some words about your clan"
|
</div>
|
||||||
class="input w-full max-w-xs"
|
<input
|
||||||
classList={{ "input-error": !!field.error }}
|
{...props}
|
||||||
value={field.value || ""}
|
required
|
||||||
/>
|
type="text"
|
||||||
<div class="label">
|
placeholder="Template to use"
|
||||||
{field.error && (
|
class="input input-bordered"
|
||||||
<span class="label-text-alt">{field.error}</span>
|
classList={{ "input-error": !!field.error }}
|
||||||
)}
|
value={field.value}
|
||||||
</div>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)}
|
</div>
|
||||||
</Field>
|
</div>
|
||||||
{actions}
|
)}
|
||||||
</div>
|
</Field>
|
||||||
|
{
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={formStore.submitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const EditMetaFields = (props: MetaFieldsProps) => {
|
type Meta = Extract<
|
||||||
// const { meta, editable, actions, directory } = props;
|
|
||||||
|
|
||||||
// const [editing, setEditing] = createSignal<
|
|
||||||
// keyof MetaFieldsProps["meta"] | null
|
|
||||||
// >(null);
|
|
||||||
// return (
|
|
||||||
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
type ClanMeta = Extract<
|
|
||||||
OperationResponse<"show_clan_meta">,
|
OperationResponse<"show_clan_meta">,
|
||||||
{ status: "success" }
|
{ status: "success" }
|
||||||
>["data"];
|
>["data"];
|
||||||
|
|
||||||
export const ClanDetails = (props: ClanDetailsProps) => {
|
|
||||||
const { directory } = props;
|
|
||||||
const [loading, setLoading] = createSignal(false);
|
|
||||||
const [errors, setErrors] = createSignal<
|
|
||||||
| Extract<
|
|
||||||
OperationResponse<"show_clan_meta">,
|
|
||||||
{ status: "error" }
|
|
||||||
>["errors"]
|
|
||||||
| null
|
|
||||||
>(null);
|
|
||||||
const [data, setData] = createSignal<ClanMeta>();
|
|
||||||
|
|
||||||
const loadMeta = () => {
|
|
||||||
pyApi.show_clan_meta.dispatch({ uri: directory });
|
|
||||||
setLoading(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
loadMeta();
|
|
||||||
pyApi.show_clan_meta.receive((response) => {
|
|
||||||
setLoading(false);
|
|
||||||
if (response.status === "error") {
|
|
||||||
setErrors(response.errors);
|
|
||||||
return console.error(response.errors);
|
|
||||||
}
|
|
||||||
setData(response.data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<Switch fallback={"loading"}>
|
|
||||||
<Match when={loading()}>
|
|
||||||
<div>Loading</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={data()}>
|
|
||||||
{(data) => {
|
|
||||||
const meta = data();
|
|
||||||
return (
|
|
||||||
<ClanForm
|
|
||||||
directory={directory}
|
|
||||||
meta={meta}
|
|
||||||
actions={
|
|
||||||
<div class="card-actions justify-between">
|
|
||||||
<button class="btn btn-link" onClick={() => loadMeta()}>
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary">Open</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Match>
|
|
||||||
<Match when={errors()}>
|
|
||||||
<button class="btn btn-secondary" onClick={() => loadMeta()}>
|
|
||||||
Retry
|
|
||||||
</button>
|
|
||||||
<For each={errors()}>
|
|
||||||
{(item) => (
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<span class="bg-red-400 text-white">{item.message}</span>
|
|
||||||
<span class="bg-red-400 text-white">{item.description}</span>
|
|
||||||
<span class="bg-red-400 text-white">{item.location}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,82 +1,9 @@
|
|||||||
import { pyApi } from "@/src/api";
|
import { ClanForm } from "./clanDetails";
|
||||||
import { Match, Switch, createEffect, createSignal } from "solid-js";
|
|
||||||
import toast from "solid-toast";
|
|
||||||
import { ClanDetails, ClanForm } from "./clanDetails";
|
|
||||||
|
|
||||||
export const clan = () => {
|
export const CreateClan = () => {
|
||||||
const [mode, setMode] = createSignal<"init" | "open" | "create">("init");
|
|
||||||
const [clanDir, setClanDir] = createSignal<string | null>(null);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
console.log(mode());
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Switch fallback={"invalid"}>
|
<ClanForm />
|
||||||
<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>
|
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,16 +24,17 @@ type BlockDevices = Extract<
|
|||||||
OperationResponse<"show_block_devices">,
|
OperationResponse<"show_block_devices">,
|
||||||
{ status: "success" }
|
{ status: "success" }
|
||||||
>["data"]["blockdevices"];
|
>["data"]["blockdevices"];
|
||||||
|
|
||||||
export const Flash = () => {
|
export const Flash = () => {
|
||||||
const [formStore, { Form, Field }] = createForm<FlashFormValues>({});
|
const [formStore, { Form, Field }] = createForm<FlashFormValues>({});
|
||||||
|
|
||||||
const [devices, setDevices] = createSignal<BlockDevices>([]);
|
const [devices, setDevices] = createSignal<BlockDevices>([]);
|
||||||
pyApi.show_block_devices.receive((r) => {
|
// pyApi.show_block_devices.receive((r) => {
|
||||||
console.log("block devices", r);
|
// console.log("block devices", r);
|
||||||
if (r.status === "success") {
|
// if (r.status === "success") {
|
||||||
setDevices(r.data.blockdevices);
|
// setDevices(r.data.blockdevices);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
const handleSubmit: SubmitHandler<FlashFormValues> = (values, event) => {
|
const handleSubmit: SubmitHandler<FlashFormValues> = (values, event) => {
|
||||||
// pyApi.open_file.dispatch({ file_request: { mode: "save" } });
|
// pyApi.open_file.dispatch({ file_request: { mode: "save" } });
|
||||||
@@ -50,11 +51,11 @@ export const Flash = () => {
|
|||||||
console.log("submit", values);
|
console.log("submit", values);
|
||||||
};
|
};
|
||||||
|
|
||||||
effect(() => {
|
// effect(() => {
|
||||||
if (route() === "flash") {
|
// if (route() === "flash") {
|
||||||
pyApi.show_block_devices.dispatch({});
|
// pyApi.show_block_devices.dispatch({});
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
return (
|
return (
|
||||||
<div class="">
|
<div class="">
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ type ServiceModel = Extract<
|
|||||||
export const HostList: Component = () => {
|
export const HostList: Component = () => {
|
||||||
const [services, setServices] = createSignal<ServiceModel>();
|
const [services, setServices] = createSignal<ServiceModel>();
|
||||||
|
|
||||||
pyApi.show_mdns.receive((r) => {
|
// pyApi.show_mdns.receive((r) => {
|
||||||
const { status } = r;
|
// const { status } = r;
|
||||||
if (status === "error") return console.error(r.errors);
|
// if (status === "error") return console.error(r.errors);
|
||||||
setServices(r.data.services);
|
// setServices(r.data.services);
|
||||||
});
|
// });
|
||||||
|
|
||||||
createEffect(() => {
|
// createEffect(() => {
|
||||||
if (route() === "hosts") pyApi.show_mdns.dispatch({});
|
// if (route() === "hosts") pyApi.show_mdns.dispatch({});
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -7,117 +7,86 @@ import {
|
|||||||
createSignal,
|
createSignal,
|
||||||
type Component,
|
type Component,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { useMachineContext } from "../../Config";
|
import { activeURI, route, setActiveURI } from "@/src/App";
|
||||||
import { route, setCurrClanURI } from "@/src/App";
|
import { OperationResponse, callApi, pyApi } from "@/src/api";
|
||||||
import { OperationResponse, pyApi } from "@/src/api";
|
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { MachineListItem } from "@/src/components/MachineListItem";
|
import { MachineListItem } from "@/src/components/MachineListItem";
|
||||||
|
|
||||||
type FilesModel = Extract<
|
// type FilesModel = Extract<
|
||||||
OperationResponse<"get_directory">,
|
// OperationResponse<"get_directory">,
|
||||||
{ status: "success" }
|
// { status: "success" }
|
||||||
>["data"]["files"];
|
// >["data"]["files"];
|
||||||
|
|
||||||
type ServiceModel = Extract<
|
// type ServiceModel = Extract<
|
||||||
OperationResponse<"show_mdns">,
|
// OperationResponse<"show_mdns">,
|
||||||
{ status: "success" }
|
// { status: "success" }
|
||||||
>["data"]["services"];
|
// >["data"]["services"];
|
||||||
|
|
||||||
type MachinesModel = Extract<
|
type MachinesModel = Extract<
|
||||||
OperationResponse<"list_machines">,
|
OperationResponse<"list_machines">,
|
||||||
{ status: "success" }
|
{ status: "success" }
|
||||||
>["data"];
|
>["data"];
|
||||||
|
|
||||||
pyApi.open_file.receive((r) => {
|
// pyApi.open_file.receive((r) => {
|
||||||
if (r.op_key === "open_clan") {
|
// if (r.op_key === "open_clan") {
|
||||||
console.log(r);
|
// console.log(r);
|
||||||
if (r.status === "error") return console.error(r.errors);
|
// if (r.status === "error") return console.error(r.errors);
|
||||||
|
|
||||||
if (r.data) {
|
// if (r.data) {
|
||||||
setCurrClanURI(r.data);
|
// setCurrClanURI(r.data);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
export const MachineListView: Component = () => {
|
export const MachineListView: Component = () => {
|
||||||
const [{ machines, loading }, { getMachines }] = useMachineContext();
|
// const [files, setFiles] = createSignal<FilesModel>([]);
|
||||||
|
|
||||||
const [files, setFiles] = createSignal<FilesModel>([]);
|
// pyApi.get_directory.receive((r) => {
|
||||||
pyApi.get_directory.receive((r) => {
|
// const { status } = r;
|
||||||
const { status } = r;
|
// if (status === "error") return console.error(r.errors);
|
||||||
if (status === "error") return console.error(r.errors);
|
// setFiles(r.data.files);
|
||||||
setFiles(r.data.files);
|
// });
|
||||||
});
|
|
||||||
|
|
||||||
const [services, setServices] = createSignal<ServiceModel>();
|
// const [services, setServices] = createSignal<ServiceModel>();
|
||||||
pyApi.show_mdns.receive((r) => {
|
// pyApi.show_mdns.receive((r) => {
|
||||||
const { status } = r;
|
// const { status } = r;
|
||||||
if (status === "error") return console.error(r.errors);
|
// if (status === "error") return console.error(r.errors);
|
||||||
setServices(r.data.services);
|
// setServices(r.data.services);
|
||||||
});
|
// });
|
||||||
|
|
||||||
createEffect(() => {
|
const [machines, setMachines] = createSignal<MachinesModel>({});
|
||||||
console.log(files());
|
const [loading, setLoading] = createSignal<boolean>(false);
|
||||||
});
|
|
||||||
|
|
||||||
const [data, setData] = createSignal<MachinesModel>({});
|
const listMachines = async () => {
|
||||||
createEffect(() => {
|
const uri = activeURI();
|
||||||
if (route() === "machines") getMachines();
|
if (!uri) {
|
||||||
});
|
return;
|
||||||
|
|
||||||
const unpackedMachines = () => Object.entries(data());
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const response = machines();
|
|
||||||
if (response?.status === "success") {
|
|
||||||
console.log(response.data);
|
|
||||||
setData(response.data);
|
|
||||||
toast.success("Machines loaded");
|
|
||||||
}
|
}
|
||||||
if (response?.status === "error") {
|
setLoading(true);
|
||||||
setData({});
|
const response = await callApi("list_machines", {
|
||||||
console.error(response.errors);
|
flake_url: uri,
|
||||||
toast.error("Error loading machines");
|
});
|
||||||
response.errors.forEach((error) =>
|
setLoading(false);
|
||||||
toast.error(
|
if (response.status === "success") {
|
||||||
`${error.message}: ${error.description} From ${error.location}`
|
setMachines(response.data);
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (route() === "machines") listMachines();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unpackedMachines = () => Object.entries(machines());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="max-w-screen-lg">
|
<div class="max-w-screen-lg">
|
||||||
<div class="tooltip tooltip-bottom" data-tip="Open Clan">
|
<div class="tooltip tooltip-bottom" data-tip="Open Clan"></div>
|
||||||
<button
|
|
||||||
class="btn btn-ghost"
|
|
||||||
onClick={() =>
|
|
||||||
pyApi.open_file.dispatch({
|
|
||||||
file_request: {
|
|
||||||
title: "Open Clan",
|
|
||||||
mode: "select_folder",
|
|
||||||
},
|
|
||||||
op_key: "open_clan",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span class="material-icons ">folder_open</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-bottom" data-tip="Search install targets">
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost"
|
|
||||||
onClick={() => pyApi.show_mdns.dispatch({})}
|
|
||||||
>
|
|
||||||
<span class="material-icons ">search</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-bottom" data-tip="Refresh">
|
<div class="tooltip tooltip-bottom" data-tip="Refresh">
|
||||||
<button class="btn btn-ghost" onClick={() => getMachines()}>
|
<button class="btn btn-ghost" onClick={() => listMachines()}>
|
||||||
<span class="material-icons ">refresh</span>
|
<span class="material-icons ">refresh</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Show when={services()}>
|
{/* <Show when={services()}>
|
||||||
{(services) => (
|
{(services) => (
|
||||||
<For each={Object.values(services())}>
|
<For each={Object.values(services())}>
|
||||||
{(service) => (
|
{(service) => (
|
||||||
@@ -163,7 +132,7 @@ export const MachineListView: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show> */}
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={loading()}>
|
<Match when={loading()}>
|
||||||
{/* Loading skeleton */}
|
{/* Loading skeleton */}
|
||||||
|
|||||||
98
pkgs/webview-ui/app/src/routes/settings/index.tsx
Normal file
98
pkgs/webview-ui/app/src/routes/settings/index.tsx
Normal file
@@ -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<SettingsForm>({
|
||||||
|
initialValues: {
|
||||||
|
base_dir: activeURI(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit: SubmitHandler<SettingsForm> = async (values, event) => {
|
||||||
|
//
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="card card-normal">
|
||||||
|
<Form onSubmit={handleSubmit} shouldActive>
|
||||||
|
<div class="card-body">
|
||||||
|
<Field name="base_dir" validate={[required("Clan URI is required")]}>
|
||||||
|
{(field, props) => (
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text block after:ml-0.5 after:text-primary">
|
||||||
|
Directory
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="stats shadow">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-figure text-primary">
|
||||||
|
<span class="material-icons">inventory</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-title">Clan URI</div>
|
||||||
|
<div
|
||||||
|
class="stat-value"
|
||||||
|
classList={{ "text-slate-500": !field.value }}
|
||||||
|
>
|
||||||
|
{field.value || "Not set"}
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost mx-4"
|
||||||
|
onClick={async () => {
|
||||||
|
const location = await registerClan();
|
||||||
|
if (location) {
|
||||||
|
setActiveURI(location);
|
||||||
|
setValue(formStore, "base_dir", location);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="material-icons">edit</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="stat-desc">Where the clan source resides</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="label">
|
||||||
|
{field.error && (
|
||||||
|
<span class="label-text-alt">{field.error}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
32
pkgs/webview-ui/app/src/routes/welcome/index.tsx
Normal file
32
pkgs/webview-ui/app/src/routes/welcome/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { setActiveURI, setRoute } from "@/src/App";
|
||||||
|
import { registerClan } from "../settings";
|
||||||
|
|
||||||
|
export const Welcome = () => {
|
||||||
|
return (
|
||||||
|
<div class="hero min-h-screen">
|
||||||
|
<div class="hero-content mb-32 text-center">
|
||||||
|
<div class="max-w-md">
|
||||||
|
<h1 class="text-5xl font-bold">Welcome to Clan</h1>
|
||||||
|
<p class="py-6">Own the services you use.</p>
|
||||||
|
<div class="flex flex-col items-start gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary w-full"
|
||||||
|
onClick={() => setRoute("createClan")}
|
||||||
|
>
|
||||||
|
Build your own
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="link w-full text-right text-primary"
|
||||||
|
onClick={async () => {
|
||||||
|
const uri = await registerClan();
|
||||||
|
if (uri) setActiveURI(uri);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Or select folder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
33
pkgs/webview-ui/app/util.ts
Normal file
33
pkgs/webview-ui/app/util.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
npmDeps = pkgs.fetchNpmDeps {
|
npmDeps = pkgs.fetchNpmDeps {
|
||||||
src = ./app;
|
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.
|
# The prepack script runs the build script, which we'd rather do in the build phase.
|
||||||
npmPackFlags = [ "--ignore-scripts" ];
|
npmPackFlags = [ "--ignore-scripts" ];
|
||||||
|
|||||||
Reference in New Issue
Block a user