clan-app: Add cancellable tasks
This commit is contained in:
@@ -20,6 +20,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../webview-lib"
|
"path": "../webview-lib"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../clan-cli/clan_lib"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
17
pkgs/clan-app/clan_app/api/cancel.py
Normal file
17
pkgs/clan-app/clan_app/api/cancel.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from clan_lib.api import ErrorDataClass, SuccessDataClass
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_task(
|
||||||
|
task_id: str, *, op_key: str
|
||||||
|
) -> SuccessDataClass[None] | ErrorDataClass:
|
||||||
|
"""Cancel a task by its op_key."""
|
||||||
|
log.info(f"Cancelling task with op_key: {task_id}")
|
||||||
|
return SuccessDataClass(
|
||||||
|
op_key=op_key,
|
||||||
|
data=None,
|
||||||
|
status="success",
|
||||||
|
)
|
||||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
|||||||
from clan_cli.custom_logger import setup_logging
|
from clan_cli.custom_logger import setup_logging
|
||||||
from clan_lib.api import API
|
from clan_lib.api import API
|
||||||
|
|
||||||
|
from clan_app.api.cancel import cancel_task
|
||||||
from clan_app.api.file_gtk import open_file
|
from clan_app.api.file_gtk import open_file
|
||||||
from clan_app.deps.webview.webview import Size, SizeHint, Webview
|
from clan_app.deps.webview.webview import Size, SizeHint, Webview
|
||||||
|
|
||||||
@@ -42,6 +43,8 @@ def app_run(app_opts: ClanAppOptions) -> int:
|
|||||||
webview = Webview(debug=app_opts.debug)
|
webview = Webview(debug=app_opts.debug)
|
||||||
|
|
||||||
API.overwrite_fn(open_file)
|
API.overwrite_fn(open_file)
|
||||||
|
# breakpoint()
|
||||||
|
API.overwrite_fn(cancel_task)
|
||||||
webview.bind_jsonschema_api(API)
|
webview.bind_jsonschema_api(API)
|
||||||
webview.size = Size(1280, 1024, SizeHint.NONE)
|
webview.size = Size(1280, 1024, SizeHint.NONE)
|
||||||
webview.navigate(content_uri)
|
webview.navigate(content_uri)
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ class Webview:
|
|||||||
reconciled_arguments["op_key"] = seq.decode()
|
reconciled_arguments["op_key"] = seq.decode()
|
||||||
# TODO: We could remove the wrapper in the MethodRegistry
|
# TODO: We could remove the wrapper in the MethodRegistry
|
||||||
# and just call the method directly
|
# and just call the method directly
|
||||||
|
|
||||||
result = wrap_method(**reconciled_arguments)
|
result = wrap_method(**reconciled_arguments)
|
||||||
|
|
||||||
serialized = json.dumps(
|
serialized = json.dumps(
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ class FileRequest:
|
|||||||
initial_folder: str | None = field(default=None)
|
initial_folder: str | None = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
@API.register_abstract
|
||||||
|
def cancel_task(task_id: str) -> None:
|
||||||
|
"""Cancel a task by its op_key."""
|
||||||
|
|
||||||
|
|
||||||
@API.register_abstract
|
@API.register_abstract
|
||||||
def open_file(file_request: FileRequest) -> list[str] | None:
|
def open_file(file_request: FileRequest) -> list[str] | None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,10 +7,15 @@ pkgs.clangStdenv.mkDerivation {
|
|||||||
src = pkgs.fetchFromGitHub {
|
src = pkgs.fetchFromGitHub {
|
||||||
owner = "webview";
|
owner = "webview";
|
||||||
repo = "webview";
|
repo = "webview";
|
||||||
rev = "83a4b4a5bbcb4b0ba2ca3ee226c2da1414719106";
|
rev = "f1a9d6b6fb8bcc2e266057224887a3d628f30f90";
|
||||||
sha256 = "sha256-5R8kllvP2EBuDANIl07fxv/EcbPpYgeav8Wfz7Kt13c=";
|
sha256 = "sha256-sK7GXDbb2zEntWH5ylC2B39zW+gXvqQ1l843gvziDZo=";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# We add the function id to the promise to be able to cancel it through the UI
|
||||||
|
# We disallow remote connections from the UI on Linux
|
||||||
|
# TODO: Disallow remote connections on MacOS
|
||||||
|
patches = [ ./fixes.patch ];
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
"out"
|
"out"
|
||||||
"dev"
|
"dev"
|
||||||
|
|||||||
54
pkgs/webview-lib/fixes.patch
Normal file
54
pkgs/webview-lib/fixes.patch
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
diff --git a/core/include/webview/detail/backends/gtk_webkitgtk.hh b/core/include/webview/detail/backends/gtk_webkitgtk.hh
|
||||||
|
index f44db8f..b5657ca 100644
|
||||||
|
--- a/core/include/webview/detail/backends/gtk_webkitgtk.hh
|
||||||
|
+++ b/core/include/webview/detail/backends/gtk_webkitgtk.hh
|
||||||
|
@@ -303,6 +303,37 @@ private:
|
||||||
|
add_init_script("function(message) {\n\
|
||||||
|
return window.webkit.messageHandlers.__webview__.postMessage(message);\n\
|
||||||
|
}");
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+ //===================MY CHANGES=========================
|
||||||
|
+ // TODO: Would be nice to have this configurable from the API.
|
||||||
|
+ auto on_decide_policy = +[] (WebKitWebView *,
|
||||||
|
+ WebKitPolicyDecision *decision,
|
||||||
|
+ WebKitPolicyDecisionType decision_type, gpointer) -> gboolean {
|
||||||
|
+ if (decision_type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
|
||||||
|
+ return FALSE; // Continue with the default handler
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ WebKitNavigationPolicyDecision * navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION (decision);
|
||||||
|
+ WebKitNavigationAction * navigation_action = webkit_navigation_policy_decision_get_navigation_action (navigation_decision);
|
||||||
|
+ WebKitURIRequest * request = webkit_navigation_action_get_request (navigation_action);
|
||||||
|
+ const char * uri = webkit_uri_request_get_uri (request);
|
||||||
|
+
|
||||||
|
+ if (g_str_has_prefix(uri, "file://") ||
|
||||||
|
+ g_str_has_prefix(uri, "http://localhost") ||
|
||||||
|
+ g_str_has_prefix(uri, "http://127.0.0.1") ||
|
||||||
|
+ g_str_has_prefix(uri, "http://[::1]")) {
|
||||||
|
+ printf("Allowing %s URI\n", uri);
|
||||||
|
+ return FALSE; // Continue with the default handler
|
||||||
|
+ } else {
|
||||||
|
+ printf("Blocking %s URI at %s:%d\n", uri, __FILE__, __LINE__);
|
||||||
|
+ webkit_policy_decision_ignore(decision);
|
||||||
|
+ return TRUE; // Stop the default handler
|
||||||
|
+ }
|
||||||
|
+ };
|
||||||
|
+ g_signal_connect(GTK_WIDGET(m_webview), "decide-policy",
|
||||||
|
+ G_CALLBACK(on_decide_policy), this);
|
||||||
|
+ //============END=========
|
||||||
|
}
|
||||||
|
|
||||||
|
void window_settings(bool debug) {
|
||||||
|
diff --git a/core/include/webview/detail/engine_base.hh b/core/include/webview/detail/engine_base.hh
|
||||||
|
index 01c8d29..8ea5622 100644
|
||||||
|
--- a/core/include/webview/detail/engine_base.hh
|
||||||
|
+++ b/core/include/webview/detail/engine_base.hh
|
||||||
|
@@ -232,6 +232,7 @@ protected:
|
||||||
|
var promise = new Promise(function(resolve, reject) {\n\
|
||||||
|
_promises[_id] = { resolve, reject };\n\
|
||||||
|
});\n\
|
||||||
|
+ promise._webviewMessageId = _id;\n\
|
||||||
|
this.post(JSON.stringify({\n\
|
||||||
|
id: _id,\n\
|
||||||
|
method: method,\n\
|
||||||
54
pkgs/webview-ui/app/package-lock.json
generated
54
pkgs/webview-ui/app/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||||
"@floating-ui/dom": "^1.6.8",
|
"@floating-ui/dom": "^1.6.8",
|
||||||
"@modular-forms/solid": "^0.21.0",
|
"@modular-forms/solid": "^0.21.0",
|
||||||
"@solid-primitives/storage": "^3.7.1",
|
"@solid-primitives/storage": "^3.7.1",
|
||||||
@@ -63,7 +64,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
@@ -91,7 +91,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.27.1",
|
"@babel/helper-validator-identifier": "^7.27.1",
|
||||||
@@ -106,7 +105,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz",
|
||||||
"integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==",
|
"integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -116,7 +114,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
||||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
@@ -147,7 +144,6 @@
|
|||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -157,7 +153,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
|
||||||
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
|
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.27.1",
|
"@babel/parser": "^7.27.1",
|
||||||
@@ -174,7 +169,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz",
|
||||||
"integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==",
|
"integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.27.1",
|
"@babel/compat-data": "^7.27.1",
|
||||||
@@ -191,7 +185,6 @@
|
|||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
@@ -201,7 +194,6 @@
|
|||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -211,7 +203,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/traverse": "^7.27.1",
|
"@babel/traverse": "^7.27.1",
|
||||||
@@ -225,7 +216,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
|
||||||
"integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
|
"integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-imports": "^7.27.1",
|
"@babel/helper-module-imports": "^7.27.1",
|
||||||
@@ -243,7 +233,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -253,7 +242,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -263,7 +251,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -273,7 +260,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -283,7 +269,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
|
||||||
"integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
|
"integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/template": "^7.27.1",
|
"@babel/template": "^7.27.1",
|
||||||
@@ -297,7 +282,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz",
|
||||||
"integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==",
|
"integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.27.1"
|
"@babel/types": "^7.27.1"
|
||||||
@@ -309,6 +293,21 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/plugin-syntax-import-attributes": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/plugin-syntax-jsx": {
|
"node_modules/@babel/plugin-syntax-jsx": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
|
||||||
@@ -345,7 +344,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz",
|
||||||
"integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==",
|
"integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
@@ -360,7 +358,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
|
||||||
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
|
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
@@ -379,7 +376,6 @@
|
|||||||
"version": "11.12.0",
|
"version": "11.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
@@ -389,7 +385,6 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
|
||||||
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
|
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.27.1",
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
@@ -1257,7 +1252,6 @@
|
|||||||
"version": "0.3.8",
|
"version": "0.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/set-array": "^1.2.1",
|
"@jridgewell/set-array": "^1.2.1",
|
||||||
@@ -1272,7 +1266,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -1282,7 +1275,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -1292,14 +1284,12 @@
|
|||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.25",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -2840,7 +2830,6 @@
|
|||||||
"version": "4.24.5",
|
"version": "4.24.5",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
||||||
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -2916,7 +2905,6 @@
|
|||||||
"version": "1.0.30001717",
|
"version": "1.0.30001717",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz",
|
||||||
"integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==",
|
"integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3104,7 +3092,6 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/corvu": {
|
"node_modules/corvu": {
|
||||||
@@ -3506,7 +3493,6 @@
|
|||||||
"version": "1.5.150",
|
"version": "1.5.150",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.150.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.150.tgz",
|
||||||
"integrity": "sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==",
|
"integrity": "sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
@@ -3620,7 +3606,6 @@
|
|||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -4065,7 +4050,6 @@
|
|||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -4581,7 +4565,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@@ -4641,7 +4624,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jsesc": "bin/jsesc"
|
"jsesc": "bin/jsesc"
|
||||||
@@ -4672,7 +4654,6 @@
|
|||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
@@ -5577,7 +5558,6 @@
|
|||||||
"version": "2.0.19",
|
"version": "2.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
@@ -7424,7 +7404,6 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -7993,7 +7972,6 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||||
"@floating-ui/dom": "^1.6.8",
|
"@floating-ui/dom": "^1.6.8",
|
||||||
"@modular-forms/solid": "^0.21.0",
|
"@modular-forms/solid": "^0.21.0",
|
||||||
"@solid-primitives/storage": "^3.7.1",
|
"@solid-primitives/storage": "^3.7.1",
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import schema from "@/api/API.json" assert { type: "json" };
|
|
||||||
import { API, Error } from "@/api/API";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { Schema as Inventory } from "@/api/Inventory";
|
|
||||||
|
|
||||||
export type OperationNames = keyof API;
|
|
||||||
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
|
|
||||||
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
|
||||||
|
|
||||||
export type ApiEnvelope<T> =
|
|
||||||
| {
|
|
||||||
status: "success";
|
|
||||||
data: T;
|
|
||||||
op_key: string;
|
|
||||||
}
|
|
||||||
| Error;
|
|
||||||
|
|
||||||
export type Services = NonNullable<Inventory["services"]>;
|
|
||||||
export type ServiceNames = keyof Services;
|
|
||||||
export type ClanService<T extends ServiceNames> = Services[T];
|
|
||||||
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
|
|
||||||
Services[T]
|
|
||||||
>[string];
|
|
||||||
|
|
||||||
export type SuccessQuery<T extends OperationNames> = Extract<
|
|
||||||
OperationResponse<T>,
|
|
||||||
{ status: "success" }
|
|
||||||
>;
|
|
||||||
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
|
||||||
|
|
||||||
export type ErrorQuery<T extends OperationNames> = Extract<
|
|
||||||
OperationResponse<T>,
|
|
||||||
{ status: "error" }
|
|
||||||
>;
|
|
||||||
export type ErrorData<T extends OperationNames> = ErrorQuery<T>["errors"];
|
|
||||||
|
|
||||||
export type ClanOperations = Record<OperationNames, (str: string) => void>;
|
|
||||||
|
|
||||||
export interface GtkResponse<T> {
|
|
||||||
result: T;
|
|
||||||
op_key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const callApi = async <K extends OperationNames>(
|
|
||||||
method: K,
|
|
||||||
args: OperationArgs<K>,
|
|
||||||
): Promise<OperationResponse<K>> => {
|
|
||||||
console.log("Calling API", method, args);
|
|
||||||
const response = await (
|
|
||||||
window as unknown as Record<
|
|
||||||
OperationNames,
|
|
||||||
(
|
|
||||||
args: OperationArgs<OperationNames>,
|
|
||||||
) => Promise<OperationResponse<OperationNames>>
|
|
||||||
>
|
|
||||||
)[method](args);
|
|
||||||
return response as OperationResponse<K>;
|
|
||||||
};
|
|
||||||
132
pkgs/webview-ui/app/src/api/index.tsx
Normal file
132
pkgs/webview-ui/app/src/api/index.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import schema from "@/api/API.json" with { type: "json" };
|
||||||
|
import { API, Error } from "@/api/API";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { Schema as Inventory } from "@/api/Inventory";
|
||||||
|
import { toast, Toast } from "solid-toast";
|
||||||
|
import {
|
||||||
|
ErrorToastComponent,
|
||||||
|
InfoToastComponent,
|
||||||
|
} from "@/src/components/toast";
|
||||||
|
export type OperationNames = keyof API;
|
||||||
|
export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
|
||||||
|
export type OperationResponse<T extends OperationNames> = API[T]["return"];
|
||||||
|
|
||||||
|
export type ApiEnvelope<T> =
|
||||||
|
| {
|
||||||
|
status: "success";
|
||||||
|
data: T;
|
||||||
|
op_key: string;
|
||||||
|
}
|
||||||
|
| Error;
|
||||||
|
|
||||||
|
export type Services = NonNullable<Inventory["services"]>;
|
||||||
|
export type ServiceNames = keyof Services;
|
||||||
|
export type ClanService<T extends ServiceNames> = Services[T];
|
||||||
|
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
|
||||||
|
Services[T]
|
||||||
|
>[string];
|
||||||
|
|
||||||
|
export type SuccessQuery<T extends OperationNames> = Extract<
|
||||||
|
OperationResponse<T>,
|
||||||
|
{ status: "success" }
|
||||||
|
>;
|
||||||
|
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
|
||||||
|
|
||||||
|
export type ErrorQuery<T extends OperationNames> = Extract<
|
||||||
|
OperationResponse<T>,
|
||||||
|
{ status: "error" }
|
||||||
|
>;
|
||||||
|
export type ErrorData<T extends OperationNames> = ErrorQuery<T>["errors"];
|
||||||
|
|
||||||
|
export type ClanOperations = Record<OperationNames, (str: string) => void>;
|
||||||
|
|
||||||
|
export interface GtkResponse<T> {
|
||||||
|
result: T;
|
||||||
|
op_key: string;
|
||||||
|
}
|
||||||
|
const _callApi = <K extends OperationNames>(
|
||||||
|
method: K,
|
||||||
|
args: OperationArgs<K>,
|
||||||
|
): { promise: Promise<OperationResponse<K>>; op_key: string } => {
|
||||||
|
const promise = (
|
||||||
|
window as unknown as Record<
|
||||||
|
OperationNames,
|
||||||
|
(
|
||||||
|
args: OperationArgs<OperationNames>,
|
||||||
|
) => Promise<OperationResponse<OperationNames>>
|
||||||
|
>
|
||||||
|
)[method](args) as Promise<OperationResponse<K>>;
|
||||||
|
const op_key = (promise as any)._webviewMessageId as string;
|
||||||
|
debugger;
|
||||||
|
return { promise, op_key };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = async (ops_key: string) => {
|
||||||
|
console.log("Canceling operation: ", ops_key);
|
||||||
|
const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key });
|
||||||
|
const resp = await promise;
|
||||||
|
if (resp.status === "error") {
|
||||||
|
toast.custom(
|
||||||
|
(t) => (
|
||||||
|
<ErrorToastComponent
|
||||||
|
t={t}
|
||||||
|
message={"Failed to cancel operation: " + ops_key}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
duration: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.custom(
|
||||||
|
(t) => (
|
||||||
|
<InfoToastComponent t={t} message={"Canceled operation: " + ops_key} />
|
||||||
|
),
|
||||||
|
{
|
||||||
|
duration: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log("Cancel response: ", resp);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const callApi = async <K extends OperationNames>(
|
||||||
|
method: K,
|
||||||
|
args: OperationArgs<K>,
|
||||||
|
): Promise<OperationResponse<K>> => {
|
||||||
|
console.log("Calling API", method, args);
|
||||||
|
const { promise, op_key } = _callApi(method, args);
|
||||||
|
|
||||||
|
const toastId = toast.custom(
|
||||||
|
(
|
||||||
|
t, // t is the Toast object, t.id is the id of THIS toast instance
|
||||||
|
) => (
|
||||||
|
<InfoToastComponent
|
||||||
|
t={t}
|
||||||
|
message={"Exectuting " + method}
|
||||||
|
onCancel={handleCancel.bind(null, op_key)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
duration: Infinity,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await promise;
|
||||||
|
if (response.status === "error") {
|
||||||
|
toast.remove(toastId);
|
||||||
|
toast.error(
|
||||||
|
<div>
|
||||||
|
{response.errors.map((err) => (
|
||||||
|
<p>{err.message}</p>
|
||||||
|
))}
|
||||||
|
</div>,
|
||||||
|
{
|
||||||
|
duration: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.remove(toastId);
|
||||||
|
}
|
||||||
|
return response as OperationResponse<K>;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { createMemo, JSX, Show, splitProps } from "solid-js";
|
import { createMemo, JSX, Show, splitProps } from "solid-js";
|
||||||
|
|
||||||
interface FileInputProps {
|
export interface FileInputProps {
|
||||||
ref: (element: HTMLInputElement) => void;
|
ref: (element: HTMLInputElement) => void;
|
||||||
name: string;
|
name: string;
|
||||||
value?: File[] | File;
|
value?: File[] | File;
|
||||||
|
|||||||
196
pkgs/webview-ui/app/src/components/fileSelect/index.tsx
Normal file
196
pkgs/webview-ui/app/src/components/fileSelect/index.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { FileInput, type FileInputProps } from "@/src/components/FileInput"; // Assuming FileInput can take a ref and has onClick
|
||||||
|
import { Typography } from "@/src/components/Typography";
|
||||||
|
import Fieldset from "@/src/Form/fieldset";
|
||||||
|
import Icon from "@/src/components/icon"; // For displaying file icons
|
||||||
|
import { callApi } from "@/src/api";
|
||||||
|
import type {
|
||||||
|
FieldComponent,
|
||||||
|
FieldValues,
|
||||||
|
FieldName,
|
||||||
|
} from "@modular-forms/solid";
|
||||||
|
import { Show, For, type Component, type JSX } from "solid-js";
|
||||||
|
|
||||||
|
// Types for the file dialog options passed to callApi
|
||||||
|
interface FileRequestFilter {
|
||||||
|
patterns: string[];
|
||||||
|
mime_types?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileDialogOptions {
|
||||||
|
title: string;
|
||||||
|
filters?: FileRequestFilter;
|
||||||
|
initial_folder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Props for the CustomFileField component
|
||||||
|
interface FileSelectorOpts<
|
||||||
|
TForm extends FieldValues,
|
||||||
|
TFieldName extends FieldName<TForm>,
|
||||||
|
> {
|
||||||
|
Field: FieldComponent<TForm>; // The Field component from createForm
|
||||||
|
name: TFieldName; // Name of the form field (e.g., "sshKeys", "profilePicture")
|
||||||
|
label: string; // Legend for Fieldset or main label for the input
|
||||||
|
description?: string | JSX.Element; // Optional description text
|
||||||
|
multiple?: boolean; // True if multiple files can be selected, false for single file
|
||||||
|
fileDialogOptions: FileDialogOptions; // Configuration for the custom file dialog
|
||||||
|
|
||||||
|
// Optional props for styling
|
||||||
|
inputClass?: string;
|
||||||
|
fileListClass?: string;
|
||||||
|
// You can add more specific props like `validate` if you want to pass them to Field
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileSelectorField: Component<FileSelectorOpts<any, any>> = (
|
||||||
|
props,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
Field,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
multiple = false,
|
||||||
|
fileDialogOptions,
|
||||||
|
inputClass,
|
||||||
|
fileListClass,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// Ref to the underlying HTMLInputElement (assuming FileInput forwards refs or is simple)
|
||||||
|
let actualInputElement: HTMLInputElement | undefined;
|
||||||
|
|
||||||
|
const openAndSetFiles = async (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!actualInputElement) {
|
||||||
|
console.error(
|
||||||
|
"CustomFileField: Input element ref is not set. Cannot proceed.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
const mode = multiple ? "open_multiple_files" : "open_file";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await callApi("open_file", {
|
||||||
|
file_request: {
|
||||||
|
title: fileDialogOptions.title,
|
||||||
|
mode: mode,
|
||||||
|
filters: fileDialogOptions.filters,
|
||||||
|
initial_folder: fileDialogOptions.initial_folder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
response.status === "success" &&
|
||||||
|
response.data &&
|
||||||
|
Array.isArray(response.data)
|
||||||
|
) {
|
||||||
|
(response.data as string[]).forEach((filename) => {
|
||||||
|
// Create File objects. Content is empty as we only have paths.
|
||||||
|
// Type might be generic or derived if possible.
|
||||||
|
dataTransfer.items.add(
|
||||||
|
new File([], filename, { type: "application/octet-stream" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else if (response.status === "error") {
|
||||||
|
// Consider using a toast or other user notification for API errors
|
||||||
|
console.error("Error from open_file API:", response.errors);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to call open_file API:", error);
|
||||||
|
// Consider using a toast here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the FileList on the actual input element
|
||||||
|
Object.defineProperty(actualInputElement, "files", {
|
||||||
|
value: dataTransfer.files,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dispatch an 'input' event so modular-forms updates its state
|
||||||
|
const inputEvent = new Event("input", { bubbles: true, cancelable: true });
|
||||||
|
actualInputElement.dispatchEvent(inputEvent);
|
||||||
|
|
||||||
|
// Optionally, dispatch 'change' if your forms setup relies more on it
|
||||||
|
// const changeEvent = new Event("change", { bubbles: true, cancelable: true });
|
||||||
|
// actualInputElement.dispatchEvent(changeEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fieldset legend={label}>
|
||||||
|
{description &&
|
||||||
|
(typeof description === "string" ? (
|
||||||
|
<Typography hierarchy="body" size="s" weight="medium" class="mb-2">
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
description
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Field name={name} type={multiple ? "File[]" : "File"}>
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<>
|
||||||
|
{/*
|
||||||
|
This FileInput component should be clickable.
|
||||||
|
Its 'ref' needs to point to the actual <input type="file"> element.
|
||||||
|
If FileInput is complex, it might need an 'inputRef' prop or similar.
|
||||||
|
*/}
|
||||||
|
<FileInput
|
||||||
|
{...(fieldProps as FileInputProps)} // Spread modular-forms props
|
||||||
|
ref={(el: HTMLInputElement) => {
|
||||||
|
(fieldProps as any).ref(el); // Pass ref to modular-forms
|
||||||
|
actualInputElement = el; // Capture for local use
|
||||||
|
}}
|
||||||
|
class={inputClass}
|
||||||
|
multiple={multiple}
|
||||||
|
// The onClick here triggers our custom dialog logic
|
||||||
|
onClick={openAndSetFiles}
|
||||||
|
// The 'value' prop for a file input is not for displaying selected files directly.
|
||||||
|
// We'll display them below. FileInput might show placeholder text.
|
||||||
|
// value={undefined} // Explicitly not setting value from field.value here
|
||||||
|
error={field.error} // Display error from modular-forms
|
||||||
|
/>
|
||||||
|
{field.error && (
|
||||||
|
<Typography color="error" hierarchy="body" size="xs" class="mt-1">
|
||||||
|
{field.error}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Display the list of selected files */}
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
field.value &&
|
||||||
|
(multiple
|
||||||
|
? (field.value as File[]).length > 0
|
||||||
|
: field.value instanceof File)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class={`mt-2 space-y-1 ${fileListClass || ""}`}>
|
||||||
|
<For
|
||||||
|
each={
|
||||||
|
multiple
|
||||||
|
? (field.value as File[])
|
||||||
|
: field.value instanceof File
|
||||||
|
? [field.value as File]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(file) => (
|
||||||
|
<div class="flex items-center justify-between rounded border border-def-1 bg-bg-2 p-2 text-sm">
|
||||||
|
<span class="truncate" title={file.name}>
|
||||||
|
<Icon icon="File" class="mr-2 inline-block" size={14} />
|
||||||
|
{file.name}
|
||||||
|
</span>
|
||||||
|
{/* A remove button per file is complex with FileList & modular-forms.
|
||||||
|
For now, clearing all files is simpler (e.g., via FileInput's own clear).
|
||||||
|
Or, the user re-selects files to change the selection. */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
);
|
||||||
|
};
|
||||||
244
pkgs/webview-ui/app/src/components/toast/index.tsx
Normal file
244
pkgs/webview-ui/app/src/components/toast/index.tsx
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import { toast, Toast } from "solid-toast"; // Make sure to import Toast type
|
||||||
|
import { Component, JSX } from "solid-js";
|
||||||
|
|
||||||
|
// --- Icon Components ---
|
||||||
|
|
||||||
|
const ErrorIcon: Component = () => (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{ "margin-right": "10px", "flex-shrink": "0" }}
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" fill="#FF4D4F" />
|
||||||
|
<path
|
||||||
|
d="M12 7V13"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="16.5" r="1.5" fill="white" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const InfoIcon: Component = () => (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{ "margin-right": "10px", "flex-shrink": "0" }}
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" fill="#2196F3" />
|
||||||
|
<path
|
||||||
|
d="M12 11V17"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="8.5" r="1.5" fill="white" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const WarningIcon: Component = () => (
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{ "margin-right": "10px", "flex-shrink": "0" }}
|
||||||
|
>
|
||||||
|
<path d="M12 2L22 21H2L12 2Z" fill="#FFC107" />
|
||||||
|
<path
|
||||||
|
d="M12 9V14"
|
||||||
|
stroke="#424242"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="16.5" r="1" fill="#424242" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Base Props and Styles ---
|
||||||
|
|
||||||
|
export interface BaseToastProps {
|
||||||
|
t: Toast;
|
||||||
|
message: string;
|
||||||
|
onCancel?: () => void; // Optional custom function on X click
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseToastStyle: JSX.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "space-between", // To push X to the right
|
||||||
|
gap: "10px", // Space between content and close button
|
||||||
|
background: "#FFFFFF",
|
||||||
|
color: "#333333",
|
||||||
|
padding: "12px 16px",
|
||||||
|
"border-radius": "6px",
|
||||||
|
"box-shadow": "0 2px 8px rgba(0, 0, 0, 0.12)",
|
||||||
|
"font-family":
|
||||||
|
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||||
|
"font-size": "14px",
|
||||||
|
"line-height": "1.4",
|
||||||
|
"min-width": "280px",
|
||||||
|
"max-width": "450px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeButtonStyle: JSX.CSSProperties = {
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
color: "red", // As per original example's X button
|
||||||
|
"font-size": "1.5em",
|
||||||
|
"font-weight": "bold",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "0 0 0 10px", // Space to its left
|
||||||
|
"line-height": "1",
|
||||||
|
"align-self": "center", // Ensure vertical alignment
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Toast Component Definitions ---
|
||||||
|
|
||||||
|
// Error Toast
|
||||||
|
export interface ErrorToastProps extends BaseToastProps {}
|
||||||
|
export const ErrorToastComponent: Component<ErrorToastProps> = (props) => {
|
||||||
|
const handleCancelClick = () => {
|
||||||
|
if (props.onCancel) {
|
||||||
|
props.onCancel();
|
||||||
|
}
|
||||||
|
toast.dismiss(props.t.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={baseToastStyle}>
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", "align-items": "center", "flex-grow": "1" }}
|
||||||
|
>
|
||||||
|
<ErrorIcon />
|
||||||
|
<span>{props.message}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelClick}
|
||||||
|
style={closeButtonStyle}
|
||||||
|
aria-label="Close notification"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Info Toast
|
||||||
|
export interface InfoToastProps extends BaseToastProps {}
|
||||||
|
export const InfoToastComponent: Component<InfoToastProps> = (props) => {
|
||||||
|
const handleCancelClick = () => {
|
||||||
|
if (props.onCancel) {
|
||||||
|
props.onCancel();
|
||||||
|
}
|
||||||
|
toast.dismiss(props.t.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={baseToastStyle}>
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", "align-items": "center", "flex-grow": "1" }}
|
||||||
|
>
|
||||||
|
<InfoIcon />
|
||||||
|
<span>{props.message}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelClick}
|
||||||
|
style={closeButtonStyle}
|
||||||
|
aria-label="Close notification"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Warning Toast
|
||||||
|
export interface WarningToastProps extends BaseToastProps {}
|
||||||
|
export const WarningToastComponent: Component<WarningToastProps> = (props) => {
|
||||||
|
const handleCancelClick = () => {
|
||||||
|
if (props.onCancel) {
|
||||||
|
props.onCancel();
|
||||||
|
}
|
||||||
|
toast.dismiss(props.t.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={baseToastStyle}>
|
||||||
|
<div
|
||||||
|
style={{ display: "flex", "align-items": "center", "flex-grow": "1" }}
|
||||||
|
>
|
||||||
|
<WarningIcon />
|
||||||
|
<span>{props.message}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelClick}
|
||||||
|
style={closeButtonStyle}
|
||||||
|
aria-label="Close notification"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Example Usage ---
|
||||||
|
/*
|
||||||
|
import { toast } from 'solid-toast';
|
||||||
|
import {
|
||||||
|
ErrorToastComponent,
|
||||||
|
InfoToastComponent,
|
||||||
|
WarningToastComponent
|
||||||
|
} from './your-toast-components-file'; // Adjust path as necessary
|
||||||
|
|
||||||
|
const logCancel = (type: string) => console.log(`${type} toast cancelled by user.`);
|
||||||
|
|
||||||
|
// Function to show an error toast
|
||||||
|
export const showErrorToast = (message: string) => {
|
||||||
|
toast.custom((t) => (
|
||||||
|
<ErrorToastComponent
|
||||||
|
t={t}
|
||||||
|
message={message}
|
||||||
|
onCancel={() => logCancel('Error')}
|
||||||
|
/>
|
||||||
|
), { duration: Infinity }); // Use Infinity duration if you want it to only close on X click
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to show an info toast
|
||||||
|
export const showInfoToast = (message: string) => {
|
||||||
|
toast.custom((t) => (
|
||||||
|
<InfoToastComponent
|
||||||
|
t={t}
|
||||||
|
message={message}
|
||||||
|
// onCancel not provided, so only dismisses
|
||||||
|
/>
|
||||||
|
), { duration: 6000 }); // Or some default duration
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to show a warning toast
|
||||||
|
export const showWarningToast = (message: string) => {
|
||||||
|
toast.custom((t) => (
|
||||||
|
<WarningToastComponent
|
||||||
|
t={t}
|
||||||
|
message={message}
|
||||||
|
onCancel={() => alert('Warning toast was cancelled!')}
|
||||||
|
/>
|
||||||
|
), { duration: Infinity });
|
||||||
|
};
|
||||||
|
|
||||||
|
// How to use them:
|
||||||
|
// showErrorToast("Target IP must be provided.");
|
||||||
|
// showInfoToast("Your profile has been updated successfully.");
|
||||||
|
// showWarningToast("Your session is about to expire in 5 minutes.");
|
||||||
|
*/
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { callApi } from "@/src/api";
|
import { callApi } from "@/src/api";
|
||||||
import { Button } from "@/src/components/button";
|
import { Button } from "@/src/components/button";
|
||||||
import { FileInput } from "@/src/components/FileInput";
|
// Icon is used in CustomFileField, ensure it's available or remove if not needed there
|
||||||
import Icon from "@/src/components/icon";
|
import Icon from "@/src/components/icon";
|
||||||
|
|
||||||
import { Typography } from "@/src/components/Typography";
|
import { Typography } from "@/src/components/Typography";
|
||||||
import { Header } from "@/src/layout/header";
|
import { Header } from "@/src/layout/header";
|
||||||
|
|
||||||
import { SelectInput } from "@/src/Form/fields/Select";
|
import { SelectInput } from "@/src/Form/fields/Select";
|
||||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||||
import {
|
import {
|
||||||
@@ -17,20 +15,26 @@ import {
|
|||||||
getValues,
|
getValues,
|
||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { createEffect, createSignal, For, Show } from "solid-js";
|
import { createEffect, createSignal, For, Show } from "solid-js"; // For, Show might not be needed directly here now
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { FieldLayout } from "@/src/Form/fields/layout";
|
import { FieldLayout } from "@/src/Form/fields/layout";
|
||||||
import { InputLabel } from "@/src/components/inputBase";
|
import { InputLabel } from "@/src/components/inputBase";
|
||||||
import { Modal } from "@/src/components/modal";
|
import { Modal } from "@/src/components/modal";
|
||||||
import Fieldset from "@/src/Form/fieldset";
|
import Fieldset from "@/src/Form/fieldset"; // Still used for other fieldsets
|
||||||
import Accordion from "@/src/components/accordion";
|
import Accordion from "@/src/components/accordion";
|
||||||
|
|
||||||
|
// Import the new generic component
|
||||||
|
import {
|
||||||
|
FileSelectorField,
|
||||||
|
type FileDialogOptions,
|
||||||
|
} from "@/src/components/fileSelect"; // Adjust path
|
||||||
|
|
||||||
interface Wifi extends FieldValues {
|
interface Wifi extends FieldValues {
|
||||||
ssid: string;
|
ssid: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlashFormValues extends FieldValues {
|
export interface FlashFormValues extends FieldValues {
|
||||||
machine: {
|
machine: {
|
||||||
devicePath: string;
|
devicePath: string;
|
||||||
flake: string;
|
flake: string;
|
||||||
@@ -39,7 +43,7 @@ interface FlashFormValues extends FieldValues {
|
|||||||
language: string;
|
language: string;
|
||||||
keymap: string;
|
keymap: string;
|
||||||
wifi: Wifi[];
|
wifi: Wifi[];
|
||||||
sshKeys: File[];
|
sshKeys: File[]; // This field will use CustomFileField
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Flash = () => {
|
export const Flash = () => {
|
||||||
@@ -51,15 +55,15 @@ export const Flash = () => {
|
|||||||
},
|
},
|
||||||
language: "en_US.UTF-8",
|
language: "en_US.UTF-8",
|
||||||
keymap: "en",
|
keymap: "en",
|
||||||
|
// sshKeys: [] // Initial value for sshKeys (optional, modular-forms handles undefined)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ==== WIFI NETWORK ==== */
|
/* ==== WIFI NETWORK (logic remains the same) ==== */
|
||||||
const [wifiNetworks, setWifiNetworks] = createSignal<Wifi[]>([]);
|
const [wifiNetworks, setWifiNetworks] = createSignal<Wifi[]>([]);
|
||||||
const [passwordVisibility, setPasswordVisibility] = createSignal<boolean[]>(
|
const [passwordVisibility, setPasswordVisibility] = createSignal<boolean[]>(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const formWifi = getValue(formStore, "wifi");
|
const formWifi = getValue(formStore, "wifi");
|
||||||
if (formWifi !== undefined) {
|
if (formWifi !== undefined) {
|
||||||
@@ -67,7 +71,6 @@ export const Flash = () => {
|
|||||||
setPasswordVisibility(new Array(formWifi.length).fill(false));
|
setPasswordVisibility(new Array(formWifi.length).fill(false));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const addWifiNetwork = () => {
|
const addWifiNetwork = () => {
|
||||||
setWifiNetworks((c) => {
|
setWifiNetworks((c) => {
|
||||||
const res = [...c, { ssid: "", password: "" }];
|
const res = [...c, { ssid: "", password: "" }];
|
||||||
@@ -76,7 +79,6 @@ export const Flash = () => {
|
|||||||
});
|
});
|
||||||
setPasswordVisibility((c) => [...c, false]);
|
setPasswordVisibility((c) => [...c, false]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeWifiNetwork = (index: number) => {
|
const removeWifiNetwork = (index: number) => {
|
||||||
const updatedNetworks = wifiNetworks().filter((_, i) => i !== index);
|
const updatedNetworks = wifiNetworks().filter((_, i) => i !== index);
|
||||||
setWifiNetworks(updatedNetworks);
|
setWifiNetworks(updatedNetworks);
|
||||||
@@ -86,7 +88,6 @@ export const Flash = () => {
|
|||||||
setPasswordVisibility(updatedVisibility);
|
setPasswordVisibility(updatedVisibility);
|
||||||
setValue(formStore, "wifi", updatedNetworks);
|
setValue(formStore, "wifi", updatedNetworks);
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePasswordVisibility = (index: number) => {
|
const togglePasswordVisibility = (index: number) => {
|
||||||
const updatedVisibility = [...passwordVisibility()];
|
const updatedVisibility = [...passwordVisibility()];
|
||||||
updatedVisibility[index] = !updatedVisibility[index];
|
updatedVisibility[index] = !updatedVisibility[index];
|
||||||
@@ -123,40 +124,33 @@ export const Flash = () => {
|
|||||||
},
|
},
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
}));
|
}));
|
||||||
|
// Define the options for the SSH key file dialog
|
||||||
/**
|
const sshKeyDialogOptions: FileDialogOptions = {
|
||||||
* Opens the custom file dialog
|
title: "Select SSH Public Key(s)",
|
||||||
* Returns a native FileList to allow interaction with the native input type="file"
|
filters: { patterns: ["*.pub"] },
|
||||||
*/
|
initial_folder: "~/.ssh",
|
||||||
const selectSshKeys = async (): Promise<FileList> => {
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
|
|
||||||
const response = await callApi("open_file", {
|
|
||||||
file_request: {
|
|
||||||
title: "Select SSH Key",
|
|
||||||
mode: "open_multiple_files",
|
|
||||||
filters: { patterns: ["*.pub"] },
|
|
||||||
initial_folder: "~/.ssh",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (response.status === "success" && response.data) {
|
|
||||||
// Add synthetic files to the DataTransfer object
|
|
||||||
// FileList cannot be instantiated directly.
|
|
||||||
response.data.forEach((filename) => {
|
|
||||||
dataTransfer.items.add(new File([], filename));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return dataTransfer.files;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||||
const [isFlashing, setFlashing] = createSignal(false);
|
const [isFlashing, setFlashing] = createSignal(false);
|
||||||
|
|
||||||
const handleSubmit = (values: FlashFormValues) => {
|
const handleSubmit = (values: FlashFormValues) => {
|
||||||
|
// Basic check for sshKeys, could add to modular-forms validation
|
||||||
|
if (!values.sshKeys || values.sshKeys.length === 0) {
|
||||||
|
toast.error("Please select at least one SSH key.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setConfirmOpen(true);
|
setConfirmOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
// Wait for the flash to complete
|
|
||||||
const values = getValues(formStore) as FlashFormValues;
|
const values = getValues(formStore) as FlashFormValues;
|
||||||
|
// Additional check, though handleSubmit should catch it
|
||||||
|
if (!values.sshKeys || values.sshKeys.length === 0) {
|
||||||
|
toast.error("SSH keys are missing. Cannot proceed with flash.");
|
||||||
|
setConfirmOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFlashing(true);
|
setFlashing(true);
|
||||||
console.log("Confirmed flash:", values);
|
console.log("Confirmed flash:", values);
|
||||||
try {
|
try {
|
||||||
@@ -173,6 +167,7 @@ export const Flash = () => {
|
|||||||
system_config: {
|
system_config: {
|
||||||
language: values.language,
|
language: values.language,
|
||||||
keymap: values.keymap,
|
keymap: values.keymap,
|
||||||
|
// Ensure sshKeys is correctly mapped (File[] to string[])
|
||||||
ssh_keys_path: values.sshKeys.map((file) => file.name),
|
ssh_keys_path: values.sshKeys.map((file) => file.name),
|
||||||
},
|
},
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
@@ -202,6 +197,7 @@ export const Flash = () => {
|
|||||||
handleClose={() => !isFlashing() && setConfirmOpen(false)}
|
handleClose={() => !isFlashing() && setConfirmOpen(false)}
|
||||||
title="Confirm"
|
title="Confirm"
|
||||||
>
|
>
|
||||||
|
{/* ... Modal content as before ... */}
|
||||||
<div class="flex flex-col gap-4 p-4">
|
<div class="flex flex-col gap-4 p-4">
|
||||||
<div class="flex flex-col justify-between rounded-sm border p-4 align-middle text-red-900 border-def-2">
|
<div class="flex flex-col justify-between rounded-sm border p-4 align-middle text-red-900 border-def-2">
|
||||||
<Typography
|
<Typography
|
||||||
@@ -236,54 +232,22 @@ export const Flash = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div class="w-full self-stretch p-8">
|
<div class="w-full self-stretch p-8">
|
||||||
{/* <Typography tag="p" hierarchy="body" size="default" color="primary">
|
|
||||||
USB Utility image.
|
|
||||||
</Typography>
|
|
||||||
<Typography tag="p" hierarchy="body" size="default" color="secondary">
|
|
||||||
Will make bootstrapping new machines easier by providing secure remote
|
|
||||||
connection to any machine when plugged in.
|
|
||||||
</Typography> */}
|
|
||||||
<Form
|
<Form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
class="mx-auto flex w-full max-w-2xl flex-col gap-y-6"
|
class="mx-auto flex w-full max-w-2xl flex-col gap-y-6"
|
||||||
>
|
>
|
||||||
<Fieldset legend="Authorized SSH Keys">
|
<FileSelectorField
|
||||||
<Typography hierarchy="body" size="s" weight="medium">
|
Field={Field}
|
||||||
Provide your SSH public key. For secure and passwordless SSH
|
name="sshKeys" // Corresponds to FlashFormValues.sshKeys
|
||||||
connections.
|
label="Authorized SSH Keys"
|
||||||
</Typography>
|
description="Provide your SSH public key(s) for secure, passwordless connections. (.pub files)"
|
||||||
<Field name="sshKeys" type="File[]">
|
multiple={true} // Allow multiple SSH keys
|
||||||
{(field, props) => (
|
fileDialogOptions={sshKeyDialogOptions}
|
||||||
<>
|
// You could add custom validation via modular-forms 'validate' prop on CustomFileField if needed
|
||||||
<FileInput
|
// e.g. validate={[required("At least one SSH key is required.")]}
|
||||||
{...props}
|
// This would require CustomFileField to accept and pass `validate` to its internal `Field`.
|
||||||
onClick={async (event) => {
|
/>
|
||||||
event.preventDefault(); // Prevent the native file dialog from opening
|
|
||||||
const input = event.target;
|
|
||||||
const files = await selectSshKeys();
|
|
||||||
|
|
||||||
// Set the files
|
|
||||||
Object.defineProperty(input, "files", {
|
|
||||||
value: files,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
// Define the files property on the input element
|
|
||||||
const changeEvent = new Event("input", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
input.dispatchEvent(changeEvent);
|
|
||||||
}}
|
|
||||||
value={field.value}
|
|
||||||
error={field.error}
|
|
||||||
//helperText="Provide your SSH public key. For secure and passwordless SSH connections."
|
|
||||||
//label="Authorized SSH Keys"
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</Fieldset>
|
|
||||||
<Fieldset legend="General">
|
<Fieldset legend="General">
|
||||||
<Field name="disk" validate={[required("This field is required")]}>
|
<Field name="disk" validate={[required("This field is required")]}>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
@@ -322,6 +286,7 @@ export const Flash = () => {
|
|||||||
</Fieldset>
|
</Fieldset>
|
||||||
|
|
||||||
<Fieldset legend="Network Settings">
|
<Fieldset legend="Network Settings">
|
||||||
|
{/* ... Network settings as before ... */}
|
||||||
<FieldLayout
|
<FieldLayout
|
||||||
label={<InputLabel>Networks</InputLabel>}
|
label={<InputLabel>Networks</InputLabel>}
|
||||||
field={
|
field={
|
||||||
@@ -338,6 +303,7 @@ export const Flash = () => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{/* TODO: You would render the actual WiFi input fields here using a <For> loop over wifiNetworks() signal */}
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
|
|
||||||
<Accordion title="Advanced">
|
<Accordion title="Advanced">
|
||||||
@@ -445,20 +411,23 @@ export const Flash = () => {
|
|||||||
</Field>
|
</Field>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<div class="mt-2 flex justify-end pt-2">
|
<div class="mt-2 flex justify-end pt-2">
|
||||||
<Button
|
<Button
|
||||||
class="self-end"
|
class="self-end"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={formStore.submitting}
|
disabled={formStore.submitting || isFlashing()}
|
||||||
startIcon={
|
startIcon={
|
||||||
formStore.submitting ? (
|
formStore.submitting || isFlashing() ? (
|
||||||
<Icon icon="Load" />
|
<Icon icon="Load" />
|
||||||
) : (
|
) : (
|
||||||
<Icon icon="Flash" />
|
<Icon icon="Flash" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{formStore.submitting ? "Flashing..." : "Flash Installer"}
|
{formStore.submitting || isFlashing()
|
||||||
|
? "Flashing..."
|
||||||
|
: "Flash Installer"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ import { SummaryStep } from "./install/summary-step";
|
|||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { VarsStep, VarsValues } from "./install/vars-step";
|
import { VarsStep, VarsValues } from "./install/vars-step";
|
||||||
import Fieldset from "@/src/Form/fieldset";
|
import Fieldset from "@/src/Form/fieldset";
|
||||||
|
import {
|
||||||
|
FileSelectorField,
|
||||||
|
type FileDialogOptions,
|
||||||
|
} from "@/src/components/fileSelect";
|
||||||
type MachineFormInterface = MachineData & {
|
type MachineFormInterface = MachineData & {
|
||||||
sshKey?: File;
|
sshKey?: File;
|
||||||
disk?: string;
|
disk?: string;
|
||||||
@@ -50,6 +53,7 @@ export interface AllStepsValues extends FieldValues {
|
|||||||
"2": DiskValues;
|
"2": DiskValues;
|
||||||
"3": VarsValues;
|
"3": VarsValues;
|
||||||
"4": NonNullable<unknown>;
|
"4": NonNullable<unknown>;
|
||||||
|
sshKey?: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoadingBar = () => (
|
const LoadingBar = () => (
|
||||||
@@ -104,9 +108,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading_toast = toast.loading(
|
|
||||||
"Installing machine. Grab coffee (15min)...",
|
|
||||||
);
|
|
||||||
setIsInstalling(true);
|
setIsInstalling(true);
|
||||||
|
|
||||||
// props.machine.disk_
|
// props.machine.disk_
|
||||||
@@ -125,16 +126,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
schema_name: diskValues.schema,
|
schema_name: diskValues.schema,
|
||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (disk_response.status === "error") {
|
|
||||||
toast.error(
|
|
||||||
`Failed to set disk schema: ${disk_response.errors[0].message}`,
|
|
||||||
);
|
|
||||||
setProgressText(
|
|
||||||
"Failed to set disk schema. \n" + disk_response.errors[0].message,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgressText("Installing machine ... (2/5)");
|
setProgressText("Installing machine ... (2/5)");
|
||||||
@@ -147,6 +138,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
identifier: curr_uri,
|
identifier: curr_uri,
|
||||||
},
|
},
|
||||||
override_target_host: target,
|
override_target_host: target,
|
||||||
|
private_key: values.sshKey?.name,
|
||||||
},
|
},
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
@@ -164,24 +156,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
await sleep(10 * 1000);
|
await sleep(10 * 1000);
|
||||||
|
|
||||||
const installResponse = await installPromise;
|
const installResponse = await installPromise;
|
||||||
|
|
||||||
toast.dismiss(loading_toast);
|
|
||||||
|
|
||||||
if (installResponse.status === "error") {
|
|
||||||
toast.error("Failed to install machine");
|
|
||||||
setIsDone(true);
|
|
||||||
setProgressText(
|
|
||||||
"Failed to install machine. \n" + installResponse.errors[0].message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (installResponse.status === "success") {
|
|
||||||
toast.success("Machine installed successfully");
|
|
||||||
setIsDone(true);
|
|
||||||
setProgressText(
|
|
||||||
"Machine installed successfully. Please unplug the usb stick and reboot the system.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [step, setStep] = createSignal<StepIdx>("1");
|
const [step, setStep] = createSignal<StepIdx>("1");
|
||||||
@@ -431,14 +405,6 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (machine_response.status === "error") {
|
|
||||||
toast.error(
|
|
||||||
`Failed to set machine: ${machine_response.errors[0].message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (machine_response.status === "success") {
|
|
||||||
toast.success("Machine set successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -461,9 +427,8 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const handleUpdateButton = async () => {
|
const handleUpdateButton = async () => {
|
||||||
const t = toast.loading("Checking for generators...");
|
|
||||||
await generatorsQuery.refetch();
|
await generatorsQuery.refetch();
|
||||||
toast.dismiss(t);
|
|
||||||
if (generatorsQuery.data?.length !== 0) {
|
if (generatorsQuery.data?.length !== 0) {
|
||||||
navigate(`/machines/${machineName()}/vars`);
|
navigate(`/machines/${machineName()}/vars`);
|
||||||
} else {
|
} else {
|
||||||
@@ -489,7 +454,6 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
|
|
||||||
const target = targetHost();
|
const target = targetHost();
|
||||||
|
|
||||||
const loading_toast = toast.loading("Updating machine...");
|
|
||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
const r = await callApi("update_machines", {
|
const r = await callApi("update_machines", {
|
||||||
base_path: curr_uri,
|
base_path: curr_uri,
|
||||||
@@ -502,15 +466,6 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
setIsUpdating(false);
|
|
||||||
toast.dismiss(loading_toast);
|
|
||||||
|
|
||||||
if (r.status === "error") {
|
|
||||||
toast.error("Failed to update machine");
|
|
||||||
}
|
|
||||||
if (r.status === "success") {
|
|
||||||
toast.success("Machine updated successfully");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -666,6 +621,22 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
<FileSelectorField
|
||||||
|
Field={Field}
|
||||||
|
name="sshKeys" // Corresponds to FlashFormValues.sshKeys
|
||||||
|
label="SSH Private Key"
|
||||||
|
description="Provide your SSH private key for secure, passwordless connections."
|
||||||
|
multiple={false}
|
||||||
|
fileDialogOptions={
|
||||||
|
{
|
||||||
|
title: "Select SSH Keys",
|
||||||
|
initial_folder: "~/.ssh",
|
||||||
|
} as FileDialogOptions
|
||||||
|
}
|
||||||
|
// You could add custom validation via modular-forms 'validate' prop on CustomFileField if needed
|
||||||
|
// e.g. validate={[required("At least one SSH key is required.")]}
|
||||||
|
// This would require CustomFileField to accept and pass `validate` to its internal `Field`.
|
||||||
|
/>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,19 @@ import {
|
|||||||
setValue,
|
setValue,
|
||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
import { createEffect, createSignal, JSX, Match, Switch } from "solid-js";
|
import { createEffect, createSignal, JSX, Match, Switch } from "solid-js";
|
||||||
import toast from "solid-toast";
|
|
||||||
import { TextInput } from "@/src/Form/fields";
|
import { TextInput } from "@/src/Form/fields";
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { Badge } from "@/src/components/badge";
|
import { Badge } from "@/src/components/badge";
|
||||||
import { Group } from "@/src/components/group";
|
import { Group } from "@/src/components/group";
|
||||||
|
import {
|
||||||
|
FileSelectorField,
|
||||||
|
type FileDialogOptions,
|
||||||
|
} from "@/src/components/fileSelect";
|
||||||
|
|
||||||
export type HardwareValues = FieldValues & {
|
export type HardwareValues = FieldValues & {
|
||||||
report: boolean;
|
report: boolean;
|
||||||
target: string;
|
target: string;
|
||||||
|
sshKey?: File;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StepProps<T> {
|
export interface StepProps<T> {
|
||||||
@@ -75,21 +79,21 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
|||||||
const curr_uri = activeURI();
|
const curr_uri = activeURI();
|
||||||
if (!curr_uri) return;
|
if (!curr_uri) return;
|
||||||
|
|
||||||
const loading_toast = toast.loading("Generating hardware report...");
|
|
||||||
|
|
||||||
await validate(formStore, "target");
|
await validate(formStore, "target");
|
||||||
const target = getValue(formStore, "target");
|
const target = getValue(formStore, "target");
|
||||||
|
const sshFile = getValue(formStore, "sshKey") as File | undefined;
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
toast.error("Target ip must be provided");
|
console.error("Target is not set");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsGenerating(true);
|
|
||||||
const r = await callApi("generate_machine_hardware_info", {
|
const r = await callApi("generate_machine_hardware_info", {
|
||||||
opts: {
|
opts: {
|
||||||
machine: {
|
machine: {
|
||||||
name: props.machine_id,
|
name: props.machine_id,
|
||||||
override_target_host: target,
|
override_target_host: target,
|
||||||
|
private_key: sshFile?.name,
|
||||||
flake: {
|
flake: {
|
||||||
identifier: curr_uri,
|
identifier: curr_uri,
|
||||||
},
|
},
|
||||||
@@ -97,16 +101,9 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
|||||||
backend: "nixos-facter",
|
backend: "nixos-facter",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setIsGenerating(false);
|
|
||||||
toast.dismiss(loading_toast);
|
|
||||||
// TODO: refresh the machine details
|
// TODO: refresh the machine details
|
||||||
|
|
||||||
if (r.status === "error") {
|
|
||||||
toast.error(`Failed to generate report. ${r.errors[0].message}`);
|
|
||||||
}
|
|
||||||
if (r.status === "success") {
|
|
||||||
toast.success("Report generated successfully");
|
|
||||||
}
|
|
||||||
hwReportQuery.refetch();
|
hwReportQuery.refetch();
|
||||||
submit(formStore);
|
submit(formStore);
|
||||||
};
|
};
|
||||||
@@ -128,6 +125,22 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
<FileSelectorField
|
||||||
|
Field={Field}
|
||||||
|
name="sshKey" // Corresponds to FlashFormValues.sshKeys
|
||||||
|
label="SSH Private Key"
|
||||||
|
description="Provide your SSH private key for secure, passwordless connections."
|
||||||
|
multiple={false}
|
||||||
|
fileDialogOptions={
|
||||||
|
{
|
||||||
|
title: "Select SSH Keys",
|
||||||
|
initial_folder: "~/.ssh",
|
||||||
|
} as FileDialogOptions
|
||||||
|
}
|
||||||
|
// You could add custom validation via modular-forms 'validate' prop on CustomFileField if needed
|
||||||
|
// e.g. validate={[required("At least one SSH key is required.")]}
|
||||||
|
// This would require CustomFileField to accept and pass `validate` to its internal `Field`.
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Group>
|
<Group>
|
||||||
<Field
|
<Field
|
||||||
|
|||||||
Reference in New Issue
Block a user