Merge branch 'main' into amunsen-main
This commit is contained in:
@@ -241,7 +241,7 @@ class TerminalLogger(AbstractLogger):
|
|||||||
|
|
||||||
class XMLLogger(AbstractLogger):
|
class XMLLogger(AbstractLogger):
|
||||||
def __init__(self, outfile: str) -> None:
|
def __init__(self, outfile: str) -> None:
|
||||||
self.logfile_handle = codecs.open(outfile, "wb")
|
self.logfile_handle = codecs.open(outfile, "wb") # noqa: SIM115
|
||||||
self.xml = XMLGenerator(self.logfile_handle, encoding="utf-8")
|
self.xml = XMLGenerator(self.logfile_handle, encoding="utf-8")
|
||||||
self.queue: Queue[dict[str, str]] = Queue()
|
self.queue: Queue[dict[str, str]] = Queue()
|
||||||
|
|
||||||
|
|||||||
30
flake.lock
generated
30
flake.lock
generated
@@ -7,11 +7,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1728763831,
|
"lastModified": 1729281548,
|
||||||
"narHash": "sha256-KOp33tls7jRAhcmu77aVxKpSMou8QgK0BC+Y3sYLuGo=",
|
"narHash": "sha256-MuojlSnwAJAwfhgmW8ZtZrwm2Sko4fqubCvReqbUzYw=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "b6215392ec3bd05e9ebfbb2f7945c414096fce8f",
|
"rev": "a6a3179ddf396dfc28a078e2f169354d0c137125",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -63,11 +63,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1728723853,
|
"lastModified": 1729127036,
|
||||||
"narHash": "sha256-0TWkpl3PGMjJ9cwzMwC99xRIQYwJkn0ETPMGGNN9tAM=",
|
"narHash": "sha256-NGLgmG+s6jY15TImq8i3GS0IuCCcNSt2McS20q9xRCs=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixos-images",
|
"repo": "nixos-images",
|
||||||
"rev": "74da6331449bf5c6409c672ea227c07398ebb300",
|
"rev": "3103f26e0631a543963c03c583f03fd42fd9d51a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -78,11 +78,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1728538411,
|
"lastModified": 1729265718,
|
||||||
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
|
"narHash": "sha256-4HQI+6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
|
"rev": "ccc0c2126893dd20963580b6478d1a10a4512185",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -112,11 +112,11 @@
|
|||||||
"nixpkgs-stable": []
|
"nixpkgs-stable": []
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1728345710,
|
"lastModified": 1729394972,
|
||||||
"narHash": "sha256-lpunY1+bf90ts+sA2/FgxVNIegPDKCpEoWwOPu4ITTQ=",
|
"narHash": "sha256-fADlzOzcSaGsrO+THUZ8SgckMMc7bMQftztKFCLVcFI=",
|
||||||
"owner": "Mic92",
|
"owner": "Mic92",
|
||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"rev": "06535d0e3d0201e6a8080dd32dbfde339b94f01b",
|
"rev": "c504fd7ac946d7a1b17944d73b261ca0a0b226a5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -147,11 +147,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1727984844,
|
"lastModified": 1729242555,
|
||||||
"narHash": "sha256-xpRqITAoD8rHlXQafYZOLvUXCF6cnZkPfoq67ThN0Hc=",
|
"narHash": "sha256-6jWSWxv2crIXmYSEb3LEVsFkCkyVHNllk61X4uhqfCs=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "4446c7a6fc0775df028c5a3f6727945ba8400e64",
|
"rev": "d986489c1c757f6921a48c1439f19bfb9b8ecab5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -9,7 +10,7 @@ from clan_app.api import GObjApi, GResult, ImplFunc
|
|||||||
from clan_app.api.file import open_file
|
from clan_app.api.file import open_file
|
||||||
|
|
||||||
gi.require_version("WebKit", "6.0")
|
gi.require_version("WebKit", "6.0")
|
||||||
from gi.repository import GLib, GObject, WebKit
|
from gi.repository import Gio, GLib, GObject, WebKit
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -122,21 +123,46 @@ class WebExecutor(GObject.Object):
|
|||||||
|
|
||||||
def on_result(self, source: ImplFunc, data: GResult) -> None:
|
def on_result(self, source: ImplFunc, data: GResult) -> None:
|
||||||
result = dataclass_to_dict(data.result)
|
result = dataclass_to_dict(data.result)
|
||||||
serialized = json.dumps(result, indent=4)
|
# Important:
|
||||||
|
# 2. ensure_ascii = False. non-ASCII characters are correctly handled, instead of being escaped.
|
||||||
|
serialized = json.dumps(result, indent=4, ensure_ascii=False)
|
||||||
log.debug(f"Result for {data.method_name}: {serialized}")
|
log.debug(f"Result for {data.method_name}: {serialized}")
|
||||||
|
|
||||||
# Use idle_add to queue the response call to js on the main GTK thread
|
# Use idle_add to queue the response call to js on the main GTK thread
|
||||||
self.return_data_to_js(data.method_name, serialized)
|
self.return_data_to_js(data.method_name, serialized)
|
||||||
|
|
||||||
def return_data_to_js(self, method_name: str, serialized: str) -> bool:
|
def return_data_to_js(self, method_name: str, serialized: str) -> bool:
|
||||||
|
js = f"""
|
||||||
|
window.clan.{method_name}({serialized});
|
||||||
|
"""
|
||||||
|
|
||||||
|
def dump_failed_code() -> None:
|
||||||
|
tmp_file = Path("/tmp/clan-pyjs-bridge-error.js")
|
||||||
|
with tmp_file.open("w") as f:
|
||||||
|
f.write(js)
|
||||||
|
log.debug(f"Failed code dumped in JS file: {tmp_file}")
|
||||||
|
|
||||||
|
# Error handling if the JavaScript evaluation fails
|
||||||
|
def on_js_evaluation_finished(
|
||||||
|
webview: WebKit.WebView, task: Gio.AsyncResult
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
# Get the result of the JavaScript evaluation
|
||||||
|
value = webview.evaluate_javascript_finish(task)
|
||||||
|
if not value:
|
||||||
|
log.exception("No value returned")
|
||||||
|
dump_failed_code()
|
||||||
|
except GLib.Error:
|
||||||
|
log.exception("Error evaluating JS")
|
||||||
|
dump_failed_code()
|
||||||
|
|
||||||
self.webview.evaluate_javascript(
|
self.webview.evaluate_javascript(
|
||||||
f"""
|
js,
|
||||||
window.clan.{method_name}(`{serialized}`);
|
|
||||||
""",
|
|
||||||
-1,
|
-1,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
on_js_evaluation_finished,
|
||||||
)
|
)
|
||||||
return GLib.SOURCE_REMOVE
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
|
|||||||
@@ -134,9 +134,10 @@ def get_roles(module_path: Path) -> None | list[str]:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class ModuleInfo:
|
class ModuleInfo:
|
||||||
description: str
|
description: str
|
||||||
categories: list[str] | None
|
readme: str
|
||||||
|
categories: list[str]
|
||||||
roles: list[str] | None
|
roles: list[str] | None
|
||||||
readme: str | None = None
|
features: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
def get_modules(base_path: str) -> dict[str, str]:
|
def get_modules(base_path: str) -> dict[str, str]:
|
||||||
@@ -179,7 +180,7 @@ def get_module_info(
|
|||||||
"""
|
"""
|
||||||
Retrieves information about a module
|
Retrieves information about a module
|
||||||
"""
|
"""
|
||||||
if not module_path:
|
if not module_path.exists():
|
||||||
msg = "Module not found"
|
msg = "Module not found"
|
||||||
raise ClanError(
|
raise ClanError(
|
||||||
msg,
|
msg,
|
||||||
@@ -205,6 +206,7 @@ def get_module_info(
|
|||||||
categories=frontmatter.categories,
|
categories=frontmatter.categories,
|
||||||
roles=get_roles(module_path),
|
roles=get_roles(module_path),
|
||||||
readme=readme_content,
|
readme=readme_content,
|
||||||
|
features=["inventory"] if has_inventory_feature(module_path) else [],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ Note: This module assumes the presence of other modules and classes such as `Cla
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
|
||||||
from dataclasses import dataclass, fields, is_dataclass
|
from dataclasses import dataclass, fields, is_dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -51,7 +50,8 @@ from clan_cli.errors import ClanError
|
|||||||
def sanitize_string(s: str) -> str:
|
def sanitize_string(s: str) -> str:
|
||||||
# Using the native string sanitizer to handle all edge cases
|
# Using the native string sanitizer to handle all edge cases
|
||||||
# Remove the outer quotes '"string"'
|
# Remove the outer quotes '"string"'
|
||||||
return json.dumps(s)[1:-1]
|
# return json.dumps(s)[1:-1]
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def dataclass_to_dict(obj: Any, *, use_alias: bool = True) -> Any:
|
def dataclass_to_dict(obj: Any, *, use_alias: bool = True) -> Any:
|
||||||
@@ -161,7 +161,7 @@ def construct_value(
|
|||||||
msg = f"Expected string, got {field_value}"
|
msg = f"Expected string, got {field_value}"
|
||||||
raise ClanError(msg, location=f"{loc}")
|
raise ClanError(msg, location=f"{loc}")
|
||||||
|
|
||||||
return json.loads(f'"{field_value}"')
|
return field_value
|
||||||
|
|
||||||
if t is int and not isinstance(field_value, str):
|
if t is int and not isinstance(field_value, str):
|
||||||
return int(field_value) # type: ignore
|
return int(field_value) # type: ignore
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
runCommand,
|
runCommand,
|
||||||
setuptools,
|
setuptools,
|
||||||
stdenv,
|
stdenv,
|
||||||
|
nixVersions,
|
||||||
|
|
||||||
# custom args
|
# custom args
|
||||||
clan-core-path,
|
clan-core-path,
|
||||||
@@ -67,24 +68,33 @@ let
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
# Create a custom nixpkgs for use within the project
|
# Create a custom nixpkgs for use within the project
|
||||||
nixpkgs' = runCommand "nixpkgs" { nativeBuildInputs = [ nix ]; } ''
|
|
||||||
mkdir $out
|
|
||||||
cat > $out/flake.nix << EOF
|
|
||||||
{
|
|
||||||
description = "dependencies for the clan-cli";
|
|
||||||
|
|
||||||
inputs = {
|
nixpkgs' =
|
||||||
nixpkgs.url = "path://${nixpkgs}";
|
runCommand "nixpkgs"
|
||||||
};
|
{
|
||||||
|
nativeBuildInputs = [
|
||||||
|
# old nix version doesn't support --flake flag
|
||||||
|
(if lib.versionAtLeast nix.version "2.24" then nix else nixVersions.latest)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
mkdir $out
|
||||||
|
cat > $out/flake.nix << EOF
|
||||||
|
{
|
||||||
|
description = "dependencies for the clan-cli";
|
||||||
|
|
||||||
outputs = _inputs: { };
|
inputs = {
|
||||||
}
|
nixpkgs.url = "path://${nixpkgs}";
|
||||||
EOF
|
};
|
||||||
ln -sf ${nixpkgs} $out/path
|
|
||||||
nix flake update $out \
|
outputs = _inputs: { };
|
||||||
--store ./. \
|
}
|
||||||
--extra-experimental-features 'nix-command flakes'
|
EOF
|
||||||
'';
|
ln -sf ${nixpkgs} $out/path
|
||||||
|
HOME=$TMPDIR nix flake update --flake $out \
|
||||||
|
--store ./. \
|
||||||
|
--extra-experimental-features 'nix-command flakes'
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
python3.pkgs.buildPythonApplication {
|
python3.pkgs.buildPythonApplication {
|
||||||
name = "clan-cli";
|
name = "clan-cli";
|
||||||
|
|||||||
@@ -217,8 +217,8 @@ def test_none_or_string() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_roundtrip_escape() -> None:
|
def test_roundtrip_escape() -> None:
|
||||||
assert from_dict(str, "\\n") == "\n"
|
assert from_dict(str, "\n") == "\n"
|
||||||
assert dataclass_to_dict("\n") == "\\n"
|
assert dataclass_to_dict("\n") == "\n"
|
||||||
|
|
||||||
# Test that the functions are inverses of each other
|
# Test that the functions are inverses of each other
|
||||||
# f(g(x)) == x
|
# f(g(x)) == x
|
||||||
|
|||||||
@@ -11,27 +11,25 @@ from clan_cli.api import (
|
|||||||
def test_sanitize_string() -> None:
|
def test_sanitize_string() -> None:
|
||||||
# Simple strings
|
# Simple strings
|
||||||
assert sanitize_string("Hello World") == "Hello World"
|
assert sanitize_string("Hello World") == "Hello World"
|
||||||
assert sanitize_string("Hello\nWorld") == "Hello\\nWorld"
|
assert sanitize_string("Hello\nWorld") == "Hello\nWorld"
|
||||||
assert sanitize_string("Hello\tWorld") == "Hello\\tWorld"
|
assert sanitize_string("Hello\tWorld") == "Hello\tWorld"
|
||||||
assert sanitize_string("Hello\rWorld") == "Hello\\rWorld"
|
assert sanitize_string("Hello\rWorld") == "Hello\rWorld"
|
||||||
assert sanitize_string("Hello\fWorld") == "Hello\\fWorld"
|
assert sanitize_string("Hello\fWorld") == "Hello\fWorld"
|
||||||
assert sanitize_string("Hello\vWorld") == "Hello\\u000bWorld"
|
assert sanitize_string("Hello\vWorld") == "Hello\u000bWorld"
|
||||||
assert sanitize_string("Hello\bWorld") == "Hello\\bWorld"
|
assert sanitize_string("Hello\bWorld") == "Hello\bWorld"
|
||||||
assert sanitize_string("Hello\\World") == "Hello\\\\World"
|
assert sanitize_string("Hello\\World") == "Hello\\World"
|
||||||
assert sanitize_string('Hello"World') == 'Hello\\"World'
|
assert sanitize_string('Hello"World') == 'Hello"World'
|
||||||
assert sanitize_string("Hello'World") == "Hello'World"
|
assert sanitize_string("Hello'World") == "Hello'World"
|
||||||
assert sanitize_string("Hello\0World") == "Hello\\u0000World"
|
assert sanitize_string("Hello\0World") == "Hello\x00World"
|
||||||
# Console escape characters
|
# Console escape characters
|
||||||
|
|
||||||
assert sanitize_string("\033[1mBold\033[0m") == "\\u001b[1mBold\\u001b[0m" # Red
|
assert sanitize_string("\033[1mBold\033[0m") == "\033[1mBold\033[0m" # Red
|
||||||
assert sanitize_string("\033[31mRed\033[0m") == "\\u001b[31mRed\\u001b[0m" # Blue
|
assert sanitize_string("\033[31mRed\033[0m") == "\033[31mRed\033[0m" # Blue
|
||||||
assert (
|
assert sanitize_string("\033[42mGreen\033[0m") == "\033[42mGreen\033[0m" # Green
|
||||||
sanitize_string("\033[42mGreen\033[0m") == "\\u001b[42mGreen\\u001b[0m"
|
assert sanitize_string("\033[4mUnderline\033[0m") == "\033[4mUnderline\033[0m"
|
||||||
) # Green
|
|
||||||
assert sanitize_string("\033[4mUnderline\033[0m") == "\\u001b[4mUnderline\\u001b[0m"
|
|
||||||
assert (
|
assert (
|
||||||
sanitize_string("\033[91m\033[1mBold Red\033[0m")
|
sanitize_string("\033[91m\033[1mBold Red\033[0m")
|
||||||
== "\\u001b[91m\\u001b[1mBold Red\\u001b[0m"
|
== "\033[91m\033[1mBold Red\033[0m"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -531,12 +531,12 @@ class StatusNotifierImplementation(BaseImplementation):
|
|||||||
):
|
):
|
||||||
self.add_method(method_name, in_args, out_args, callback)
|
self.add_method(method_name, in_args, out_args, callback)
|
||||||
|
|
||||||
for signal_name, value in (
|
for signal_name, signal_value in (
|
||||||
("NewIcon", ()),
|
("NewIcon", ()),
|
||||||
("NewIconThemePath", ("s",)),
|
("NewIconThemePath", ("s",)),
|
||||||
("NewStatus", ("s",)),
|
("NewStatus", ("s",)),
|
||||||
):
|
):
|
||||||
self.add_signal(signal_name, value)
|
self.add_signal(signal_name, signal_value)
|
||||||
|
|
||||||
def register(self):
|
def register(self):
|
||||||
self.menu.register()
|
self.menu.register()
|
||||||
|
|||||||
1282
pkgs/webview-ui/app/package-lock.json
generated
1282
pkgs/webview-ui/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -42,9 +42,11 @@
|
|||||||
"@solidjs/router": "^0.14.2",
|
"@solidjs/router": "^0.14.2",
|
||||||
"@tanstack/eslint-plugin-query": "^5.51.12",
|
"@tanstack/eslint-plugin-query": "^5.51.12",
|
||||||
"@tanstack/solid-query": "^5.51.2",
|
"@tanstack/solid-query": "^5.51.2",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"solid-js": "^1.8.11",
|
"solid-js": "^1.8.11",
|
||||||
|
"solid-markdown": "^2.0.13",
|
||||||
"solid-toast": "^0.5.0"
|
"solid-toast": "^0.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { A, RouteSectionProps } from "@solidjs/router";
|
|||||||
import { AppRoute, routes } from "./index";
|
import { AppRoute, routes } from "./index";
|
||||||
|
|
||||||
export const Sidebar = (props: RouteSectionProps) => {
|
export const Sidebar = (props: RouteSectionProps) => {
|
||||||
const query = createQuery(() => ({
|
const clanQuery = createQuery(() => ({
|
||||||
queryKey: [activeURI(), "meta"],
|
queryKey: [activeURI(), "meta"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const curr = activeURI();
|
const curr = activeURI();
|
||||||
@@ -21,8 +21,8 @@ export const Sidebar = (props: RouteSectionProps) => {
|
|||||||
return (
|
return (
|
||||||
<aside class="w-80 rounded-xl border border-slate-900 bg-slate-800 pb-10">
|
<aside class="w-80 rounded-xl border border-slate-900 bg-slate-800 pb-10">
|
||||||
<div class="m-4 flex flex-col text-center capitalize text-white">
|
<div class="m-4 flex flex-col text-center capitalize text-white">
|
||||||
<span class="text-lg">{query.data?.name}</span>
|
<span class="text-lg">{clanQuery.data?.name}</span>
|
||||||
<span class="text-sm">{query.data?.description}</span>
|
<span class="text-sm">{clanQuery.data?.description}</span>
|
||||||
<RouteMenu class="menu px-4 py-2" routes={routes} />
|
<RouteMenu class="menu px-4 py-2" routes={routes} />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -150,14 +150,13 @@ export const callApi = <K extends OperationNames>(
|
|||||||
|
|
||||||
const deserialize =
|
const deserialize =
|
||||||
<T>(fn: (response: T) => void) =>
|
<T>(fn: (response: T) => void) =>
|
||||||
(str: string) => {
|
(r: unknown) => {
|
||||||
try {
|
try {
|
||||||
const r = JSON.parse(str) as T;
|
fn(r as T);
|
||||||
fn(r);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Error parsing JSON: ", e);
|
console.error("Error parsing JSON: ", e);
|
||||||
window.localStorage.setItem("error", str);
|
window.localStorage.setItem("error", JSON.stringify(r));
|
||||||
console.error(str);
|
console.error(r);
|
||||||
console.error("See localStorage 'error'");
|
console.error("See localStorage 'error'");
|
||||||
alert(`Error parsing JSON: ${e}`);
|
alert(`Error parsing JSON: ${e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { Flash } from "./routes/flash/view";
|
|||||||
import { HostList } from "./routes/hosts/view";
|
import { HostList } from "./routes/hosts/view";
|
||||||
import { Welcome } from "./routes/welcome";
|
import { Welcome } from "./routes/welcome";
|
||||||
import { Toaster } from "solid-toast";
|
import { Toaster } from "solid-toast";
|
||||||
|
import { ModuleList } from "./routes/modules/list";
|
||||||
|
import { ModuleDetails } from "./routes/modules/details";
|
||||||
|
|
||||||
export const client = new QueryClient();
|
export const client = new QueryClient();
|
||||||
|
|
||||||
@@ -88,6 +90,24 @@ export const routes: AppRoute[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/modules",
|
||||||
|
label: "Modules",
|
||||||
|
icon: "apps",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
label: "App Store",
|
||||||
|
component: () => <ModuleList />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/:id",
|
||||||
|
label: "Details",
|
||||||
|
hidden: true,
|
||||||
|
component: () => <ModuleDetails />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/tools",
|
path: "/tools",
|
||||||
label: "Tools",
|
label: "Tools",
|
||||||
|
|||||||
25
pkgs/webview-ui/app/src/queries/index.ts
Normal file
25
pkgs/webview-ui/app/src/queries/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
|
import { callApi } from "../api";
|
||||||
|
import toast from "solid-toast";
|
||||||
|
|
||||||
|
export const createModulesQuery = (uri: string | null) =>
|
||||||
|
createQuery(() => ({
|
||||||
|
queryKey: [uri, "list_modules"],
|
||||||
|
placeholderData: [],
|
||||||
|
enabled: !!uri,
|
||||||
|
queryFn: async () => {
|
||||||
|
console.log({ uri });
|
||||||
|
if (uri) {
|
||||||
|
const response = await callApi("list_modules", {
|
||||||
|
base_path: uri,
|
||||||
|
});
|
||||||
|
console.log({ response });
|
||||||
|
if (response.status === "error") {
|
||||||
|
toast.error("Failed to fetch data");
|
||||||
|
} else {
|
||||||
|
return Object.entries(response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
}));
|
||||||
303
pkgs/webview-ui/app/src/routes/modules/details.tsx
Normal file
303
pkgs/webview-ui/app/src/routes/modules/details.tsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import { callApi, SuccessData } from "@/src/api";
|
||||||
|
import { activeURI } from "@/src/App";
|
||||||
|
import { BackButton } from "@/src/components/BackButton";
|
||||||
|
import { createModulesQuery } from "@/src/queries";
|
||||||
|
import { useParams } from "@solidjs/router";
|
||||||
|
import { createEffect, For, Match, Show, Switch } from "solid-js";
|
||||||
|
import { SolidMarkdown } from "solid-markdown";
|
||||||
|
import toast from "solid-toast";
|
||||||
|
import { ModuleInfo } from "./list";
|
||||||
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
|
import { JSONSchema4 } from "json-schema";
|
||||||
|
import { TextInput } from "@/src/components/TextInput";
|
||||||
|
import {
|
||||||
|
createForm,
|
||||||
|
getValue,
|
||||||
|
setValue,
|
||||||
|
SubmitHandler,
|
||||||
|
} from "@modular-forms/solid";
|
||||||
|
|
||||||
|
export const ModuleDetails = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const modulesQuery = createModulesQuery(activeURI());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="p-1">
|
||||||
|
<BackButton />
|
||||||
|
<div class="p-2">
|
||||||
|
<h3 class="text-2xl">{params.id}</h3>
|
||||||
|
<Switch>
|
||||||
|
<Match when={modulesQuery.data?.find((i) => i[0] === params.id)}>
|
||||||
|
{(d) => <Details data={d()[1]} id={d()[0]} />}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function deepMerge(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
obj1: Record<string, any>,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
obj2: Record<string, any>,
|
||||||
|
) {
|
||||||
|
const result = { ...obj1 };
|
||||||
|
|
||||||
|
for (const key in obj2) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj2, key)) {
|
||||||
|
if (obj2[key] instanceof Object && obj1[key] instanceof Object) {
|
||||||
|
result[key] = deepMerge(obj1[key], obj2[key]);
|
||||||
|
} else {
|
||||||
|
result[key] = obj2[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DetailsProps {
|
||||||
|
data: ModuleInfo;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
const Details = (props: DetailsProps) => {
|
||||||
|
return (
|
||||||
|
<div class="flex w-full flex-col gap-2">
|
||||||
|
<article class="prose">{props.data.description}</article>
|
||||||
|
<span class="label-text">Categories</span>
|
||||||
|
<div>
|
||||||
|
<For each={props.data.categories}>
|
||||||
|
{(c) => <div class="badge badge-primary m-1">{c}</div>}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<span class="label-text">Roles</span>
|
||||||
|
<div>
|
||||||
|
<For each={props.data.roles}>
|
||||||
|
{(r) => <div class="badge badge-secondary m-1">{r}</div>}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<SolidMarkdown>{props.data.readme}</SolidMarkdown>
|
||||||
|
</div>
|
||||||
|
<div class="my-2 flex w-full gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
onClick={async () => {
|
||||||
|
const uri = activeURI();
|
||||||
|
if (!uri) return;
|
||||||
|
const res = await callApi("get_inventory", { base_path: uri });
|
||||||
|
if (res.status === "error") {
|
||||||
|
toast.error("Failed to fetch inventory");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inventory = res.data;
|
||||||
|
const newInventory = deepMerge(inventory, {
|
||||||
|
services: {
|
||||||
|
[props.id]: {
|
||||||
|
default: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
callApi("set_inventory", {
|
||||||
|
flake_dir: uri,
|
||||||
|
inventory: newInventory,
|
||||||
|
message: `Add module: ${props.id} in 'default' instance`,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="material-icons ">add</span>
|
||||||
|
Add to Clan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ModuleForm id={props.id} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModuleSchemasType = Record<string, Record<string, JSONSchema4>>;
|
||||||
|
|
||||||
|
const Unsupported = (props: { schema: JSONSchema4; what: string }) => (
|
||||||
|
<div>
|
||||||
|
Cannot render {props.what}
|
||||||
|
<pre>
|
||||||
|
<code>{JSON.stringify(props.schema, null, 2)}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function removeTrailingS(str: string) {
|
||||||
|
// Check if the last character is "s" or "S"
|
||||||
|
if (str.endsWith("s") || str.endsWith("S")) {
|
||||||
|
return str.slice(0, -1); // Remove the last character
|
||||||
|
}
|
||||||
|
return str; // Return unchanged if no trailing "s"
|
||||||
|
}
|
||||||
|
interface SchemaFormProps {
|
||||||
|
title: string;
|
||||||
|
schema: JSONSchema4;
|
||||||
|
path: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModuleForm = (props: { id: string }) => {
|
||||||
|
// TODO: Fetch the synced schema for all the modules at runtime
|
||||||
|
// We use static schema file at build time for now. (Different versions might have different schema at runtime)
|
||||||
|
const schemaQuery = createQuery(() => ({
|
||||||
|
queryKey: [activeURI(), "modules_schema"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const moduleSchema = await import(
|
||||||
|
"../../../api/modules_schemas.json"
|
||||||
|
).then((m) => m.default as ModuleSchemasType);
|
||||||
|
|
||||||
|
return moduleSchema;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log("Schema Query", schemaQuery.data?.[props.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [formStore, { Form, Field }] = createForm();
|
||||||
|
const handleSubmit: SubmitHandler<NonNullable<unknown>> = async (
|
||||||
|
values,
|
||||||
|
event,
|
||||||
|
) => {
|
||||||
|
console.log("Submitted form values", values);
|
||||||
|
};
|
||||||
|
const SchemaForm = (props: SchemaFormProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
fallback={<Unsupported what={"schema"} schema={props.schema} />}
|
||||||
|
>
|
||||||
|
<Match when={props.schema.type === "object"}>
|
||||||
|
<Switch
|
||||||
|
fallback={<Unsupported what={"object"} schema={props.schema} />}
|
||||||
|
>
|
||||||
|
<Match
|
||||||
|
when={
|
||||||
|
!props.schema.additionalProperties && props.schema.properties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(properties) => (
|
||||||
|
<For each={Object.entries(properties())}>
|
||||||
|
{([key, value]) => (
|
||||||
|
<SchemaForm
|
||||||
|
title={key}
|
||||||
|
schema={value}
|
||||||
|
path={[...props.path, key]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
<Match
|
||||||
|
when={
|
||||||
|
typeof props.schema.additionalProperties == "object" &&
|
||||||
|
props.schema.additionalProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(additionalProperties) => (
|
||||||
|
<>
|
||||||
|
<div>{props.title}</div>
|
||||||
|
{/* @ts-expect-error: We don't know the field names ahead of time */}
|
||||||
|
<Field name={props.title}>
|
||||||
|
{(f, p) => (
|
||||||
|
<>
|
||||||
|
<Show when={f.value}>
|
||||||
|
<For
|
||||||
|
each={Object.entries(
|
||||||
|
f.value as Record<string, unknown>,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(v) => (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{removeTrailingS(props.title)}: {v[0]}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<SchemaForm
|
||||||
|
path={[...props.path, v[0]]}
|
||||||
|
schema={additionalProperties()}
|
||||||
|
title={v[0]}
|
||||||
|
/>{" "}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const value = getValue(formStore, props.title);
|
||||||
|
setValue(formStore, props.title, {
|
||||||
|
// @ts-expect-error: TODO: check to be an object
|
||||||
|
...value,
|
||||||
|
foo: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Match>
|
||||||
|
<Match when={props.schema.type === "array"}>
|
||||||
|
TODO: Array field "{props.title}"
|
||||||
|
</Match>
|
||||||
|
<Match when={props.schema.type === "string"}>
|
||||||
|
{/* @ts-expect-error: We dont know the field names ahead of time */}
|
||||||
|
<Field name={props.path.join(".")}>
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<TextInput
|
||||||
|
formStore={formStore}
|
||||||
|
inputProps={fieldProps}
|
||||||
|
label={props.title}
|
||||||
|
// @ts-expect-error: It is a string, otherwise the json schema would be invalid
|
||||||
|
value={field.value ?? ""}
|
||||||
|
error={field.error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="ModuleForm">
|
||||||
|
<Switch fallback={"No Schema found"}>
|
||||||
|
<Match when={schemaQuery.isLoading}>Loading...</Match>
|
||||||
|
<Match when={schemaQuery.data?.[props.id]}>
|
||||||
|
{(rolesSchemas) => (
|
||||||
|
<>
|
||||||
|
Configure this module
|
||||||
|
<For each={Object.entries(rolesSchemas())}>
|
||||||
|
{([role, schema]) => (
|
||||||
|
<div class="my-2">
|
||||||
|
<h4 class="text-xl">{role}</h4>
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<SchemaForm title={role} schema={schema} path={[]} />
|
||||||
|
<br />
|
||||||
|
<button class="btn btn-primary">Save</button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
45
pkgs/webview-ui/app/src/routes/modules/list.tsx
Normal file
45
pkgs/webview-ui/app/src/routes/modules/list.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { callApi, SuccessData } from "@/src/api";
|
||||||
|
import { activeURI } from "@/src/App";
|
||||||
|
import { createModulesQuery } from "@/src/queries";
|
||||||
|
import { A, useNavigate } from "@solidjs/router";
|
||||||
|
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||||
|
import { createEffect, For, Match, Switch } from "solid-js";
|
||||||
|
import { SolidMarkdown } from "solid-markdown";
|
||||||
|
|
||||||
|
export type ModuleInfo = SuccessData<"list_modules">[string];
|
||||||
|
|
||||||
|
const ModuleListItem = (props: { name: string; info: ModuleInfo }) => {
|
||||||
|
const { name, info } = props;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-figure text-primary">
|
||||||
|
<div class="join">more</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<A href={`/modules/${name}`}>
|
||||||
|
<div class="stat-value underline">{name}</div>
|
||||||
|
</A>
|
||||||
|
|
||||||
|
<div>{info.description}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModuleList = () => {
|
||||||
|
const modulesQuery = createModulesQuery(activeURI());
|
||||||
|
return (
|
||||||
|
<Switch fallback="Shit">
|
||||||
|
<Match when={modulesQuery.isLoading}>Loading....</Match>
|
||||||
|
<Match when={modulesQuery.data}>
|
||||||
|
<div>
|
||||||
|
Show Modules
|
||||||
|
<For each={modulesQuery.data}>
|
||||||
|
{([k, v]) => <ModuleListItem info={v} name={k} />}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user