clan-app: Add cancellable tasks
This commit is contained in:
@@ -20,6 +20,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../webview-lib"
|
||||
},
|
||||
{
|
||||
"path": "../clan-cli/clan_lib"
|
||||
}
|
||||
],
|
||||
"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_lib.api import API
|
||||
|
||||
from clan_app.api.cancel import cancel_task
|
||||
from clan_app.api.file_gtk import open_file
|
||||
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)
|
||||
|
||||
API.overwrite_fn(open_file)
|
||||
# breakpoint()
|
||||
API.overwrite_fn(cancel_task)
|
||||
webview.bind_jsonschema_api(API)
|
||||
webview.size = Size(1280, 1024, SizeHint.NONE)
|
||||
webview.navigate(content_uri)
|
||||
|
||||
@@ -113,6 +113,7 @@ class Webview:
|
||||
reconciled_arguments["op_key"] = seq.decode()
|
||||
# TODO: We could remove the wrapper in the MethodRegistry
|
||||
# and just call the method directly
|
||||
|
||||
result = wrap_method(**reconciled_arguments)
|
||||
|
||||
serialized = json.dumps(
|
||||
|
||||
@@ -32,6 +32,11 @@ class FileRequest:
|
||||
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
|
||||
def open_file(file_request: FileRequest) -> list[str] | None:
|
||||
"""
|
||||
|
||||
@@ -7,10 +7,15 @@ pkgs.clangStdenv.mkDerivation {
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "webview";
|
||||
repo = "webview";
|
||||
rev = "83a4b4a5bbcb4b0ba2ca3ee226c2da1414719106";
|
||||
sha256 = "sha256-5R8kllvP2EBuDANIl07fxv/EcbPpYgeav8Wfz7Kt13c=";
|
||||
rev = "f1a9d6b6fb8bcc2e266057224887a3d628f30f90";
|
||||
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 = [
|
||||
"out"
|
||||
"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",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||
"@floating-ui/dom": "^1.6.8",
|
||||
"@modular-forms/solid": "^0.21.0",
|
||||
"@solid-primitives/storage": "^3.7.1",
|
||||
@@ -63,7 +64,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -91,7 +91,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
@@ -106,7 +105,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz",
|
||||
"integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -116,7 +114,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
@@ -147,7 +144,6 @@
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -157,7 +153,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz",
|
||||
"integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.27.1",
|
||||
@@ -174,7 +169,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz",
|
||||
"integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.1",
|
||||
@@ -191,7 +185,6 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
@@ -201,7 +194,6 @@
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -211,7 +203,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
@@ -225,7 +216,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
|
||||
"integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
@@ -243,7 +233,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -253,7 +242,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -263,7 +251,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -273,7 +260,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -283,7 +269,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz",
|
||||
"integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.1",
|
||||
@@ -297,7 +282,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz",
|
||||
"integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.27.1"
|
||||
@@ -309,6 +293,21 @@
|
||||
"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": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
|
||||
@@ -345,7 +344,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz",
|
||||
"integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -360,7 +358,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz",
|
||||
"integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -379,7 +376,6 @@
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -389,7 +385,6 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz",
|
||||
"integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
@@ -1257,7 +1252,6 @@
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
@@ -1272,7 +1266,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -1282,7 +1275,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -1292,14 +1284,12 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -2840,7 +2830,6 @@
|
||||
"version": "4.24.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
||||
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -2916,7 +2905,6 @@
|
||||
"version": "1.0.30001717",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz",
|
||||
"integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -3104,7 +3092,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/corvu": {
|
||||
@@ -3506,7 +3493,6 @@
|
||||
"version": "1.5.150",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.150.tgz",
|
||||
"integrity": "sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@@ -3620,7 +3606,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -4065,7 +4050,6 @@
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -4581,7 +4565,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
@@ -4641,7 +4624,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
@@ -4672,7 +4654,6 @@
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
@@ -5577,7 +5558,6 @@
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
@@ -7424,7 +7404,6 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -7993,7 +7972,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-syntax-import-attributes": "^7.27.1",
|
||||
"@floating-ui/dom": "^1.6.8",
|
||||
"@modular-forms/solid": "^0.21.0",
|
||||
"@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 { createMemo, JSX, Show, splitProps } from "solid-js";
|
||||
|
||||
interface FileInputProps {
|
||||
export interface FileInputProps {
|
||||
ref: (element: HTMLInputElement) => void;
|
||||
name: string;
|
||||
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 { 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 { Typography } from "@/src/components/Typography";
|
||||
import { Header } from "@/src/layout/header";
|
||||
|
||||
import { SelectInput } from "@/src/Form/fields/Select";
|
||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||
import {
|
||||
@@ -17,20 +15,26 @@ import {
|
||||
getValues,
|
||||
} from "@modular-forms/solid";
|
||||
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 { FieldLayout } from "@/src/Form/fields/layout";
|
||||
import { InputLabel } from "@/src/components/inputBase";
|
||||
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 the new generic component
|
||||
import {
|
||||
FileSelectorField,
|
||||
type FileDialogOptions,
|
||||
} from "@/src/components/fileSelect"; // Adjust path
|
||||
|
||||
interface Wifi extends FieldValues {
|
||||
ssid: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface FlashFormValues extends FieldValues {
|
||||
export interface FlashFormValues extends FieldValues {
|
||||
machine: {
|
||||
devicePath: string;
|
||||
flake: string;
|
||||
@@ -39,7 +43,7 @@ interface FlashFormValues extends FieldValues {
|
||||
language: string;
|
||||
keymap: string;
|
||||
wifi: Wifi[];
|
||||
sshKeys: File[];
|
||||
sshKeys: File[]; // This field will use CustomFileField
|
||||
}
|
||||
|
||||
export const Flash = () => {
|
||||
@@ -51,15 +55,15 @@ export const Flash = () => {
|
||||
},
|
||||
language: "en_US.UTF-8",
|
||||
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 [passwordVisibility, setPasswordVisibility] = createSignal<boolean[]>(
|
||||
[],
|
||||
);
|
||||
|
||||
createEffect(() => {
|
||||
const formWifi = getValue(formStore, "wifi");
|
||||
if (formWifi !== undefined) {
|
||||
@@ -67,7 +71,6 @@ export const Flash = () => {
|
||||
setPasswordVisibility(new Array(formWifi.length).fill(false));
|
||||
}
|
||||
});
|
||||
|
||||
const addWifiNetwork = () => {
|
||||
setWifiNetworks((c) => {
|
||||
const res = [...c, { ssid: "", password: "" }];
|
||||
@@ -76,7 +79,6 @@ export const Flash = () => {
|
||||
});
|
||||
setPasswordVisibility((c) => [...c, false]);
|
||||
};
|
||||
|
||||
const removeWifiNetwork = (index: number) => {
|
||||
const updatedNetworks = wifiNetworks().filter((_, i) => i !== index);
|
||||
setWifiNetworks(updatedNetworks);
|
||||
@@ -86,7 +88,6 @@ export const Flash = () => {
|
||||
setPasswordVisibility(updatedVisibility);
|
||||
setValue(formStore, "wifi", updatedNetworks);
|
||||
};
|
||||
|
||||
const togglePasswordVisibility = (index: number) => {
|
||||
const updatedVisibility = [...passwordVisibility()];
|
||||
updatedVisibility[index] = !updatedVisibility[index];
|
||||
@@ -123,40 +124,33 @@ export const Flash = () => {
|
||||
},
|
||||
staleTime: Infinity,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Opens the custom file dialog
|
||||
* Returns a native FileList to allow interaction with the native input type="file"
|
||||
*/
|
||||
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",
|
||||
// Define the options for the SSH key file dialog
|
||||
const sshKeyDialogOptions: FileDialogOptions = {
|
||||
title: "Select SSH Public Key(s)",
|
||||
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 [isFlashing, setFlashing] = createSignal(false);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
// Wait for the flash to complete
|
||||
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);
|
||||
console.log("Confirmed flash:", values);
|
||||
try {
|
||||
@@ -173,6 +167,7 @@ export const Flash = () => {
|
||||
system_config: {
|
||||
language: values.language,
|
||||
keymap: values.keymap,
|
||||
// Ensure sshKeys is correctly mapped (File[] to string[])
|
||||
ssh_keys_path: values.sshKeys.map((file) => file.name),
|
||||
},
|
||||
dry_run: false,
|
||||
@@ -202,6 +197,7 @@ export const Flash = () => {
|
||||
handleClose={() => !isFlashing() && setConfirmOpen(false)}
|
||||
title="Confirm"
|
||||
>
|
||||
{/* ... Modal content as before ... */}
|
||||
<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">
|
||||
<Typography
|
||||
@@ -236,54 +232,22 @@ export const Flash = () => {
|
||||
</div>
|
||||
</Modal>
|
||||
<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
|
||||
onSubmit={handleSubmit}
|
||||
class="mx-auto flex w-full max-w-2xl flex-col gap-y-6"
|
||||
>
|
||||
<Fieldset legend="Authorized SSH Keys">
|
||||
<Typography hierarchy="body" size="s" weight="medium">
|
||||
Provide your SSH public key. For secure and passwordless SSH
|
||||
connections.
|
||||
</Typography>
|
||||
<Field name="sshKeys" type="File[]">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<FileInput
|
||||
{...props}
|
||||
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
|
||||
<FileSelectorField
|
||||
Field={Field}
|
||||
name="sshKeys" // Corresponds to FlashFormValues.sshKeys
|
||||
label="Authorized SSH Keys"
|
||||
description="Provide your SSH public key(s) for secure, passwordless connections. (.pub files)"
|
||||
multiple={true} // Allow multiple SSH keys
|
||||
fileDialogOptions={sshKeyDialogOptions}
|
||||
// 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`.
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset legend="General">
|
||||
<Field name="disk" validate={[required("This field is required")]}>
|
||||
{(field, props) => (
|
||||
@@ -322,6 +286,7 @@ export const Flash = () => {
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset legend="Network Settings">
|
||||
{/* ... Network settings as before ... */}
|
||||
<FieldLayout
|
||||
label={<InputLabel>Networks</InputLabel>}
|
||||
field={
|
||||
@@ -338,6 +303,7 @@ export const Flash = () => {
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{/* TODO: You would render the actual WiFi input fields here using a <For> loop over wifiNetworks() signal */}
|
||||
</Fieldset>
|
||||
|
||||
<Accordion title="Advanced">
|
||||
@@ -445,20 +411,23 @@ export const Flash = () => {
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</Accordion>
|
||||
|
||||
<div class="mt-2 flex justify-end pt-2">
|
||||
<Button
|
||||
class="self-end"
|
||||
type="submit"
|
||||
disabled={formStore.submitting}
|
||||
disabled={formStore.submitting || isFlashing()}
|
||||
startIcon={
|
||||
formStore.submitting ? (
|
||||
formStore.submitting || isFlashing() ? (
|
||||
<Icon icon="Load" />
|
||||
) : (
|
||||
<Icon icon="Flash" />
|
||||
)
|
||||
}
|
||||
>
|
||||
{formStore.submitting ? "Flashing..." : "Flash Installer"}
|
||||
{formStore.submitting || isFlashing()
|
||||
? "Flashing..."
|
||||
: "Flash Installer"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -28,7 +28,10 @@ import { SummaryStep } from "./install/summary-step";
|
||||
import cx from "classnames";
|
||||
import { VarsStep, VarsValues } from "./install/vars-step";
|
||||
import Fieldset from "@/src/Form/fieldset";
|
||||
|
||||
import {
|
||||
FileSelectorField,
|
||||
type FileDialogOptions,
|
||||
} from "@/src/components/fileSelect";
|
||||
type MachineFormInterface = MachineData & {
|
||||
sshKey?: File;
|
||||
disk?: string;
|
||||
@@ -50,6 +53,7 @@ export interface AllStepsValues extends FieldValues {
|
||||
"2": DiskValues;
|
||||
"3": VarsValues;
|
||||
"4": NonNullable<unknown>;
|
||||
sshKey?: File;
|
||||
}
|
||||
|
||||
const LoadingBar = () => (
|
||||
@@ -104,9 +108,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const loading_toast = toast.loading(
|
||||
"Installing machine. Grab coffee (15min)...",
|
||||
);
|
||||
setIsInstalling(true);
|
||||
|
||||
// props.machine.disk_
|
||||
@@ -125,16 +126,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
schema_name: diskValues.schema,
|
||||
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)");
|
||||
@@ -147,6 +138,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
identifier: curr_uri,
|
||||
},
|
||||
override_target_host: target,
|
||||
private_key: values.sshKey?.name,
|
||||
},
|
||||
password: "",
|
||||
},
|
||||
@@ -164,24 +156,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
await sleep(10 * 1000);
|
||||
|
||||
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");
|
||||
@@ -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;
|
||||
};
|
||||
@@ -461,9 +427,8 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
}));
|
||||
|
||||
const handleUpdateButton = async () => {
|
||||
const t = toast.loading("Checking for generators...");
|
||||
await generatorsQuery.refetch();
|
||||
toast.dismiss(t);
|
||||
|
||||
if (generatorsQuery.data?.length !== 0) {
|
||||
navigate(`/machines/${machineName()}/vars`);
|
||||
} else {
|
||||
@@ -489,7 +454,6 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
|
||||
const target = targetHost();
|
||||
|
||||
const loading_toast = toast.loading("Updating machine...");
|
||||
setIsUpdating(true);
|
||||
const r = await callApi("update_machines", {
|
||||
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(() => {
|
||||
@@ -666,6 +621,22 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -15,15 +15,19 @@ import {
|
||||
setValue,
|
||||
} from "@modular-forms/solid";
|
||||
import { createEffect, createSignal, JSX, Match, Switch } from "solid-js";
|
||||
import toast from "solid-toast";
|
||||
import { TextInput } from "@/src/Form/fields";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { Badge } from "@/src/components/badge";
|
||||
import { Group } from "@/src/components/group";
|
||||
import {
|
||||
FileSelectorField,
|
||||
type FileDialogOptions,
|
||||
} from "@/src/components/fileSelect";
|
||||
|
||||
export type HardwareValues = FieldValues & {
|
||||
report: boolean;
|
||||
target: string;
|
||||
sshKey?: File;
|
||||
};
|
||||
|
||||
export interface StepProps<T> {
|
||||
@@ -75,21 +79,21 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
||||
const curr_uri = activeURI();
|
||||
if (!curr_uri) return;
|
||||
|
||||
const loading_toast = toast.loading("Generating hardware report...");
|
||||
|
||||
await validate(formStore, "target");
|
||||
const target = getValue(formStore, "target");
|
||||
const sshFile = getValue(formStore, "sshKey") as File | undefined;
|
||||
|
||||
if (!target) {
|
||||
toast.error("Target ip must be provided");
|
||||
console.error("Target is not set");
|
||||
return;
|
||||
}
|
||||
setIsGenerating(true);
|
||||
|
||||
const r = await callApi("generate_machine_hardware_info", {
|
||||
opts: {
|
||||
machine: {
|
||||
name: props.machine_id,
|
||||
override_target_host: target,
|
||||
private_key: sshFile?.name,
|
||||
flake: {
|
||||
identifier: curr_uri,
|
||||
},
|
||||
@@ -97,16 +101,9 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
||||
backend: "nixos-facter",
|
||||
},
|
||||
});
|
||||
setIsGenerating(false);
|
||||
toast.dismiss(loading_toast);
|
||||
|
||||
// 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();
|
||||
submit(formStore);
|
||||
};
|
||||
@@ -128,6 +125,22 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
<Field
|
||||
|
||||
Reference in New Issue
Block a user