diff --git a/desktop/index.html b/desktop/index.html index aa6c9999..a8f9c0d6 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -2,13 +2,12 @@ - flow Desktop
- + diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 63f8b381..e268d5df 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -18,9 +18,10 @@ "@tauri-apps/plugin-shell": "^2.2.2", "@types/prismjs": "^1.26.5", "prismjs": "^1.30.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^10.1.0" + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-markdown": "^10.1.0", + "wouter": "^3.7.1" }, "devDependencies": { "@chromatic-com/storybook": "^4.0.0", @@ -32,8 +33,8 @@ "@storybook/react-vite": "^9.0.4", "@tauri-apps/cli": "^2", "@types/node": "^20.11.24", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", "@vitejs/plugin-react": "^4.3.4", "@vitest/browser": "^3.2.1", "@vitest/coverage-v8": "^3.2.1", @@ -101,22 +102,22 @@ } }, "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -132,16 +133,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", - "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.3", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -165,6 +166,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -238,27 +249,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", - "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3" + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", - "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -324,38 +335,28 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "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" - } - }, "node_modules/@babel/types": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", - "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1043,22 +1044,22 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz", - "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz", - "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.1", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react": { @@ -1077,12 +1078,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.3.tgz", - "integrity": "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -1090,9 +1091,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, "node_modules/@humanfs/core": { @@ -1211,18 +1212,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1235,16 +1232,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "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" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1253,9 +1240,9 @@ "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==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1270,9 +1257,9 @@ "dev": true }, "node_modules/@mantine/core": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.0.2.tgz", - "integrity": "sha512-2Ps7bRTeTbRwAKTCL9xdflPz0pwOlTq6ohyTbDZMCADqecf09GHI7GiX+HJatqbPZ2t8jK0fN1b48YhjJaxTqg==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.2.1.tgz", + "integrity": "sha512-KxvydotyFRdrRbqULUX2G35/GddPFju9XQUv/vdDWu1ytIWZViTguc+WSj1aBd0DtfRrSaofU5ezZISEXVrPBA==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.28", @@ -1283,15 +1270,15 @@ "type-fest": "^4.27.0" }, "peerDependencies": { - "@mantine/hooks": "8.0.2", + "@mantine/hooks": "8.2.1", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/hooks": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.0.2.tgz", - "integrity": "sha512-0jpEdC0KIAZ54D5kd9rJudrEm6vkvnrL9yYHnkuNbxokXSzDdYA/wpHnKR5WW+u6fW4JF6A6A7gN1vXKeC9MSw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.2.1.tgz", + "integrity": "sha512-gnRDk5FXCD9fa0AjlAj9otCsZL9QJzVrpYZk9KjOEoP5XR1TEE2F9/rGbajh1UVjPnD3jUlNLRJMH0YHTlA65A==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" @@ -2497,30 +2484,23 @@ "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==" }, - "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "license": "MIT" - }, "node_modules/@types/react": { - "version": "18.3.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", - "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.0.0" } }, "node_modules/@types/resolve": { @@ -5982,6 +5962,7 @@ "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": { @@ -6255,6 +6236,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -7028,6 +7010,12 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -7784,13 +7772,10 @@ "license": "MIT" }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } @@ -7828,16 +7813,15 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.1.1" } }, "node_modules/react-is": { @@ -8067,6 +8051,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexparam": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", + "integrity": "sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -8269,13 +8262,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -9540,6 +9530,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9951,6 +9950,20 @@ "node": ">=0.10.0" } }, + "node_modules/wouter": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.7.1.tgz", + "integrity": "sha512-od5LGmndSUzntZkE2R5CHhoiJ7YMuTIbiXsa0Anytc2RATekgv4sfWRAxLEULBrp7ADzinWQw8g470lkT8+fOw==", + "license": "Unlicense", + "dependencies": { + "mitt": "^3.0.1", + "regexparam": "^3.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/desktop/package.json b/desktop/package.json index 3b5f6867..5e5a5bb7 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -26,9 +26,10 @@ "@tauri-apps/plugin-shell": "^2.2.2", "@types/prismjs": "^1.26.5", "prismjs": "^1.30.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^10.1.0" + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-markdown": "^10.1.0", + "wouter": "^3.7.1" }, "devDependencies": { "@chromatic-com/storybook": "^4.0.0", @@ -40,8 +41,8 @@ "@storybook/react-vite": "^9.0.4", "@tauri-apps/cli": "^2", "@types/node": "^20.11.24", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.1", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", "@vitejs/plugin-react": "^4.3.4", "@vitest/browser": "^3.2.1", "@vitest/coverage-v8": "^3.2.1", diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index f87d5aa0..7ec3905d 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -1,13 +1,14 @@ -import "@mantine/core/styles.css"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; +import { Route, Switch } from "wouter"; import "./App.css"; -import { useBackendData, useExecutable } from "./hooks/useBackendData"; -import { NotifierProvider, useNotifier } from "./hooks/useNotifier"; -import { SettingsProvider } from "./hooks/useSettings"; -import { AppShell, View, Viewer } from "./layout"; -import { ThemeProvider } from "./theme/ThemeProvider"; -import { NotificationType } from "./types/notification"; +import { AppProvider } from "./hooks/useAppContext.tsx"; +import { NotifierProvider } from "./hooks/useNotifier"; +import { AppShell } from "./layout"; +import { PageWrapper } from "./components/PageWrapper.tsx"; +import { Settings, Welcome, Data } from "./pages"; +import { WorkspaceRoute } from "./pages/Workspace/WorkspaceRoute.tsx"; +import { ExecutableRoute } from "./pages/Executable/ExecutableRoute.tsx"; +import { Text } from "@mantine/core"; const queryClient = new QueryClient({ defaultOptions: { @@ -18,104 +19,42 @@ const queryClient = new QueryClient({ }, }); -function AppContent() { - const [currentView, setCurrentView] = useState(View.Welcome); - const [welcomeMessage, setWelcomeMessage] = useState(""); - const [selectedExecutable, setSelectedExecutable] = useState( - null - ); - const [selectedWorkspace, setSelectedWorkspace] = useState( - null - ); - const { notification, setNotification } = useNotifier(); - - const { config, workspaces, executables, isLoading, hasError, refreshAll } = - useBackendData(selectedWorkspace); - - const { executable, executableError } = useExecutable( - selectedExecutable || "" - ); - - // Set initial workspace from config when it loads - useEffect(() => { - if (config?.currentWorkspace && workspaces && workspaces.length > 0) { - // Only update if we don't have a selected workspace or if the config workspace is different - if (!selectedWorkspace || config.currentWorkspace !== selectedWorkspace) { - setSelectedWorkspace(config.currentWorkspace); - } - } - }, [config, workspaces]); - - useEffect(() => { - if (hasError) { - setNotification({ - title: "Unexpected error", - message: hasError.message || "An error occurred", - type: NotificationType.Error, - autoClose: false, - }); - } - }, [hasError]); - - useEffect(() => { - if (welcomeMessage === "" && executables?.length > 0) { - setWelcomeMessage("Select an executable to get started."); - } - }, [executables, welcomeMessage]); - - const handleLogoClick = () => { - setCurrentView(View.Welcome); - }; - - return ( - setCurrentView(view as View)} - workspaces={workspaces || []} - selectedWorkspace={selectedWorkspace} - onSelectWorkspace={(workspaceName) => { - setSelectedWorkspace(workspaceName); - setCurrentView(View.Workspace); - }} - visibleExecutables={executables || []} - onSelectExecutable={(executable) => { - if (executable === selectedExecutable) { - return; - } - setSelectedExecutable(executable); - if (currentView !== View.Executable) { - setCurrentView(View.Executable); - } - }} - onLogoClick={handleLogoClick} - hasError={hasError} - isLoading={isLoading} - refreshAll={refreshAll} - notification={notification} - setNotification={setNotification} - > - w.name === selectedWorkspace) || null - } - /> - - ); -} - function App() { return ( - - - - - + + + + + + + + + + + + + Logs view coming soon... + + + + + + + + + + + + + ); diff --git a/desktop/src/components/CodeHighlighter/CodeHighlighter.stories.tsx b/desktop/src/components/CodeHighlighter/CodeHighlighter.stories.tsx index 7fcbbbe9..b42ccfea 100644 --- a/desktop/src/components/CodeHighlighter/CodeHighlighter.stories.tsx +++ b/desktop/src/components/CodeHighlighter/CodeHighlighter.stories.tsx @@ -51,12 +51,12 @@ NC='\\033[0m' # No Color # Function to log messages log() { - echo -e "\${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]\${NC} \$1" | tee -a "\$LOG_FILE" + echo -e "\${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]\${NC} $1" | tee -a "\$LOG_FILE" } # Function to handle errors error() { - echo -e "\${RED}[ERROR]\${NC} \$1" >&2 + echo -e "\${RED}[ERROR]\${NC} $1" >&2 exit 1 } diff --git a/desktop/src/components/CodeHighlighter/CodeHighlighter.tsx b/desktop/src/components/CodeHighlighter/CodeHighlighter.tsx index 12a748be..43fd50a7 100644 --- a/desktop/src/components/CodeHighlighter/CodeHighlighter.tsx +++ b/desktop/src/components/CodeHighlighter/CodeHighlighter.tsx @@ -53,6 +53,7 @@ export function CodeHighlighter({ type: NotificationType.Error, autoClose: true, }); + console.error(error); } }; diff --git a/desktop/src/components/MarkdownRenderer/MarkdownRenderer.tsx b/desktop/src/components/MarkdownRenderer/MarkdownRenderer.tsx index 4ffc2ab9..9597ccaa 100644 --- a/desktop/src/components/MarkdownRenderer/MarkdownRenderer.tsx +++ b/desktop/src/components/MarkdownRenderer/MarkdownRenderer.tsx @@ -47,7 +47,11 @@ export function MarkdownRenderer({ pre: (props: ComponentPropsWithoutRef) => { const codeElement = props.children as React.ReactElement; const codeContent = codeElement?.props?.children || ""; - return {codeContent}; + return ( + + {codeContent} + + ); }, }} > diff --git a/desktop/src/components/PageWrapper.tsx b/desktop/src/components/PageWrapper.tsx new file mode 100644 index 00000000..1e6a855a --- /dev/null +++ b/desktop/src/components/PageWrapper.tsx @@ -0,0 +1,16 @@ +import { ScrollArea } from "@mantine/core"; + +export function PageWrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/desktop/src/components/Settings.tsx b/desktop/src/components/Settings.tsx index b8c00e0c..a1606047 100644 --- a/desktop/src/components/Settings.tsx +++ b/desktop/src/components/Settings.tsx @@ -12,14 +12,16 @@ export function SettingRow({ label, description, children }: SettingRowProps) { - {label} + + {label} + {description && ( - {description} + + {description} + )} - - {children} - + {children} ); @@ -33,12 +35,17 @@ interface SettingSectionProps { export function SettingSection({ title, children }: SettingSectionProps) { return ( - + {title} - - {children} - + {children} ); } diff --git a/desktop/src/hooks/useAppContext.tsx b/desktop/src/hooks/useAppContext.tsx new file mode 100644 index 00000000..e50e7552 --- /dev/null +++ b/desktop/src/hooks/useAppContext.tsx @@ -0,0 +1,165 @@ +import React from "react"; +import { createContext, useContext, useState, useEffect } from "react"; +import { Config } from "../types/generated/config"; +import { EnrichedWorkspace } from "../types/workspace"; +import { EnrichedExecutable } from "../types/executable"; +import { useConfig } from "./useConfig"; +import { useWorkspaces } from "./useWorkspace"; +import { useExecutables } from "./useExecutable"; +import { invoke } from "@tauri-apps/api/core"; +import { useQuery } from "@tanstack/react-query"; + +interface AppContextType { + config: Config | null; + selectedWorkspace: string | null; + setSelectedWorkspace: (workspaceName: string | null) => void; + workspaces: EnrichedWorkspace[]; + executables: EnrichedExecutable[]; + isLoading: boolean; + hasError: Error | null; + refreshAll: () => void; +} + +export const AppContext = createContext(undefined); + +export function useFlowBinaryCheck() { + const { + data: isFlowBinaryAvailable, + isLoading: isCheckingBinary, + error: binaryCheckError, + } = useQuery({ + queryKey: ["flowBinaryCheck"], + queryFn: async () => { + try { + await invoke("check_flow_binary"); + return true; + } catch (error) { + console.error(error); + throw new Error("flow CLI not found or not executable"); + } + }, + retry: false, + refetchOnWindowFocus: false, + }); + + return { + isFlowBinaryAvailable, + isCheckingBinary, + binaryCheckError, + }; +} + +export function AppProvider({ children }: { children: React.ReactNode }) { + const { isCheckingBinary, binaryCheckError } = useFlowBinaryCheck(); + const enabled = !binaryCheckError && !isCheckingBinary; + + const { config, isConfigLoading, configError, refreshConfig } = + useConfig(enabled); + + const { + workspaces, + isWorkspacesLoading, + workspacesError, + refreshWorkspaces, + } = useWorkspaces(enabled); + const [selectedWorkspace, setSelectedWorkspace] = useState( + null, + ); + + useEffect(() => { + if (config?.currentWorkspace && workspaces && workspaces.length > 0) { + // Only set if we don't have a selected workspace or if the config workspace exists + const configWorkspaceExists = workspaces.some( + (w) => w.name === config.currentWorkspace, + ); + if (!selectedWorkspace && configWorkspaceExists) { + setSelectedWorkspace(config.currentWorkspace); + } + } + }, [config, workspaces, selectedWorkspace]); + + const { + executables, + isExecutablesLoading, + executablesError, + refreshExecutables, + } = useExecutables(selectedWorkspace, enabled); + + const isLoading = + isConfigLoading || isWorkspacesLoading || isExecutablesLoading; + const hasError = configError || workspacesError || executablesError; + if (hasError) { + console.error("Error", hasError); + } + + const refreshAll = () => { + refreshConfig(); + refreshWorkspaces(); + refreshExecutables(); + }; + + // If flow binary is not available, return early with error state + if (binaryCheckError) { + return ( + {}, + }} + > + {children} + + ); + } + + // If still checking binary, show loading state + if (isCheckingBinary) { + return ( + {}, + }} + > + {children} + + ); + } + + return ( + + {children} + + ); +} + +export function useAppContext() { + const context = useContext(AppContext); + if (context === undefined) { + throw new Error("useAppContext must be used within an AppProvider"); + } + return context; +} diff --git a/desktop/src/hooks/useBackendData.ts b/desktop/src/hooks/useBackendData.ts deleted file mode 100644 index c35776d9..00000000 --- a/desktop/src/hooks/useBackendData.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { invoke } from "@tauri-apps/api/core"; -import React from "react"; -import { EnrichedExecutable } from "../types/executable"; -import { Config } from "../types/generated/config"; -import { EnrichedWorkspace } from "../types/workspace"; - -export function useConfig(enabled: boolean = true) { - const queryClient = useQueryClient(); - - const { - data: config, - isLoading: isConfigLoading, - error: configError, - } = useQuery({ - queryKey: ["config"], - queryFn: async () => { - return await invoke("get_config"); - }, - enabled, - }); - - const refreshConfig = () => { - queryClient.invalidateQueries({ queryKey: ["config"] }); - }; - - const updateTheme = async (theme: string) => { - try { - await invoke("set_config_theme", { theme }); - refreshConfig(); - } catch (error) { - throw new Error(`Failed to update theme: ${error}`); - } - }; - - const updateWorkspaceMode = async (mode: string) => { - try { - await invoke("set_config_workspace_mode", { mode }); - refreshConfig(); - } catch (error) { - throw new Error(`Failed to update workspace mode: ${error}`); - } - }; - - const updateLogMode = async (mode: string) => { - try { - await invoke("set_config_log_mode", { mode }); - refreshConfig(); - } catch (error) { - throw new Error(`Failed to update log mode: ${error}`); - } - }; - - const updateNamespace = async (namespace: string) => { - try { - await invoke("set_config_namespace", { namespace }); - refreshConfig(); - } catch (error) { - throw new Error(`Failed to update namespace: ${error}`); - } - }; - - const updateCurrentWorkspace = async (workspace: string) => { - try { - await invoke("set_workspace", { name: workspace, fixed: false }); - refreshConfig(); - } catch (error) { - throw new Error(`Failed to update current workspace: ${error}`); - } - }; - - const updateDefaultTimeout = async (timeout: string) => { - try { - await invoke("set_config_timeout", { timeout }); - refreshConfig(); - } catch (error) { - throw new Error(`Failed to update default timeout: ${error}`); - } - }; - - return { - config, - isConfigLoading, - configError, - refreshConfig, - updateTheme, - updateWorkspaceMode, - updateLogMode, - updateNamespace, - updateCurrentWorkspace, - updateDefaultTimeout, - }; -} - -export function useWorkspaces(enabled: boolean = true) { - const queryClient = useQueryClient(); - - const { - data: workspaces, - isLoading: isWorkspacesLoading, - error: workspacesError, - } = useQuery({ - queryKey: ["workspaces"], - queryFn: async () => { - const response = await invoke("list_workspaces"); - return response; - }, - enabled, - }); - - const refreshWorkspaces = () => { - queryClient.invalidateQueries({ queryKey: ["workspaces"] }); - }; - - return { - workspaces, - isWorkspacesLoading, - workspacesError, - refreshWorkspaces, - }; -} - -export function useExecutable(executableRef: string) { - const queryClient = useQueryClient(); - const [currentExecutable, setCurrentExecutable] = - React.useState(null); - - const { - data: executable, - isLoading: isExecutableLoading, - error: executableError, - } = useQuery({ - queryKey: ["executable", executableRef], - queryFn: async () => { - if (!executableRef) return null; - const response = await invoke("get_executable", { - executableRef: executableRef, - }); - return response; - }, - enabled: !!executableRef, - }); - - // Update current executable when we have new data - React.useEffect(() => { - if (executable) { - setCurrentExecutable(executable); - } - }, [executable]); - - const refreshExecutable = () => { - if (executableRef) { - queryClient.invalidateQueries({ - queryKey: ["executable", executableRef], - }); - } - }; - - return { - executable: currentExecutable, - isExecutableLoading, - executableError, - refreshExecutable, - }; -} - -export function useExecutables(selectedWorkspace: string | null, enabled: boolean = true) { - const queryClient = useQueryClient(); - - const { - data: executables, - isLoading: isExecutablesLoading, - error: executablesError, - } = useQuery({ - queryKey: ["executables", selectedWorkspace], - queryFn: async () => { - if (!selectedWorkspace) return []; - const response = await invoke("list_executables", { - workspace: selectedWorkspace, - }); - return response; - }, - enabled: enabled && !!selectedWorkspace, // Only run when workspace is selected AND enabled - }); - - const refreshExecutables = () => { - if (selectedWorkspace) { - queryClient.invalidateQueries({ - queryKey: ["executables", selectedWorkspace], - }); - } - }; - - return { - executables: executables || [], - isExecutablesLoading, - executablesError, - refreshExecutables, - }; -} - -// Hook to check if flow binary is available -export function useFlowBinaryCheck() { - const { data: isFlowBinaryAvailable, isLoading: isCheckingBinary, error: binaryCheckError } = useQuery({ - queryKey: ["flowBinaryCheck"], - queryFn: async () => { - try { - await invoke("check_flow_binary"); - return true; - } catch (error) { - console.error(error); - throw new Error("flow CLI not found or not executable"); - } - }, - retry: false, - refetchOnWindowFocus: false, - }); - - return { - isFlowBinaryAvailable, - isCheckingBinary, - binaryCheckError, - }; -} - -// Composite hook that combines all data sources -export function useBackendData(selectedWorkspace: string | null) { - const { isCheckingBinary, binaryCheckError } = useFlowBinaryCheck(); - - // Only enable other queries if flow binary is available - const enabled = !binaryCheckError && !isCheckingBinary; - - const { config, isConfigLoading, configError, refreshConfig } = useConfig(enabled); - const { - workspaces, - isWorkspacesLoading, - workspacesError, - refreshWorkspaces, - } = useWorkspaces(enabled); - const { - executables, - isExecutablesLoading, - executablesError, - refreshExecutables, - } = useExecutables(selectedWorkspace, enabled); - - // If flow binary is not available, return early with error state - if (binaryCheckError) { - return { - config: null, - workspaces: [], - executables: [], - isLoading: false, - hasError: binaryCheckError, - refreshAll: () => {}, - refreshConfig: () => {}, - refreshWorkspaces: () => {}, - refreshExecutables: () => {}, - }; - } - - // If still checking binary, show loading state - if (isCheckingBinary) { - return { - config: null, - workspaces: [], - executables: [], - isLoading: true, - hasError: null, - refreshAll: () => {}, - refreshConfig: () => {}, - refreshWorkspaces: () => {}, - refreshExecutables: () => {}, - }; - } - - const isLoading = - isConfigLoading || isWorkspacesLoading || isExecutablesLoading; - const hasError = configError || workspacesError || executablesError; - if (hasError) { - console.error("Error", hasError); - } - - const refreshAll = () => { - refreshConfig(); - refreshWorkspaces(); - refreshExecutables(); - }; - - return { - config, - workspaces, - executables, - isLoading, - hasError, - refreshAll, - refreshConfig, - refreshWorkspaces, - refreshExecutables, - }; -} diff --git a/desktop/src/hooks/useConfig.ts b/desktop/src/hooks/useConfig.ts new file mode 100644 index 00000000..d94fe405 --- /dev/null +++ b/desktop/src/hooks/useConfig.ts @@ -0,0 +1,90 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { invoke } from "@tauri-apps/api/core"; +import { Config } from "../types/generated/config.ts"; + +export function useConfig(enabled: boolean = true) { + const queryClient = useQueryClient(); + + const { + data: config, + isLoading: isConfigLoading, + error: configError, + } = useQuery({ + queryKey: ["config"], + queryFn: async () => { + return await invoke("get_config"); + }, + enabled, + }); + + const refreshConfig = () => { + queryClient.invalidateQueries({ queryKey: ["config"] }); + }; + + const updateTheme = async (theme: string) => { + try { + await invoke("set_config_theme", { theme }); + refreshConfig(); + } catch (error) { + throw new Error(`Failed to update theme: ${error}`); + } + }; + + const updateWorkspaceMode = async (mode: string) => { + try { + await invoke("set_config_workspace_mode", { mode }); + refreshConfig(); + } catch (error) { + throw new Error(`Failed to update workspace mode: ${error}`); + } + }; + + const updateLogMode = async (mode: string) => { + try { + await invoke("set_config_log_mode", { mode }); + refreshConfig(); + } catch (error) { + throw new Error(`Failed to update log mode: ${error}`); + } + }; + + const updateNamespace = async (namespace: string) => { + try { + await invoke("set_config_namespace", { namespace }); + refreshConfig(); + } catch (error) { + throw new Error(`Failed to update namespace: ${error}`); + } + }; + + const updateCurrentWorkspace = async (workspace: string) => { + try { + await invoke("set_workspace", { name: workspace, fixed: false }); + refreshConfig(); + } catch (error) { + throw new Error(`Failed to update current workspace: ${error}`); + } + }; + + const updateDefaultTimeout = async (timeout: string) => { + try { + await invoke("set_config_timeout", { timeout }); + refreshConfig(); + } catch (error) { + throw new Error(`Failed to update default timeout: ${error}`); + } + }; + + return { + config, + isConfigLoading, + configError, + refreshConfig, + updateTheme, + updateWorkspaceMode, + updateLogMode, + updateNamespace, + updateCurrentWorkspace, + updateDefaultTimeout, + }; +} diff --git a/desktop/src/hooks/useExecutable.ts b/desktop/src/hooks/useExecutable.ts new file mode 100644 index 00000000..22fd169d --- /dev/null +++ b/desktop/src/hooks/useExecutable.ts @@ -0,0 +1,84 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import React from "react"; +import { EnrichedExecutable } from "../types/executable.ts"; +import { invoke } from "@tauri-apps/api/core"; + +export function useExecutable(executableRef: string) { + const queryClient = useQueryClient(); + const [currentExecutable, setCurrentExecutable] = + React.useState(null); + + const { + data: executable, + isLoading: isExecutableLoading, + error: executableError, + } = useQuery({ + queryKey: ["executable", executableRef], + queryFn: async () => { + if (!executableRef) return null; + return await invoke("get_executable", { + executableRef: executableRef, + }); + }, + enabled: !!executableRef, + }); + + // Update current executable when we have new data + React.useEffect(() => { + if (executable) { + setCurrentExecutable(executable); + } + }, [executable]); + + const refreshExecutable = () => { + if (executableRef) { + queryClient.invalidateQueries({ + queryKey: ["executable", executableRef], + }); + } + }; + + return { + executable: currentExecutable, + isExecutableLoading, + executableError, + refreshExecutable, + }; +} + +export function useExecutables( + selectedWorkspace: string | null, + enabled: boolean = true, +) { + const queryClient = useQueryClient(); + + const { + data: executables, + isLoading: isExecutablesLoading, + error: executablesError, + } = useQuery({ + queryKey: ["executables", selectedWorkspace], + queryFn: async () => { + if (!selectedWorkspace) return []; + return await invoke("list_executables", { + workspace: selectedWorkspace, + }); + }, + enabled: enabled && !!selectedWorkspace, // Only run when workspace is selected AND enabled + }); + + const refreshExecutables = () => { + if (selectedWorkspace) { + queryClient.invalidateQueries({ + queryKey: ["executables", selectedWorkspace], + }); + } + }; + + return { + executables: executables || [], + isExecutablesLoading, + executablesError, + refreshExecutables, + }; +} diff --git a/desktop/src/hooks/useSettings.tsx b/desktop/src/hooks/useSettings.tsx index d2f99d47..6c52e48e 100644 --- a/desktop/src/hooks/useSettings.tsx +++ b/desktop/src/hooks/useSettings.tsx @@ -22,7 +22,7 @@ const defaultSettings: Settings = { }; const SettingsContext = createContext( - undefined + undefined, ); export function SettingsProvider({ children }: { children: ReactNode }) { diff --git a/desktop/src/hooks/useWorkspace.ts b/desktop/src/hooks/useWorkspace.ts new file mode 100644 index 00000000..34259022 --- /dev/null +++ b/desktop/src/hooks/useWorkspace.ts @@ -0,0 +1,61 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { invoke } from "@tauri-apps/api/core"; +import { EnrichedWorkspace } from "../types/workspace.ts"; + +export function useWorkspace(workspaceName: string, enabled: boolean = true) { + const queryClient = useQueryClient(); + + const { + data: workspace, + isLoading: isWorkspaceLoading, + error: workspaceError, + } = useQuery({ + queryKey: ["workspace", workspaceName], + queryFn: async () => { + return await invoke("get_workspace", { + name: workspaceName, + }); + }, + enabled, + }); + + const refreshWorkspace = () => { + if (workspaceName) { + queryClient.invalidateQueries({ queryKey: ["workspace", workspaceName] }); + } + }; + + return { + workspace, + workspaceError, + isWorkspaceLoading, + refreshWorkspace, + }; +} + +export function useWorkspaces(enabled: boolean = true) { + const queryClient = useQueryClient(); + + const { + data: workspaces, + isLoading: isWorkspacesLoading, + error: workspacesError, + } = useQuery({ + queryKey: ["workspaces"], + queryFn: async () => { + return await invoke("list_workspaces"); + }, + enabled, + }); + + const refreshWorkspaces = () => { + queryClient.invalidateQueries({ queryKey: ["workspaces"] }); + }; + + return { + workspaces, + isWorkspacesLoading, + workspacesError, + refreshWorkspaces, + }; +} diff --git a/desktop/src/layout/AppShell/AppShell.tsx b/desktop/src/layout/AppShell/AppShell.tsx index 1b4f46b7..adb8cd23 100644 --- a/desktop/src/layout/AppShell/AppShell.tsx +++ b/desktop/src/layout/AppShell/AppShell.tsx @@ -1,57 +1,36 @@ import { - ActionIcon, Loader, AppShell as MantineAppShell, Notification as MantineNotification, Text, } from "@mantine/core"; -import { IconRefresh } from "@tabler/icons-react"; -import { ReactNode } from "react"; -import { EnrichedExecutable } from "../../types/executable"; -import { - colorFromType, - Notification, - NotificationType, -} from "../../types/notification"; -import { EnrichedWorkspace } from "../../types/workspace"; +import { ReactNode, useEffect } from "react"; +import { colorFromType, NotificationType } from "../../types/notification"; import { Header } from "../Header/Header"; import { Sidebar } from "../Sidebar/Sidebar"; -import { View } from "../Viewer/Viewer"; import styles from "./AppShell.module.css"; +import { useAppContext } from "../../hooks/useAppContext.tsx"; +import { useNotifier } from "../../hooks/useNotifier.tsx"; interface AppShellProps { children: ReactNode; - currentView: View; - setCurrentView: (view: View) => void; - workspaces: EnrichedWorkspace[]; - selectedWorkspace: string | null; - onSelectWorkspace: (workspaceName: string) => void; - visibleExecutables: EnrichedExecutable[]; - onSelectExecutable: (executable: string) => void; - onLogoClick: () => void; - hasError: Error | null; - isLoading: boolean; - refreshAll: () => void; - notification: Notification | null; - setNotification: (notification: Notification | null) => void; } -export function AppShell({ - children, - currentView, - setCurrentView, - workspaces, - selectedWorkspace, - onSelectWorkspace, - visibleExecutables, - onSelectExecutable, - onLogoClick, - hasError, - isLoading, - refreshAll, - notification, - setNotification, -}: AppShellProps) { +export function AppShell({ children }: AppShellProps) { + const { isLoading, hasError } = useAppContext(); + const { notification, setNotification } = useNotifier(); + + useEffect(() => { + if (hasError) { + setNotification({ + title: "Unexpected error", + message: hasError.message || "An error occurred", + type: NotificationType.Error, + autoClose: false, + }); + } + }, [hasError, setNotification]); + return ( -
{}} - onRefreshWorkspaces={() => { - refreshAll(); - setNotification({ - title: "Refresh completed", - message: "flow data has synced and refreshed successfully", - type: NotificationType.Success, - autoClose: true, - autoCloseDelay: 3000, - }); - }} - /> +
- + @@ -106,14 +64,7 @@ export function AppShell({ }} > Error loading data - - - + {hasError.message} ) : (
diff --git a/desktop/src/layout/Header/ActionButtons/ActionButtons.stories.tsx b/desktop/src/layout/Header/ActionButtons/ActionButtons.stories.tsx index 0bdca8a4..043f84c0 100644 --- a/desktop/src/layout/Header/ActionButtons/ActionButtons.stories.tsx +++ b/desktop/src/layout/Header/ActionButtons/ActionButtons.stories.tsx @@ -1,13 +1,13 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { ActionButtons } from './ActionButtons'; +import type { Meta, StoryObj } from "@storybook/react"; +import { ActionButtons } from "./ActionButtons"; const meta = { - title: 'Components/Header/ActionButtons', + title: "Components/Header/ActionButtons", component: ActionButtons, parameters: { - layout: 'centered', + layout: "centered", }, - tags: ['autodocs'], + tags: ["autodocs"], } satisfies Meta; export default meta; @@ -15,14 +15,14 @@ type Story = StoryObj; export const Default: Story = { args: { - onCreateWorkspace: () => console.log('Create workspace clicked'), - onRefreshWorkspaces: () => console.log('Refresh workspaces clicked'), + onCreateWorkspace: () => console.log("Create workspace clicked"), + onRefreshWorkspaces: () => console.log("Refresh workspaces clicked"), }, }; export const Interactive: Story = { args: { - onCreateWorkspace: () => alert('Create workspace action triggered'), - onRefreshWorkspaces: () => alert('Refresh workspaces action triggered'), + onCreateWorkspace: () => alert("Create workspace action triggered"), + onRefreshWorkspaces: () => alert("Refresh workspaces action triggered"), }, }; diff --git a/desktop/src/layout/Header/ActionButtons/ActionButtons.tsx b/desktop/src/layout/Header/ActionButtons/ActionButtons.tsx index d20f4627..35ab42a4 100644 --- a/desktop/src/layout/Header/ActionButtons/ActionButtons.tsx +++ b/desktop/src/layout/Header/ActionButtons/ActionButtons.tsx @@ -6,7 +6,10 @@ interface ActionButtonsProps { onRefreshWorkspaces: () => void; } -export function ActionButtons({ onCreateWorkspace, onRefreshWorkspaces }: ActionButtonsProps) { +export function ActionButtons({ + onCreateWorkspace, + onRefreshWorkspaces, +}: ActionButtonsProps) { return ( - ) -} \ No newline at end of file + ); +} diff --git a/desktop/src/layout/Header/Header.stories.tsx b/desktop/src/layout/Header/Header.stories.tsx index 42521e80..cc5cdb51 100644 --- a/desktop/src/layout/Header/Header.stories.tsx +++ b/desktop/src/layout/Header/Header.stories.tsx @@ -1,15 +1,15 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { Header } from './Header'; -import { AppShell } from '@mantine/core'; +import type { Meta, StoryObj } from "@storybook/react"; +import { Header } from "./Header"; +import { AppShell } from "@mantine/core"; const meta = { - title: 'Components/Header', + title: "Components/Header", component: Header, parameters: { - layout: 'fullscreen', + layout: "fullscreen", docs: { story: { - height: '100px', + height: "100px", }, }, }, @@ -20,7 +20,7 @@ const meta = { ), ], - tags: ['autodocs'], + tags: ["autodocs"], } satisfies Meta; export default meta; @@ -28,14 +28,14 @@ type Story = StoryObj; export const Default: Story = { args: { - onCreateWorkspace: () => console.log('Create workspace clicked'), - onRefreshWorkspaces: () => console.log('Refresh workspaces clicked'), + onCreateWorkspace: () => console.log("Create workspace clicked"), + onRefreshWorkspaces: () => console.log("Refresh workspaces clicked"), }, }; export const Interactive: Story = { args: { - onCreateWorkspace: () => alert('Create workspace action triggered'), - onRefreshWorkspaces: () => alert('Refresh workspaces action triggered'), + onCreateWorkspace: () => alert("Create workspace action triggered"), + onRefreshWorkspaces: () => alert("Refresh workspaces action triggered"), }, }; diff --git a/desktop/src/layout/Header/Header.tsx b/desktop/src/layout/Header/Header.tsx index 3c21bec9..8b634b04 100644 --- a/desktop/src/layout/Header/Header.tsx +++ b/desktop/src/layout/Header/Header.tsx @@ -1,22 +1,20 @@ import { ActionIcon } from "@mantine/core"; import { IconPlus, IconRefresh } from "@tabler/icons-react"; import styles from "./Header.module.css"; +import { useAppContext } from "../../hooks/useAppContext.tsx"; +import { NotificationType } from "../../types/notification.ts"; +import { useNotifier } from "../../hooks/useNotifier.tsx"; -interface HeaderProps { - onCreateWorkspace: () => void; - onRefreshWorkspaces: () => void; -} +export function Header() { + const { refreshAll } = useAppContext(); + const { setNotification } = useNotifier(); -export function Header({ - onCreateWorkspace, - onRefreshWorkspaces, -}: HeaderProps) { return (
{}} title="Create workspace" variant="light" > @@ -26,7 +24,16 @@ export function Header({ { + refreshAll(); + setNotification({ + title: "Refresh completed", + message: "flow data has synced and refreshed successfully", + type: NotificationType.Success, + autoClose: true, + autoCloseDelay: 3000, + }); + }} title="Refresh workspaces" > diff --git a/desktop/src/layout/Layout.tsx b/desktop/src/layout/Layout.tsx new file mode 100644 index 00000000..e842c2c6 --- /dev/null +++ b/desktop/src/layout/Layout.tsx @@ -0,0 +1,15 @@ +import "@mantine/core/styles.css"; +import { ThemeProvider } from "../theme/ThemeProvider"; +import { HeadScripts } from "../theme/HeadScripts"; +import { SettingsProvider } from "../hooks/useSettings"; + +export function Layout({ children }: { children?: React.ReactNode }) { + return ( + + + + {children} + + + ); +} diff --git a/desktop/src/layout/Sidebar/ExecutableTree/ExecutableTree.tsx b/desktop/src/layout/Sidebar/ExecutableTree/ExecutableTree.tsx index a90d9f9e..506c0c1d 100644 --- a/desktop/src/layout/Sidebar/ExecutableTree/ExecutableTree.tsx +++ b/desktop/src/layout/Sidebar/ExecutableTree/ExecutableTree.tsx @@ -22,7 +22,7 @@ import { IconSettingsAutomation, IconWindowMaximize, } from "@tabler/icons-react"; -import React from "react"; +import { useMemo, useCallback } from "react"; import { BuildVerbType, ConfigurationVerbType, @@ -37,11 +37,8 @@ import { UpdateVerbType, ValidationVerbType, } from "../../../types/executable"; - -interface ExecutableTreeProps { - visibleExecutables: EnrichedExecutable[]; - onSelectExecutable: (executable: string) => void; -} +import { useAppContext } from "../../../hooks/useAppContext.tsx"; +import { useLocation } from "wouter"; interface CustomTreeNodeData extends TreeNodeData { isNamespace: boolean; @@ -111,106 +108,89 @@ function Leaf({ elementProps, }: RenderTreeNodePayload) { const customNode = node as CustomTreeNodeData; - let icon: React.ReactNode; - if (customNode.isNamespace && hasChildren) { - if (selected && expanded) { - icon = ; + const [, setLocation] = useLocation(); + + const icon = useMemo(() => { + if (customNode.isNamespace && hasChildren) { + if (selected && expanded) { + return ; + } else { + return ; + } } else { - icon = ; - } - } else { - switch (customNode.verbType) { - case DeactivationVerbType: - icon = ; - break; - case ConfigurationVerbType: - icon = ; - break; - case DestructionVerbType: - icon = ; - break; - case RetrievalVerbType: - icon = ; - break; - case UpdateVerbType: - icon = ; - break; - case ValidationVerbType: - icon = ; - break; - case LaunchVerbType: - icon = ; - break; - case CreationVerbType: - icon = ; - break; - case RestartVerbType: - icon = ; - break; - case BuildVerbType: - icon = ; - break; - default: - icon = ; + switch (customNode.verbType) { + case DeactivationVerbType: + return ; + case ConfigurationVerbType: + return ; + case DestructionVerbType: + return ; + case RetrievalVerbType: + return ; + case UpdateVerbType: + return ; + case ValidationVerbType: + return ; + case LaunchVerbType: + return ; + case CreationVerbType: + return ; + case RestartVerbType: + return ; + case BuildVerbType: + return ; + default: + return ; + } } + }, [hasChildren, selected, expanded]); + + const handleExecutableClick = useCallback(() => { + const encodedId = encodeURIComponent(customNode.value); + setLocation(`/executable/${encodedId}`); + }, [setLocation]); + + if (customNode.isNamespace) { + return ( + + {icon} + {customNode.label} + + ); } return ( - + {icon} {customNode.label} ); } -export function ExecutableTree({ - visibleExecutables, - onSelectExecutable, -}: ExecutableTreeProps) { +export function ExecutableTree() { + const { executables } = useAppContext(); const tree = useTree(); - React.useEffect(() => { - const selectedValue = tree.selectedState[0]; - if (selectedValue) { - const findNode = ( - nodes: CustomTreeNodeData[] - ): CustomTreeNodeData | undefined => { - for (const node of nodes) { - if (node.value === selectedValue) { - return node; - } - if (node.children) { - const found = findNode(node.children as CustomTreeNodeData[]); - if (found) return found; - } - } - return undefined; - }; - - const node = findNode(getTreeData(visibleExecutables)); - if (node && !node.isNamespace) { - onSelectExecutable(selectedValue); - } - } - }, [tree.selectedState, visibleExecutables, onSelectExecutable]); + const treeData = useMemo(() => getTreeData(executables), [executables]); return ( <> - EXECUTABLES ({visibleExecutables.length}) + EXECUTABLES ({executables.length}) - {visibleExecutables.length === 0 ? ( + {executables.length === 0 ? ( No executables found ) : ( - + )} diff --git a/desktop/src/layout/Sidebar/Sidebar.tsx b/desktop/src/layout/Sidebar/Sidebar.tsx index a4a0a49c..fb1dad06 100644 --- a/desktop/src/layout/Sidebar/Sidebar.tsx +++ b/desktop/src/layout/Sidebar/Sidebar.tsx @@ -1,71 +1,97 @@ import { Group, Image, NavLink, Stack } from "@mantine/core"; -import type { EnrichedExecutable } from "../../types/executable"; -import { EnrichedWorkspace } from "../../types/workspace"; -import { View } from "../Viewer/Viewer"; -import { ViewLinks } from "../Viewer/ViewLinks"; import { ExecutableTree } from "./ExecutableTree/ExecutableTree"; import styles from "./Sidebar.module.css"; import { WorkspaceSelector } from "./WorkspaceSelector/WorkspaceSelector"; import iconImage from "/logo-dark.png"; +import { + IconDatabase, + IconFolders, + IconLogs, + IconSettings, +} from "@tabler/icons-react"; +import { Link, useLocation } from "wouter"; +import { useAppContext } from "../../hooks/useAppContext.tsx"; +import { useCallback } from "react"; -interface SidebarProps { - currentView: View; - setCurrentView: (view: View) => void; - workspaces: EnrichedWorkspace[]; - selectedWorkspace: string | null; - onSelectWorkspace: (workspaceName: string) => void; - visibleExecutables: EnrichedExecutable[]; - onSelectExecutable: (executableId: string) => void; - onLogoClick: () => void; -} +export function Sidebar() { + const [location, setLocation] = useLocation(); + const { executables, selectedWorkspace } = useAppContext(); + + const navigateToWorkspace = useCallback(() => { + setLocation(`/workspace/${selectedWorkspace || ""}`); + }, [setLocation, selectedWorkspace]); + + const navigateToLogs = useCallback(() => { + setLocation("/logs"); + }, [setLocation]); + + const navigateToCache = useCallback(() => { + setLocation("/cache"); + }, [setLocation]); + + const navigateToVault = useCallback(() => { + setLocation("/vault"); + }, [setLocation]); + + const navigateToSettings = useCallback(() => { + setLocation("/settings"); + }, [setLocation]); -export function Sidebar({ - currentView, - setCurrentView, - workspaces, - selectedWorkspace, - onSelectWorkspace, - visibleExecutables, - onSelectExecutable, - onLogoClick, -}: SidebarProps) { return (
-
- flow -
+ + flow + - + - {ViewLinks.map((link) => ( + } + active={location.startsWith("/workspace")} + variant="filled" + onClick={navigateToWorkspace} + /> + + } + active={location.startsWith("/logs")} + variant="filled" + onClick={navigateToLogs} + /> + + } + variant="filled" + childrenOffset={28} + > } - active={currentView === link.view} - onClick={() => setCurrentView(link.view)} + label="Cache" variant="filled" + active={location.startsWith("/cache")} + onClick={navigateToCache} /> - ))} - + + - {visibleExecutables && visibleExecutables.length > 0 && ( - } + active={location.startsWith("/settings")} + variant="filled" + onClick={navigateToSettings} /> - )} + + + {executables && executables.length > 0 && }
); diff --git a/desktop/src/layout/Sidebar/WorkspaceSelector/WorkspaceSelector.tsx b/desktop/src/layout/Sidebar/WorkspaceSelector/WorkspaceSelector.tsx index ddb14cf1..a61e22ef 100644 --- a/desktop/src/layout/Sidebar/WorkspaceSelector/WorkspaceSelector.tsx +++ b/desktop/src/layout/Sidebar/WorkspaceSelector/WorkspaceSelector.tsx @@ -1,49 +1,44 @@ import { ComboboxItem, Group, OptionsFilter, Select } from "@mantine/core"; -import { useConfig } from "../../../hooks/useBackendData"; +import { useConfig } from "../../../hooks/useConfig"; import { useNotifier } from "../../../hooks/useNotifier"; -import { EnrichedWorkspace } from "../../../types/workspace"; import { NotificationType } from "../../../types/notification"; - -interface WorkspaceSelectorProps { - workspaces: EnrichedWorkspace[]; - selectedWorkspace: string | null; - onSelectWorkspace: (workspaceName: string) => void; -} +import { useAppContext } from "../../../hooks/useAppContext.tsx"; const filter: OptionsFilter = ({ options, search }) => { const filtered = (options as ComboboxItem[]).filter((option) => - option.label.toLowerCase().trim().includes(search.toLowerCase().trim()) + option.label.toLowerCase().trim().includes(search.toLowerCase().trim()), ); filtered.sort((a, b) => a.label.localeCompare(b.label)); return filtered; }; -export function WorkspaceSelector({ - workspaces, - selectedWorkspace, - onSelectWorkspace, -}: WorkspaceSelectorProps) { - const { config, updateCurrentWorkspace } = useConfig(); +export function WorkspaceSelector() { + const { selectedWorkspace, setSelectedWorkspace, workspaces, config } = + useAppContext(); + const { updateCurrentWorkspace } = useConfig(); const { setNotification } = useNotifier(); const handleWorkspaceChange = async (workspaceName: string) => { - onSelectWorkspace(workspaceName); + setSelectedWorkspace(workspaceName); - if (config?.workspaceMode === 'dynamic') { + if (config?.workspaceMode === "dynamic") { try { await updateCurrentWorkspace(workspaceName); setNotification({ type: NotificationType.Success, - title: 'Workspace switched', + title: "Workspace switched", message: `Switched to workspace: ${workspaceName}`, autoClose: true, }); } catch (error) { setNotification({ type: NotificationType.Error, - title: 'Error switching workspace', - message: error instanceof Error ? error.message : 'An unknown error occurred', + title: "Error switching workspace", + message: + error instanceof Error + ? error.message + : "An unknown error occurred", autoClose: true, }); } @@ -83,10 +78,10 @@ export function WorkspaceSelector({ }, option: { color: "var(--mantine-color-white)", - "&[data-selected]": { + "&[dataSelected]": { backgroundColor: "var(--mantine-color-dark-5)", }, - "&[data-hovered]": { + "&[dataHovered]": { backgroundColor: "var(--mantine-color-dark-5)", }, }, diff --git a/desktop/src/layout/Viewer/ViewLinks.ts b/desktop/src/layout/Viewer/ViewLinks.ts deleted file mode 100644 index 73c19149..00000000 --- a/desktop/src/layout/Viewer/ViewLinks.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - IconDatabase, - IconFolders, - IconLogs, - IconSettings, -} from "@tabler/icons-react"; -import { View } from "./Viewer"; - -export const ViewLinks = [ - { icon: IconFolders, label: "Workspace", view: View.Workspace }, - { icon: IconLogs, label: "Logs", view: View.Logs }, - { icon: IconDatabase, label: "Data", view: View.Data }, - { icon: IconSettings, label: "Settings", view: View.Settings }, -]; \ No newline at end of file diff --git a/desktop/src/layout/Viewer/Viewer.tsx b/desktop/src/layout/Viewer/Viewer.tsx deleted file mode 100644 index 5b2b080f..00000000 --- a/desktop/src/layout/Viewer/Viewer.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { ScrollArea, Text } from "@mantine/core"; -import Data from "../../pages/Data/Data"; -import Executable from "../../pages/Executable/Executable"; -import { Settings } from "../../pages/Settings/Settings"; -import { Welcome } from "../../pages/Welcome/Welcome"; -import { Workspace } from "../../pages/Workspace/Workspace"; -import type { EnrichedExecutable } from "../../types/executable"; -import { EnrichedWorkspace } from "../../types/workspace"; - -export enum View { - Welcome = "welcome", - Workspace = "workspace", - Executable = "executable", - Logs = "logs", - Data = "data", - Settings = "settings", -} - -interface ViewerProps { - currentView: View; - selectedExecutable: EnrichedExecutable | null; - executableError: Error | null; - welcomeMessage?: string; - workspace: EnrichedWorkspace | null; -} - -export function Viewer({ - currentView, - selectedExecutable, - executableError, - welcomeMessage, - workspace, -}: ViewerProps) { - const renderContent = () => { - switch (currentView) { - case View.Workspace: - return ; - case View.Executable: - if (selectedExecutable) { - if (executableError) { - console.error(executableError); - return ( - - Error loading executable: {executableError.message} - - ); - } - return ; - } - return ( - - ); - case View.Welcome: - return ; - case View.Logs: - return Logs view coming soon...; - case View.Data: - return ; - case View.Settings: - return ; - default: - return ; - } - }; - - return ( - - {renderContent()} - - ); -} diff --git a/desktop/src/layout/index.ts b/desktop/src/layout/index.ts index db40cc3d..e2ab3558 100644 --- a/desktop/src/layout/index.ts +++ b/desktop/src/layout/index.ts @@ -1,5 +1,3 @@ export { AppShell } from "./AppShell/AppShell"; export { Header } from "./Header/Header"; export { Sidebar } from "./Sidebar/Sidebar"; -export { Viewer, View } from "./Viewer/Viewer"; -export { ViewLinks } from "./Viewer/ViewLinks"; \ No newline at end of file diff --git a/desktop/src/pages/Data/Data.tsx b/desktop/src/pages/Data/Data.tsx index 855b4ec4..382fc368 100644 --- a/desktop/src/pages/Data/Data.tsx +++ b/desktop/src/pages/Data/Data.tsx @@ -1,19 +1,22 @@ import { Tabs } from "@mantine/core"; import { IconBraces, IconLock } from "@tabler/icons-react"; +import { PageWrapper } from "../../components/PageWrapper.tsx"; -export default function Data() { +export function Data() { return ( - - - }> - Cache - - }> - Vault - - - Cache data should show here - Vault data should show here - + + + + }> + Cache + + }> + Vault + + + Cache data should show here + Vault data should show here + + ); } diff --git a/desktop/src/pages/Executable/Executable.tsx b/desktop/src/pages/Executable/Executable.tsx index b3618d13..54b63e4c 100644 --- a/desktop/src/pages/Executable/Executable.tsx +++ b/desktop/src/pages/Executable/Executable.tsx @@ -96,7 +96,7 @@ function getVisibilityColor(visibility?: string) { } } -export default function Executable({ executable }: ExecutableProps) { +export function Executable({ executable }: ExecutableProps) { const typeInfo = getExecutableTypeInfo(executable); const { settings } = useSettings(); const { setNotification } = useNotifier(); @@ -108,10 +108,8 @@ export default function Executable({ executable }: ExecutableProps) { let unlistenComplete: (() => void) | undefined; const setupListeners = async () => { - console.log("Setting up listeners for executable:", executable.ref); unlistenOutput = await listen("command-output", (event) => { const payload = event.payload as LogLine; - console.log("Received output:", payload); setOutput((prev) => [...prev, payload]); }); @@ -146,7 +144,6 @@ export default function Executable({ executable }: ExecutableProps) { const onOpenFile = async () => { try { - console.log(executable.flowfile); await openPath(executable.flowfile, settings.executableApp || undefined); } catch (error) { console.error(error); @@ -155,7 +152,7 @@ export default function Executable({ executable }: ExecutableProps) { const onExecute = async () => { const hasPromptParams = executable.exec?.params?.some( - (param) => param.prompt + (param) => param.prompt, ); const hasArgs = executable.exec?.args && executable.exec.args.length > 0; @@ -183,7 +180,12 @@ export default function Executable({ executable }: ExecutableProps) { ? formData.args.trim().split(/\s+/) : []; - const invokeParams: any = { + const invokeParams: { + verb: string; + executableId: string; + args: string[]; + params?: Record; + } = { verb: executable.verb, executableId: executable.id, args: argsArray, diff --git a/desktop/src/pages/Executable/ExecutableEnvironmentDetails.tsx b/desktop/src/pages/Executable/ExecutableEnvironmentDetails.tsx index 1e2b1f52..08f85e24 100644 --- a/desktop/src/pages/Executable/ExecutableEnvironmentDetails.tsx +++ b/desktop/src/pages/Executable/ExecutableEnvironmentDetails.tsx @@ -96,7 +96,7 @@ export function ExecutableEnvironmentDetails({ ); - } + }, )} diff --git a/desktop/src/pages/Executable/ExecutableRoute.tsx b/desktop/src/pages/Executable/ExecutableRoute.tsx new file mode 100644 index 00000000..1783f752 --- /dev/null +++ b/desktop/src/pages/Executable/ExecutableRoute.tsx @@ -0,0 +1,31 @@ +import { Text, LoadingOverlay } from "@mantine/core"; +import { useParams } from "wouter"; +import { useExecutable } from "../../hooks/useExecutable"; +import { PageWrapper } from "../../components/PageWrapper.tsx"; +import { Welcome } from "../Welcome/Welcome"; +import { Executable } from "./Executable"; + +export function ExecutableRoute() { + const params = useParams(); + const executableId = decodeURIComponent(params.executableId || ""); + const { executable, executableError, isExecutableLoading } = + useExecutable(executableId); + + return ( + + {isExecutableLoading && ( + + )} + {executableError && Error: {executableError.message}} + {executable ? ( + + ) : ( + + )} + + ); +} diff --git a/desktop/src/pages/Executable/ExecutionForm.tsx b/desktop/src/pages/Executable/ExecutionForm.tsx index 7385db96..9df854be 100644 --- a/desktop/src/pages/Executable/ExecutionForm.tsx +++ b/desktop/src/pages/Executable/ExecutionForm.tsx @@ -44,7 +44,6 @@ export function ExecutionForm({ const args = executable.exec?.args || []; const handleSubmit = () => { - console.log("Form submitted with data:", formData); onSubmit(formData); onClose(); }; @@ -129,11 +128,11 @@ export function ExecutionForm({ { const target = event.target as HTMLInputElement; if (target) { - handleParamChange(param.envKey, target.value); + handleParamChange(param.envKey || "", target.value); } }} required={true} diff --git a/desktop/src/pages/Executable/types/ExecutableRequestDetails.tsx b/desktop/src/pages/Executable/types/ExecutableRequestDetails.tsx index 1baa2863..5b2526be 100644 --- a/desktop/src/pages/Executable/types/ExecutableRequestDetails.tsx +++ b/desktop/src/pages/Executable/types/ExecutableRequestDetails.tsx @@ -77,7 +77,7 @@ export function ExecutableRequestDetails({ {value}
- ) + ), )}
diff --git a/desktop/src/pages/Settings/Settings.tsx b/desktop/src/pages/Settings/Settings.tsx index 9fdb59f3..8cfe7d46 100644 --- a/desktop/src/pages/Settings/Settings.tsx +++ b/desktop/src/pages/Settings/Settings.tsx @@ -1,14 +1,14 @@ -import { - Select, +import { + Select, Stack, - TextInput, + TextInput, Title, LoadingOverlay, Alert, Paper, } from "@mantine/core"; import { IconInfoCircle } from "@tabler/icons-react"; -import { useConfig } from "../../hooks/useBackendData"; +import { useConfig } from "../../hooks/useConfig"; import { useSettings } from "../../hooks/useSettings"; import { useNotifier } from "../../hooks/useNotifier"; import { ThemeName } from "../../theme/types"; @@ -16,6 +16,7 @@ import { NotificationType } from "../../types/notification"; import { SettingRow, SettingSection } from "../../components/Settings"; import styles from "./Settings.module.css"; import { useState, useEffect } from "react"; +import { PageWrapper } from "../../components/PageWrapper"; const themeOptions = [ { value: "everforest", label: "Default" }, @@ -39,7 +40,7 @@ const logModeOptions = [ export function Settings() { const { settings, updateWorkspaceApp, updateExecutableApp, updateTheme } = - useSettings(); + useSettings(); const { config, isConfigLoading, @@ -53,18 +54,20 @@ export function Settings() { updateDefaultTimeout, } = useConfig(); const { setNotification } = useNotifier(); - const [namespaceInput, setNamespaceInput] = useState(''); + const [namespaceInput, setNamespaceInput] = useState(""); useEffect(() => { if (config) { - setNamespaceInput(config.currentNamespace || ''); + setNamespaceInput(config.currentNamespace || ""); } }, [config]); if (configError) { return (
- Settings + + Settings + }> Error loading configuration: {configError.message} @@ -74,254 +77,279 @@ export function Settings() { async function handleThemeChange(value: string) { updateTheme(value as ThemeName); - return updateConfigTheme(value).then(() => { - refreshConfig(); - setNotification({ - type: NotificationType.Success, - title: 'Theme updated', - message: 'Theme has been successfully updated', - autoClose: true, + return updateConfigTheme(value) + .then(() => { + refreshConfig(); + setNotification({ + type: NotificationType.Success, + title: "Theme updated", + message: "Theme has been successfully updated", + autoClose: true, + }); + }) + .catch((error) => { + setNotification({ + type: NotificationType.Error, + title: "Error updating theme", + message: error.message, + autoClose: true, + }); }); - }).catch((error) => { - setNotification({ - type: NotificationType.Error, - title: 'Error updating theme', - message: error.message, - autoClose: true, - }); - }); } async function handleLogModeChange(value: string) { - return updateLogMode(value).then(() => { - refreshConfig(); - setNotification({ - type: NotificationType.Success, - title: 'Log mode updated', - message: 'Log mode has been successfully updated', - autoClose: true, - }); - }).catch((error) => { - setNotification({ - type: NotificationType.Error, - title: 'Error updating log mode', - message: error.message, - autoClose: true, + return updateLogMode(value) + .then(() => { + refreshConfig(); + setNotification({ + type: NotificationType.Success, + title: "Log mode updated", + message: "Log mode has been successfully updated", + autoClose: true, + }); + }) + .catch((error) => { + setNotification({ + type: NotificationType.Error, + title: "Error updating log mode", + message: error.message, + autoClose: true, + }); }); - }); } - async function handleDefaultTimeoutChange(value: string){ - return updateDefaultTimeout(value).then(() => { - refreshConfig(); - setNotification({ - type: NotificationType.Success, - title: 'Default timeout updated', - message: 'Default timeout has been successfully updated', - autoClose: true, + async function handleDefaultTimeoutChange(value: string) { + return updateDefaultTimeout(value) + .then(() => { + refreshConfig(); + setNotification({ + type: NotificationType.Success, + title: "Default timeout updated", + message: "Default timeout has been successfully updated", + autoClose: true, + }); + }) + .catch((error) => { + setNotification({ + type: NotificationType.Error, + title: "Error updating default timeout", + message: error.message, + autoClose: true, + }); }); - }).catch((error) => { - setNotification({ - type: NotificationType.Error, - title: 'Error updating default timeout', - message: error.message, - autoClose: true, - }); - }); } - async function handleCurrentWorkspaceChange(value: string){ - return updateCurrentWorkspace(value).then(() => { - refreshConfig(); - setNotification({ - type: NotificationType.Success, - title: 'Current workspace updated', - message: 'Current workspace has been successfully updated', - autoClose: true, - }); - }).catch((error) => { - setNotification({ - type: NotificationType.Error, - title: 'Error updating current workspace', - message: error.message, - autoClose: true, + async function handleCurrentWorkspaceChange(value: string) { + return updateCurrentWorkspace(value) + .then(() => { + refreshConfig(); + setNotification({ + type: NotificationType.Success, + title: "Current workspace updated", + message: "Current workspace has been successfully updated", + autoClose: true, + }); + }) + .catch((error) => { + setNotification({ + type: NotificationType.Error, + title: "Error updating current workspace", + message: error.message, + autoClose: true, + }); }); - }); } - async function handleWorkspaceModeChange(value: string){ - return updateWorkspaceMode(value).then(() => { - refreshConfig(); - setNotification({ - type: NotificationType.Success, - title: 'Workspace mode updated', - message: 'Workspace mode has been successfully updated', - autoClose: true, + async function handleWorkspaceModeChange(value: string) { + return updateWorkspaceMode(value) + .then(() => { + refreshConfig(); + setNotification({ + type: NotificationType.Success, + title: "Workspace mode updated", + message: "Workspace mode has been successfully updated", + autoClose: true, + }); + }) + .catch((error) => { + setNotification({ + type: NotificationType.Error, + title: "Error updating workspace mode", + message: error.message, + autoClose: true, + }); }); - }).catch((error) => { - setNotification({ - type: NotificationType.Error, - title: 'Error updating workspace mode', - message: error.message, - autoClose: true, - }); - }); } - async function handleNamespaceChange(value: string){ - return updateNamespace(value).then(() => { - refreshConfig(); - setNotification({ - type: NotificationType.Success, - title: 'Namespace updated', - message: 'Namespace has been successfully updated', - autoClose: true, - }); - }).catch((error) => { - setNotification({ - type: NotificationType.Error, - title: 'Error updating namespace', - message: error.message, - autoClose: true, + async function handleNamespaceChange(value: string) { + return updateNamespace(value) + .then(() => { + refreshConfig(); + setNotification({ + type: NotificationType.Success, + title: "Namespace updated", + message: "Namespace has been successfully updated", + autoClose: true, + }); + }) + .catch((error) => { + setNotification({ + type: NotificationType.Error, + title: "Error updating namespace", + message: error.message, + autoClose: true, + }); }); - }); } function handleNamespaceSubmit() { - if (namespaceInput !== (config?.currentNamespace || '')) { + if (namespaceInput !== (config?.currentNamespace || "")) { handleNamespaceChange(namespaceInput); } } return ( -
- - - - Settings - + +
+ - - - - value && handleLogModeChange(value)} - data={logModeOptions} - variant="filled" - /> - - - - handleDefaultTimeoutChange(e.currentTarget.value)} - placeholder="e.g., 30s, 5m, 1h" - variant="filled" - /> - - + + + + value && handleCurrentWorkspaceChange(value)} - data={Object.keys(config?.workspaces || {}).map(name => ({ value: name, label: name }))} - placeholder="Select workspace" - variant="filled" - /> - - - - value && handleLogModeChange(value)} + data={logModeOptions} + variant="filled" + /> + - - setNamespaceInput(e.currentTarget.value)} - onBlur={handleNamespaceSubmit} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleNamespaceSubmit(); + + + handleDefaultTimeoutChange(e.currentTarget.value) } - }} - placeholder="Enter namespace" - variant="filled" - spellCheck={false} - /> - - + placeholder="e.g., 30s, 5m, 1h" + variant="filled" + /> + + - - - updateWorkspaceApp(event.currentTarget.value)} - placeholder="System default" - variant="filled" - spellCheck={false} - /> - - - - updateExecutableApp(event.currentTarget.value)} - placeholder="System default" - variant="filled" - spellCheck={false} - /> - - - -
+ + + value && handleWorkspaceModeChange(value)} + data={workspaceModeOptions} + variant="filled" + /> + + + + setNamespaceInput(e.currentTarget.value)} + onBlur={handleNamespaceSubmit} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleNamespaceSubmit(); + } + }} + placeholder="Enter namespace" + variant="filled" + spellCheck={false} + /> + + + + + + + updateWorkspaceApp(event.currentTarget.value) + } + placeholder="System default" + variant="filled" + spellCheck={false} + /> + + + + + updateExecutableApp(event.currentTarget.value) + } + placeholder="System default" + variant="filled" + spellCheck={false} + /> + + + +
+ ); } diff --git a/desktop/src/pages/Workspace/Workspace.tsx b/desktop/src/pages/Workspace/Workspace.tsx index 41a2080d..c9af9765 100644 --- a/desktop/src/pages/Workspace/Workspace.tsx +++ b/desktop/src/pages/Workspace/Workspace.tsx @@ -150,7 +150,7 @@ export function Workspace({ workspace }: WorkspaceProps) { > {path} - ) + ), )}
@@ -167,7 +167,7 @@ export function Workspace({ workspace }: WorkspaceProps) { {path} - ) + ), )}
diff --git a/desktop/src/pages/Workspace/WorkspaceRoute.tsx b/desktop/src/pages/Workspace/WorkspaceRoute.tsx new file mode 100644 index 00000000..d9e42c8a --- /dev/null +++ b/desktop/src/pages/Workspace/WorkspaceRoute.tsx @@ -0,0 +1,31 @@ +import { useParams } from "wouter"; +import { useWorkspace } from "../../hooks/useWorkspace"; +import { LoadingOverlay, Text } from "@mantine/core"; +import { PageWrapper } from "../../components/PageWrapper.tsx"; +import { Workspace } from "./Workspace"; +import { Welcome } from "../Welcome/Welcome"; + +export function WorkspaceRoute() { + const { workspaceName } = useParams(); + const { workspace, workspaceError, isWorkspaceLoading } = useWorkspace( + workspaceName || "", + ); + + return ( + + {isWorkspaceLoading && ( + + )} + {workspaceError && Error: {workspaceError.message}} + {workspace ? ( + + ) : ( + + )} + + ); +} diff --git a/desktop/src/pages/index.ts b/desktop/src/pages/index.ts index 92f7ef45..f0fa152c 100644 --- a/desktop/src/pages/index.ts +++ b/desktop/src/pages/index.ts @@ -1,5 +1,5 @@ -export { default as Data } from "./Data/Data"; -export { default as ExecutableInfo } from "./Executable/Executable"; +export { Data } from "./Data/Data"; +export { Executable } from "./Executable/Executable"; export { Settings } from "./Settings/Settings"; export { Welcome } from "./Welcome/Welcome"; -export { Workspace } from "./Workspace/Workspace"; \ No newline at end of file +export { Workspace } from "./Workspace/Workspace"; diff --git a/desktop/src/main.tsx b/desktop/src/root.tsx similarity index 65% rename from desktop/src/main.tsx rename to desktop/src/root.tsx index eb9d142b..116928c6 100644 --- a/desktop/src/main.tsx +++ b/desktop/src/root.tsx @@ -1,6 +1,9 @@ import ReactDOM from "react-dom/client"; import App from "./App"; +import { Layout } from "./layout/Layout"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + + + , ); diff --git a/desktop/src/theme/HeadScripts.tsx b/desktop/src/theme/HeadScripts.tsx new file mode 100644 index 00000000..11e41ac7 --- /dev/null +++ b/desktop/src/theme/HeadScripts.tsx @@ -0,0 +1,20 @@ +import { ColorSchemeScript } from "@mantine/core"; +import { createPortal } from "react-dom"; +import { useEffect, useState } from "react"; + +export function HeadScripts() { + const [head, setHead] = useState(null); + + useEffect(() => { + setHead(document.head); + }, []); + + if (!head) return null; + + return createPortal( + <> + + , + head, + ); +} diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts index f74d1b2d..34758c15 100644 --- a/desktop/vite.config.ts +++ b/desktop/vite.config.ts @@ -1,7 +1,6 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; -// @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; // https://vitejs.dev/config/