diff --git a/pkgs/clan-app/clan-app.code-workspace b/pkgs/clan-app/clan-app.code-workspace index 95f1b4ff7..2e3fbfd6a 100644 --- a/pkgs/clan-app/clan-app.code-workspace +++ b/pkgs/clan-app/clan-app.code-workspace @@ -20,6 +20,9 @@ }, { "path": "../webview-lib" + }, + { + "path": "../clan-cli/clan_lib" } ], "settings": { diff --git a/pkgs/clan-app/clan_app/api/cancel.py b/pkgs/clan-app/clan_app/api/cancel.py new file mode 100644 index 000000000..389717244 --- /dev/null +++ b/pkgs/clan-app/clan_app/api/cancel.py @@ -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", + ) diff --git a/pkgs/clan-app/clan_app/app.py b/pkgs/clan-app/clan_app/app.py index 836e6e294..9f1e55b87 100644 --- a/pkgs/clan-app/clan_app/app.py +++ b/pkgs/clan-app/clan_app/app.py @@ -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) diff --git a/pkgs/clan-app/clan_app/deps/webview/webview.py b/pkgs/clan-app/clan_app/deps/webview/webview.py index 4948f6754..3ef5a83f6 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview.py @@ -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( diff --git a/pkgs/clan-cli/clan_lib/api/directory.py b/pkgs/clan-cli/clan_lib/api/directory.py index 1e7cc38ac..a1b4b11ad 100644 --- a/pkgs/clan-cli/clan_lib/api/directory.py +++ b/pkgs/clan-cli/clan_lib/api/directory.py @@ -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: """ diff --git a/pkgs/webview-lib/default.nix b/pkgs/webview-lib/default.nix index 00ce6df2d..a891e7da4 100644 --- a/pkgs/webview-lib/default.nix +++ b/pkgs/webview-lib/default.nix @@ -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" diff --git a/pkgs/webview-lib/fixes.patch b/pkgs/webview-lib/fixes.patch new file mode 100644 index 000000000..01f8a5b9f --- /dev/null +++ b/pkgs/webview-lib/fixes.patch @@ -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\ diff --git a/pkgs/webview-ui/app/package-lock.json b/pkgs/webview-ui/app/package-lock.json index 2ce6db10b..b6be4d27a 100644 --- a/pkgs/webview-ui/app/package-lock.json +++ b/pkgs/webview-ui/app/package-lock.json @@ -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": { diff --git a/pkgs/webview-ui/app/package.json b/pkgs/webview-ui/app/package.json index 8150b49ab..0db479bd6 100644 --- a/pkgs/webview-ui/app/package.json +++ b/pkgs/webview-ui/app/package.json @@ -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", diff --git a/pkgs/webview-ui/app/src/api/index.ts b/pkgs/webview-ui/app/src/api/index.ts deleted file mode 100644 index c6a3ff28f..000000000 --- a/pkgs/webview-ui/app/src/api/index.ts +++ /dev/null @@ -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 = API[T]["arguments"]; -export type OperationResponse = API[T]["return"]; - -export type ApiEnvelope = - | { - status: "success"; - data: T; - op_key: string; - } - | Error; - -export type Services = NonNullable; -export type ServiceNames = keyof Services; -export type ClanService = Services[T]; -export type ClanServiceInstance = NonNullable< - Services[T] ->[string]; - -export type SuccessQuery = Extract< - OperationResponse, - { status: "success" } ->; -export type SuccessData = SuccessQuery["data"]; - -export type ErrorQuery = Extract< - OperationResponse, - { status: "error" } ->; -export type ErrorData = ErrorQuery["errors"]; - -export type ClanOperations = Record void>; - -export interface GtkResponse { - result: T; - op_key: string; -} - -export const callApi = async ( - method: K, - args: OperationArgs, -): Promise> => { - console.log("Calling API", method, args); - const response = await ( - window as unknown as Record< - OperationNames, - ( - args: OperationArgs, - ) => Promise> - > - )[method](args); - return response as OperationResponse; -}; diff --git a/pkgs/webview-ui/app/src/api/index.tsx b/pkgs/webview-ui/app/src/api/index.tsx new file mode 100644 index 000000000..edebba2ef --- /dev/null +++ b/pkgs/webview-ui/app/src/api/index.tsx @@ -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 = API[T]["arguments"]; +export type OperationResponse = API[T]["return"]; + +export type ApiEnvelope = + | { + status: "success"; + data: T; + op_key: string; + } + | Error; + +export type Services = NonNullable; +export type ServiceNames = keyof Services; +export type ClanService = Services[T]; +export type ClanServiceInstance = NonNullable< + Services[T] +>[string]; + +export type SuccessQuery = Extract< + OperationResponse, + { status: "success" } +>; +export type SuccessData = SuccessQuery["data"]; + +export type ErrorQuery = Extract< + OperationResponse, + { status: "error" } +>; +export type ErrorData = ErrorQuery["errors"]; + +export type ClanOperations = Record void>; + +export interface GtkResponse { + result: T; + op_key: string; +} +const _callApi = ( + method: K, + args: OperationArgs, +): { promise: Promise>; op_key: string } => { + const promise = ( + window as unknown as Record< + OperationNames, + ( + args: OperationArgs, + ) => Promise> + > + )[method](args) as Promise>; + 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) => ( + + ), + { + duration: 5000, + }, + ); + } else { + toast.custom( + (t) => ( + + ), + { + duration: 5000, + }, + ); + } + console.log("Cancel response: ", resp); +}; + +export const callApi = async ( + method: K, + args: OperationArgs, +): Promise> => { + 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 + ) => ( + + ), + { + duration: Infinity, + }, + ); + + const response = await promise; + if (response.status === "error") { + toast.remove(toastId); + toast.error( +
+ {response.errors.map((err) => ( +

{err.message}

+ ))} +
, + { + duration: 5000, + }, + ); + } else { + toast.remove(toastId); + } + return response as OperationResponse; +}; diff --git a/pkgs/webview-ui/app/src/components/FileInput.tsx b/pkgs/webview-ui/app/src/components/FileInput.tsx index 7f152d3da..8d4de1d61 100644 --- a/pkgs/webview-ui/app/src/components/FileInput.tsx +++ b/pkgs/webview-ui/app/src/components/FileInput.tsx @@ -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; diff --git a/pkgs/webview-ui/app/src/components/fileSelect/index.tsx b/pkgs/webview-ui/app/src/components/fileSelect/index.tsx new file mode 100644 index 000000000..5ee02604f --- /dev/null +++ b/pkgs/webview-ui/app/src/components/fileSelect/index.tsx @@ -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, +> { + Field: FieldComponent; // 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> = ( + 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 ( +
+ {description && + (typeof description === "string" ? ( + + {description} + + ) : ( + description + ))} + + + {(field, fieldProps) => ( + <> + {/* + This FileInput component should be clickable. + Its 'ref' needs to point to the actual element. + If FileInput is complex, it might need an 'inputRef' prop or similar. + */} + { + (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 && ( + + {field.error} + + )} + + {/* Display the list of selected files */} + 0 + : field.value instanceof File) + } + > +
+ + {(file) => ( +
+ + + {file.name} + + {/* 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. */} +
+ )} +
+
+
+ + )} +
+
+ ); +}; diff --git a/pkgs/webview-ui/app/src/components/toast/index.tsx b/pkgs/webview-ui/app/src/components/toast/index.tsx new file mode 100644 index 000000000..2c8177a20 --- /dev/null +++ b/pkgs/webview-ui/app/src/components/toast/index.tsx @@ -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 = () => ( + + + + + +); + +const InfoIcon: Component = () => ( + + + + + +); + +const WarningIcon: Component = () => ( + + + + + +); + +// --- 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 = (props) => { + const handleCancelClick = () => { + if (props.onCancel) { + props.onCancel(); + } + toast.dismiss(props.t.id); + }; + + return ( +
+
+ + {props.message} +
+ +
+ ); +}; + +// Info Toast +export interface InfoToastProps extends BaseToastProps {} +export const InfoToastComponent: Component = (props) => { + const handleCancelClick = () => { + if (props.onCancel) { + props.onCancel(); + } + toast.dismiss(props.t.id); + }; + + return ( +
+
+ + {props.message} +
+ +
+ ); +}; + +// Warning Toast +export interface WarningToastProps extends BaseToastProps {} +export const WarningToastComponent: Component = (props) => { + const handleCancelClick = () => { + if (props.onCancel) { + props.onCancel(); + } + toast.dismiss(props.t.id); + }; + + return ( +
+
+ + {props.message} +
+ +
+ ); +}; + +// --- 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) => ( + 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) => ( + + ), { duration: 6000 }); // Or some default duration +}; + +// Function to show a warning toast +export const showWarningToast = (message: string) => { + toast.custom((t) => ( + 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."); +*/ diff --git a/pkgs/webview-ui/app/src/routes/flash/view.tsx b/pkgs/webview-ui/app/src/routes/flash/view.tsx index 2076fe2fc..694383c2a 100644 --- a/pkgs/webview-ui/app/src/routes/flash/view.tsx +++ b/pkgs/webview-ui/app/src/routes/flash/view.tsx @@ -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([]); const [passwordVisibility, setPasswordVisibility] = createSignal( [], ); - 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 => { - 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; + // Define the options for the SSH key file dialog + const sshKeyDialogOptions: FileDialogOptions = { + title: "Select SSH Public Key(s)", + filters: { patterns: ["*.pub"] }, + initial_folder: "~/.ssh", }; + 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 ... */}
{
- {/* - USB Utility image. - - - Will make bootstrapping new machines easier by providing secure remote - connection to any machine when plugged in. - */}
-
- - Provide your SSH public key. For secure and passwordless SSH - connections. - - - {(field, props) => ( - <> - { - 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, props) => ( @@ -322,6 +286,7 @@ export const Flash = () => {
+ {/* ... Network settings as before ... */} Networks} field={ @@ -338,6 +303,7 @@ export const Flash = () => {
} /> + {/* TODO: You would render the actual WiFi input fields here using a loop over wifiNetworks() signal */} @@ -445,20 +411,23 @@ export const Flash = () => { +
diff --git a/pkgs/webview-ui/app/src/routes/machines/details.tsx b/pkgs/webview-ui/app/src/routes/machines/details.tsx index 87dc74ad6..4f7ebc742 100644 --- a/pkgs/webview-ui/app/src/routes/machines/details.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/details.tsx @@ -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; + 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("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) => { /> )} + diff --git a/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx b/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx index 9cb17e744..b940f99b6 100644 --- a/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/install/hardware-step.tsx @@ -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 { @@ -75,21 +79,21 @@ export const HWStep = (props: StepProps) => { 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) => { 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) => { /> )} +