diff --git a/workspaces/frontend/README.md b/workspaces/frontend/README.md index b43fbf1d..c2477b62 100644 --- a/workspaces/frontend/README.md +++ b/workspaces/frontend/README.md @@ -77,3 +77,25 @@ Automatically fix linting issues: ```bash npm run test:fix ``` + +### API Types & Client Generation + +The TypeScript types and the HTTP client layer for interacting with the backend APIs are automatically generated from the backend's `swagger.json` file. This ensures the frontend remains aligned with the backend API contract at all times. + +#### Generated Code Location + +All generated files live in the `src/generated` directory. + +⚠️ Do not manually edit any files in this folder. + +#### Updating the Generated Code + +To update the generated code, first update the `swagger.version` file in the `scripts` directory to the desired commit hash of the backend's `swagger.json` file. + +Then run the following command to update the generated code: + +```bash +npm run generate:api +``` + +Finally, make any necessary adaptations based on the changes in the generated code. \ No newline at end of file diff --git a/workspaces/frontend/package-lock.json b/workspaces/frontend/package-lock.json index 3e2c72b9..5687c07d 100644 --- a/workspaces/frontend/package-lock.json +++ b/workspaces/frontend/package-lock.json @@ -18,6 +18,7 @@ "@patternfly/react-table": "^6.2.0", "@patternfly/react-tokens": "^6.2.0", "@types/js-yaml": "^4.0.9", + "axios": "^1.10.0", "date-fns": "^4.1.0", "eslint-plugin-local-rules": "^3.0.2", "js-yaml": "^4.1.0", @@ -109,7 +110,8 @@ "eslint-plugin-no-relative-import-paths": "^1.5.2", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.0.0" + "eslint-plugin-react-hooks": "^5.0.0", + "swagger-typescript-api": "^13.2.7" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1950,6 +1952,34 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@biomejs/js-api": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@biomejs/js-api/-/js-api-1.0.0.tgz", + "integrity": "sha512-69OfQ7+09AtiCIg+k+aU3rEsGit5o/SJWCS3BeBH/2nJYdJGi0cIx+ybka8i1EK69aNcZxYO1y1iAAEmYMq1HA==", + "optional": true, + "peerDependencies": { + "@biomejs/wasm-bundler": "^2.0.0", + "@biomejs/wasm-nodejs": "^2.0.0", + "@biomejs/wasm-web": "^2.0.0" + }, + "peerDependenciesMeta": { + "@biomejs/wasm-bundler": { + "optional": true + }, + "@biomejs/wasm-nodejs": { + "optional": true + }, + "@biomejs/wasm-web": { + "optional": true + } + } + }, + "node_modules/@biomejs/wasm-nodejs": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@biomejs/wasm-nodejs/-/wasm-nodejs-2.0.5.tgz", + "integrity": "sha512-pihpBMylewgDdGFZHRkgmc3OajuGIJPXhvfYuKCNK/CWyJMrYEFmPKs8Iq1kY0sYMmGlTbD4K2udV03KYa+r0Q==", + "optional": true + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3088,6 +3118,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "optional": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -5734,6 +5770,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/swagger-schema-official": { + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/@types/swagger-schema-official/-/swagger-schema-official-2.0.25.tgz", + "integrity": "sha512-T92Xav+Gf/Ik1uPW581nA+JftmjWPgskw/WBf4TJzxRG/SJ+DfNnNE+WuZ4mrXuzflQMqMkm1LSYjzYW7MB1Cg==", + "optional": true + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.8", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.8.tgz", @@ -6656,8 +6698,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -6719,6 +6760,21 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -7415,6 +7471,62 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.0.4.tgz", + "integrity": "sha512-t5FaZTYbbCtvxuZq9xxIruYydrAGsJ+8UdP0pZzMiK2xl/gNiSOy0OxhLzHUEEb0m1QXYqfzfvyIFEmz/g9lqg==", + "optional": true, + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.5.0", + "exsolve": "^1.0.5", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.1.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "optional": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "optional": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/cachedir": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", @@ -7501,6 +7613,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "optional": true + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7818,6 +7936,15 @@ "node": ">=8" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "optional": true, + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", @@ -7945,7 +8072,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, + "devOptional": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -8023,7 +8150,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -8263,6 +8389,12 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "optional": true + }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -8272,6 +8404,15 @@ "node": ">=0.8" } }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "optional": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/console-clear": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/console-clear/-/console-clear-1.1.1.tgz", @@ -9780,11 +9921,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "optional": true + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -9807,6 +9953,12 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "optional": true + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -10014,11 +10166,10 @@ } }, "node_modules/dotenv": { - "version": "16.4.6", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.6.tgz", - "integrity": "sha512-JhcR/+KIjkkjiU8yEpaB/USlzVi3i5whwOjpIRNGi9svKEXZSe+Qp6IWAjFjv+2GViAoDRCUv/QLNziQxsLqDg==", - "dev": true, - "license": "BSD-2-Clause", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "devOptional": true, "engines": { "node": ">=12" }, @@ -10418,11 +10569,17 @@ "dev": true, "license": "MIT" }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "optional": true + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -11279,6 +11436,18 @@ "node": ">=0.10.0" } }, + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "optional": true, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -11482,6 +11651,12 @@ "node": ">= 0.8" } }, + "node_modules/exsolve": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", + "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", + "optional": true + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -11599,6 +11774,12 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "devOptional": true }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "optional": true + }, "node_modules/fastest-levenshtein": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", @@ -11988,7 +12169,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -12234,7 +12414,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -12393,7 +12572,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, + "devOptional": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -12500,6 +12679,23 @@ "assert-plus": "^1.0.0" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "optional": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -13082,6 +13278,12 @@ "node": ">=0.10" } }, + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "optional": true + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -15841,6 +16043,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "optional": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16797,7 +17008,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -16806,7 +17016,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -17675,6 +17884,44 @@ "dev": true, "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "optional": true, + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz", + "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", + "optional": true + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -17704,6 +17951,15 @@ "node": ">=8" } }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "optional": true, + "dependencies": { + "es6-promise": "^3.2.1" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -18090,6 +18346,95 @@ "node": ">=6" } }, + "node_modules/nypm": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.0.tgz", + "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", + "optional": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "pathe": "^2.0.3", + "pkg-types": "^2.0.0", + "tinyexec": "^0.3.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "optional": true, + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "optional": true, + "dependencies": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "optional": true, + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "optional": true, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "optional": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -18222,6 +18567,12 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "optional": true + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -18582,6 +18933,12 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "optional": true + }, "node_modules/peek-readable": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", @@ -18602,6 +18959,12 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "optional": true + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -18670,6 +19033,17 @@ "node": ">=8" } }, + "node_modules/pkg-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", + "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", + "optional": true, + "dependencies": { + "confbox": "^0.2.1", + "exsolve": "^1.0.1", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -19561,6 +19935,16 @@ "node": ">=0.10.0" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "optional": true, + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -19808,6 +20192,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "optional": true, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -19985,7 +20378,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -20864,6 +21257,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "optional": true, + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "optional": true, + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "optional": true, + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "optional": true + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "optional": true, + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "optional": true + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -21647,6 +22094,85 @@ "node": ">= 10" } }, + "node_modules/swagger-schema-official": { + "version": "2.0.0-bab6bed", + "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", + "integrity": "sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA==", + "optional": true + }, + "node_modules/swagger-typescript-api": { + "version": "13.2.7", + "resolved": "https://registry.npmjs.org/swagger-typescript-api/-/swagger-typescript-api-13.2.7.tgz", + "integrity": "sha512-rfqqoRFpZJPl477M/snMJPM90EvI8WqhuUHSF5ecC2r/w376T29+QXNJFVPsJmbFu5rBc/8m3vhArtMctjONdw==", + "optional": true, + "dependencies": { + "@biomejs/js-api": "1.0.0", + "@biomejs/wasm-nodejs": "2.0.5", + "@types/swagger-schema-official": "^2.0.25", + "c12": "^3.0.4", + "citty": "^0.1.6", + "consola": "^3.4.2", + "eta": "^2.2.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "nanoid": "^5.1.5", + "swagger-schema-official": "2.0.0-bab6bed", + "swagger2openapi": "^7.0.8", + "typescript": "~5.8.3" + }, + "bin": { + "sta": "dist/cli.js", + "swagger-typescript-api": "dist/cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/swagger-typescript-api/node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "optional": true, + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "optional": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "boast": "boast.js", + "oas-validate": "oas-validate.js", + "swagger2openapi": "swagger2openapi.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -21874,6 +22400,12 @@ "node": ">=4" } }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "optional": true + }, "node_modules/tldts": { "version": "6.1.58", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.58.tgz", @@ -21969,6 +22501,12 @@ "node": ">= 4.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, "node_modules/tree-dump": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", @@ -22443,9 +22981,9 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -22812,6 +23350,12 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, "node_modules/webpack": { "version": "5.95.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", @@ -23381,6 +23925,16 @@ "node": ">=12" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -23571,7 +24125,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -23643,7 +24197,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -23658,7 +24212,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -23670,7 +24224,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true }, "node_modules/wrappy": { "version": "1.0.2", @@ -23784,7 +24338,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=10" @@ -23809,7 +24363,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, + "devOptional": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -23827,7 +24381,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=12" } diff --git a/workspaces/frontend/package.json b/workspaces/frontend/package.json index 4654bceb..a8e29468 100644 --- a/workspaces/frontend/package.json +++ b/workspaces/frontend/package.json @@ -19,6 +19,7 @@ "build:bundle-analyze": "webpack-bundle-analyzer ./bundle.stats.json", "build:clean": "rimraf ./dist", "build:prod": "webpack --config ./config/webpack.prod.js", + "generate:api": "./scripts/generate-api.sh && npm run prettier", "start:dev": "cross-env STYLE_THEME=$npm_config_theme webpack serve --hot --color --config ./config/webpack.dev.js", "start:dev:mock": "cross-env MOCK_API_ENABLED=true STYLE_THEME=$npm_config_theme npm run start:dev", "test": "run-s prettier:check test:lint test:unit test:cypress-ci", @@ -113,6 +114,7 @@ "@patternfly/react-table": "^6.2.0", "@patternfly/react-tokens": "^6.2.0", "@types/js-yaml": "^4.0.9", + "axios": "^1.10.0", "date-fns": "^4.1.0", "eslint-plugin-local-rules": "^3.0.2", "js-yaml": "^4.1.0", @@ -138,6 +140,7 @@ "eslint-plugin-no-relative-import-paths": "^1.5.2", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.0.0" + "eslint-plugin-react-hooks": "^5.0.0", + "swagger-typescript-api": "^13.2.7" } } diff --git a/workspaces/frontend/scripts/generate-api.sh b/workspaces/frontend/scripts/generate-api.sh new file mode 100755 index 00000000..c894fcd8 --- /dev/null +++ b/workspaces/frontend/scripts/generate-api.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +GENERATED_DIR="./src/generated" +HASH_FILE="./scripts/swagger.version" +SWAGGER_COMMIT_HASH=$(cat "$HASH_FILE") +SWAGGER_JSON_PATH="../backend/openapi/swagger.json" +TMP_SWAGGER=".tmp-swagger.json" + +if ! git cat-file -e "${SWAGGER_COMMIT_HASH}:${SWAGGER_JSON_PATH}"; then + echo "❌ Swagger file not found at commit $SWAGGER_COMMIT_HASH" + exit 1 +fi + +git show "${SWAGGER_COMMIT_HASH}:${SWAGGER_JSON_PATH}" >"$TMP_SWAGGER" + +swagger-typescript-api generate \ + -p "$TMP_SWAGGER" \ + -o "$GENERATED_DIR" \ + --extract-request-body \ + --responses \ + --clean-output \ + --axios \ + --unwrap-response-data \ + --modular + +rm "$TMP_SWAGGER" diff --git a/workspaces/frontend/scripts/swagger.version b/workspaces/frontend/scripts/swagger.version new file mode 100644 index 00000000..21ab894b --- /dev/null +++ b/workspaces/frontend/scripts/swagger.version @@ -0,0 +1 @@ +4f0a29dec0d3c9f0d0f02caab4dc84101bfef8b0 diff --git a/workspaces/frontend/src/__mocks__/mockNamespaces.ts b/workspaces/frontend/src/__mocks__/mockNamespaces.ts index 6265e681..2063b3f0 100644 --- a/workspaces/frontend/src/__mocks__/mockNamespaces.ts +++ b/workspaces/frontend/src/__mocks__/mockNamespaces.ts @@ -1,7 +1,7 @@ +import { NamespacesNamespace } from '~/generated/data-contracts'; import { buildMockNamespace } from '~/shared/mock/mockBuilder'; -import { Namespace } from '~/shared/api/backendApiTypes'; -export const mockNamespaces: Namespace[] = [ +export const mockNamespaces: NamespacesNamespace[] = [ buildMockNamespace({ name: 'default' }), buildMockNamespace({ name: 'kubeflow' }), buildMockNamespace({ name: 'custom-namespace' }), diff --git a/workspaces/frontend/src/__mocks__/utils.ts b/workspaces/frontend/src/__mocks__/utils.ts index d4af2e35..18538b43 100644 --- a/workspaces/frontend/src/__mocks__/utils.ts +++ b/workspaces/frontend/src/__mocks__/utils.ts @@ -1,5 +1,7 @@ -import { ResponseBody } from '~/shared/api/types'; +interface Envelope { + data: T; +} -export const mockBFFResponse = (data: T): ResponseBody => ({ +export const mockBFFResponse = (data: T): Envelope => ({ data, }); diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspace.mock.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspace.mock.ts index 0444639b..a3e2196c 100644 --- a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspace.mock.ts +++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspace.mock.ts @@ -1,17 +1,17 @@ -import { WorkspaceState } from '~/shared/api/backendApiTypes'; -import type { Workspace, WorkspaceKindInfo } from '~/shared/api/backendApiTypes'; +import type { WorkspacesWorkspace, WorkspacesWorkspaceKindInfo } from '~/generated/data-contracts'; +import { WorkspacesWorkspaceState } from '~/generated/data-contracts'; const generateMockWorkspace = ( name: string, namespace: string, - state: WorkspaceState, + state: WorkspacesWorkspaceState, paused: boolean, imageConfigId: string, imageConfigDisplayName: string, podConfigId: string, podConfigDisplayName: string, pvcName: string, -): Workspace => { +): WorkspacesWorkspace => { const pausedTime = new Date(2025, 0, 1).getTime(); const lastActivityTime = new Date(2025, 0, 2).getTime(); const lastUpdateTime = new Date(2025, 0, 3).getTime(); @@ -19,16 +19,16 @@ const generateMockWorkspace = ( return { name, namespace, - workspaceKind: { name: 'jupyterlab' } as WorkspaceKindInfo, + workspaceKind: { name: 'jupyterlab' } as WorkspacesWorkspaceKindInfo, deferUpdates: paused, paused, pausedTime, pendingRestart: Math.random() < 0.5, //to generate randomly True/False value state, stateMessage: - state === WorkspaceState.WorkspaceStateRunning + state === WorkspacesWorkspaceState.WorkspaceStateRunning ? 'Workspace is running smoothly.' - : state === WorkspaceState.WorkspaceStatePaused + : state === WorkspacesWorkspaceState.WorkspaceStatePaused ? 'Workspace is paused.' : 'Workspace is operational.', podTemplate: { @@ -104,11 +104,11 @@ const generateMockWorkspaces = (numWorkspaces: number, byNamespace = false) => { for (let i = 1; i <= numWorkspaces; i++) { const state = i % 3 === 0 - ? WorkspaceState.WorkspaceStateError + ? WorkspacesWorkspaceState.WorkspaceStateError : i % 2 === 0 - ? WorkspaceState.WorkspaceStatePaused - : WorkspaceState.WorkspaceStateRunning; - const paused = state === WorkspaceState.WorkspaceStatePaused; + ? WorkspacesWorkspaceState.WorkspaceStatePaused + : WorkspacesWorkspaceState.WorkspaceStateRunning; + const paused = state === WorkspacesWorkspaceState.WorkspaceStatePaused; const name = `workspace-${i}`; const namespace = namespaces[i % namespaces.length]; const pvcName = `data-pvc-${i}`; diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock.ts index 4d0a4861..d1dfa27e 100644 --- a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock.ts +++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock.ts @@ -1,7 +1,12 @@ -import type { WorkspaceKind } from '~/shared/api/backendApiTypes'; +import { + WorkspacekindsRedirectMessageLevel, + type WorkspacekindsWorkspaceKind, +} from '~/generated/data-contracts'; // Factory function to create a valid WorkspaceKind -function createMockWorkspaceKind(overrides: Partial = {}): WorkspaceKind { +function createMockWorkspaceKind( + overrides: Partial = {}, +): WorkspacekindsWorkspaceKind { return { name: 'jupyter-lab', displayName: 'JupyterLab Notebook', @@ -27,14 +32,15 @@ function createMockWorkspaceKind(overrides: Partial = {}): Worksp values: [ { id: 'jupyterlab_scipy_180', + description: 'JupyterLab with SciPy 1.8.0', displayName: 'jupyter-scipy:v1.8.0', - labels: { pythonVersion: '3.11' }, + labels: [{ key: 'pythonVersion', value: '3.11' }], hidden: true, redirect: { to: 'jupyterlab_scipy_190', message: { text: 'This update will change...', - level: 'Info', + level: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelInfo, }, }, }, @@ -45,9 +51,13 @@ function createMockWorkspaceKind(overrides: Partial = {}): Worksp values: [ { id: 'tiny_cpu', + hidden: false, displayName: 'Tiny CPU', description: 'Pod with 0.1 CPU, 128 Mb RAM', - labels: { cpu: '100m', memory: '128Mi' }, + labels: [ + { key: 'cpu', value: '100m' }, + { key: 'memory', value: '128Mi' }, + ], }, ], }, diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/Workspaces.cy.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/Workspaces.cy.ts index 35a29d3e..a593f4f6 100644 --- a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/Workspaces.cy.ts +++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/Workspaces.cy.ts @@ -1,14 +1,14 @@ -import type { Workspace } from '~/shared/api/backendApiTypes'; +import { mockNamespaces } from '~/__mocks__/mockNamespaces'; +import { mockBFFResponse } from '~/__mocks__/utils'; import { home } from '~/__tests__/cypress/cypress/pages/home'; import { mockWorkspaces, mockWorkspacesByNS, } from '~/__tests__/cypress/cypress/tests/mocked/workspace.mock'; -import { mockNamespaces } from '~/__mocks__/mockNamespaces'; -import { mockBFFResponse } from '~/__mocks__/utils'; +import type { WorkspacesWorkspace } from '~/generated/data-contracts'; // Helper function to validate the content of a single workspace row in the table -const validateWorkspaceRow = (workspace: Workspace, index: number) => { +const validateWorkspaceRow = (workspace: WorkspacesWorkspace, index: number) => { // Validate the workspace name cy.findByTestId(`workspace-row-${index}`) .find('[data-testid="workspace-name"]') diff --git a/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx b/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx index 40608d81..068f19d7 100644 --- a/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx +++ b/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx @@ -1,4 +1,7 @@ -import { WorkspaceKind, WorkspaceOptionRedirect } from '~/shared/api/backendApiTypes'; +import { + WorkspacekindsOptionRedirect, + WorkspacekindsWorkspaceKind, +} from '~/generated/data-contracts'; type KindLogoDict = Record; @@ -7,7 +10,9 @@ type KindLogoDict = Record; * @param {WorkspaceKind[]} workspaceKinds - The list of workspace kinds. * @returns {KindLogoDict} A dictionary with kind names as keys and logo URLs as values. */ -export function buildKindLogoDictionary(workspaceKinds: WorkspaceKind[] | []): KindLogoDict { +export function buildKindLogoDictionary( + workspaceKinds: WorkspacekindsWorkspaceKind[] | [], +): KindLogoDict { const kindLogoDict: KindLogoDict = {}; for (const workspaceKind of workspaceKinds) { @@ -20,7 +25,7 @@ export function buildKindLogoDictionary(workspaceKinds: WorkspaceKind[] | []): K return kindLogoDict; } -type WorkspaceRedirectStatus = Record; +type WorkspaceRedirectStatus = Record; /** * Builds a dictionary of workspace kinds to redirect statuses. @@ -28,7 +33,7 @@ type WorkspaceRedirectStatus = Record = ({ title, errors }) => { @@ -18,7 +17,9 @@ export const ValidationErrorAlert: React.FC = ({ titl {errors.map((error, index) => ( - {error.message} + + {error.message}: '{error.field}' + ))} diff --git a/workspaces/frontend/src/app/components/WorkspaceTable.tsx b/workspaces/frontend/src/app/components/WorkspaceTable.tsx index 44c86006..7c813f05 100644 --- a/workspaces/frontend/src/app/components/WorkspaceTable.tsx +++ b/workspaces/frontend/src/app/components/WorkspaceTable.tsx @@ -44,7 +44,6 @@ import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/esm/icons/ import { TimesCircleIcon } from '@patternfly/react-icons/dist/esm/icons/times-circle-icon'; import { QuestionCircleIcon } from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; import { formatDistanceToNow } from 'date-fns/formatDistanceToNow'; -import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes'; import { DataFieldKey, defineDataFields, SortableDataFieldKey } from '~/app/filterableDataHelper'; import { useTypedNavigate } from '~/app/routerHelper'; import { @@ -62,6 +61,7 @@ import { } from '~/shared/utilities/WorkspaceUtils'; import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow'; import CustomEmptyState from '~/shared/components/CustomEmptyState'; +import { WorkspacesWorkspace, WorkspacesWorkspaceState } from '~/generated/data-contracts'; const { fields: wsTableColumns, @@ -84,11 +84,11 @@ export type WorkspaceTableColumnKeys = DataFieldKey; type WorkspaceTableSortableColumnKeys = SortableDataFieldKey; interface WorkspaceTableProps { - workspaces: Workspace[]; + workspaces: WorkspacesWorkspace[]; canCreateWorkspaces?: boolean; canExpandRows?: boolean; hiddenColumns?: WorkspaceTableColumnKeys[]; - rowActions?: (workspace: Workspace) => IActions; + rowActions?: (workspace: WorkspacesWorkspace) => IActions; } const allFiltersConfig = { @@ -233,7 +233,7 @@ const WorkspaceTable = React.forwardRef( [clearAllFilters], ); - const filterableProperties: Record string> = useMemo( + const filterableProperties: Record string> = useMemo( () => ({ name: (ws) => ws.name, kind: (ws) => ws.workspaceKind.name, @@ -245,7 +245,7 @@ const WorkspaceTable = React.forwardRef( [], ); - const setWorkspaceExpanded = (workspace: Workspace, isExpanding = true) => + const setWorkspaceExpanded = (workspace: WorkspacesWorkspace, isExpanding = true) => setExpandedWorkspacesNames((prevExpanded) => { const newExpandedWorkspacesNames = prevExpanded.filter( (wsName) => wsName !== workspace.name, @@ -255,7 +255,7 @@ const WorkspaceTable = React.forwardRef( : newExpandedWorkspacesNames; }); - const isWorkspaceExpanded = (workspace: Workspace) => + const isWorkspaceExpanded = (workspace: WorkspacesWorkspace) => expandedWorkspacesNames.includes(workspace.name); const filteredWorkspaces = useMemo(() => { @@ -289,7 +289,7 @@ const WorkspaceTable = React.forwardRef( // Column sorting const getSortableRowValues = ( - workspace: Workspace, + workspace: WorkspacesWorkspace, ): Record => ({ name: workspace.name, kind: workspace.workspaceKind.name, @@ -374,19 +374,19 @@ const WorkspaceTable = React.forwardRef( } }; - const extractStateColor = (state: WorkspaceState) => { + const extractStateColor = (state: WorkspacesWorkspaceState) => { switch (state) { - case WorkspaceState.WorkspaceStateRunning: + case WorkspacesWorkspaceState.WorkspaceStateRunning: return 'green'; - case WorkspaceState.WorkspaceStatePending: + case WorkspacesWorkspaceState.WorkspaceStatePending: return 'orange'; - case WorkspaceState.WorkspaceStateTerminating: + case WorkspacesWorkspaceState.WorkspaceStateTerminating: return 'yellow'; - case WorkspaceState.WorkspaceStateError: + case WorkspacesWorkspaceState.WorkspaceStateError: return 'red'; - case WorkspaceState.WorkspaceStatePaused: + case WorkspacesWorkspaceState.WorkspaceStatePaused: return 'purple'; - case WorkspaceState.WorkspaceStateUnknown: + case WorkspacesWorkspaceState.WorkspaceStateUnknown: default: return 'grey'; } diff --git a/workspaces/frontend/src/app/context/WorkspaceActionsContext.tsx b/workspaces/frontend/src/app/context/WorkspaceActionsContext.tsx index 57349d35..75a2a7ee 100644 --- a/workspaces/frontend/src/app/context/WorkspaceActionsContext.tsx +++ b/workspaces/frontend/src/app/context/WorkspaceActionsContext.tsx @@ -8,11 +8,11 @@ import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails'; import { useTypedNavigate } from '~/app/routerHelper'; -import { Workspace } from '~/shared/api/backendApiTypes'; import DeleteModal from '~/shared/components/DeleteModal'; import { WorkspaceStartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal'; import { WorkspaceRestartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal'; import { WorkspaceStopActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; export enum ActionType { ViewDetails = 'ViewDetails', @@ -25,7 +25,7 @@ export enum ActionType { export interface WorkspaceAction { action: ActionType; - workspace: Workspace; + workspace: WorkspacesWorkspace; onActionDone?: () => void; } @@ -85,7 +85,8 @@ export const WorkspaceActionsContextProvider: React.FC (args: { workspace: Workspace; onActionDone?: () => void }) => + (actionType: ActionType) => + (args: { workspace: WorkspacesWorkspace; onActionDone?: () => void }) => setActiveWsAction({ action: actionType, ...args }); const requestViewDetailsAction = createActionRequester(ActionType.ViewDetails); @@ -113,7 +114,7 @@ export const WorkspaceActionsContextProvider: React.FC - api.pauseWorkspace({}, selectedNamespace, activeWsAction.workspace.name, { - data: { paused: false }, - }) + api.workspaces.updateWorkspacePauseState( + selectedNamespace, + activeWsAction.workspace.name, + { + data: { paused: false }, + }, + ) } onActionDone={activeWsAction.onActionDone} onUpdateAndStart={async () => { @@ -202,9 +207,13 @@ export const WorkspaceActionsContextProvider: React.FC - api.pauseWorkspace({}, selectedNamespace, activeWsAction.workspace.name, { - data: { paused: true }, - }) + api.workspaces.updateWorkspacePauseState( + selectedNamespace, + activeWsAction.workspace.name, + { + data: { paused: true }, + }, + ) } onActionDone={activeWsAction.onActionDone} onUpdateAndStop={async () => { diff --git a/workspaces/frontend/src/app/context/useNotebookAPIState.tsx b/workspaces/frontend/src/app/context/useNotebookAPIState.tsx index 6b2e512e..0fc313ac 100644 --- a/workspaces/frontend/src/app/context/useNotebookAPIState.tsx +++ b/workspaces/frontend/src/app/context/useNotebookAPIState.tsx @@ -1,103 +1,18 @@ import { useCallback } from 'react'; -import { NotebookAPIs } from '~/shared/api/notebookApi'; -import { - createWorkspace, - createWorkspaceKind, - deleteWorkspace, - deleteWorkspaceKind, - getHealthCheck, - getWorkspace, - getWorkspaceKind, - listAllWorkspaces, - listNamespaces, - listWorkspaceKinds, - listWorkspaces, - patchWorkspace, - patchWorkspaceKind, - pauseWorkspace, - updateWorkspace, - updateWorkspaceKind, -} from '~/shared/api/notebookService'; +import { NotebookApis, notebookApisImpl } from '~/shared/api/notebookApi'; import { APIState } from '~/shared/api/types'; import useAPIState from '~/shared/api/useAPIState'; -import { - mockCreateWorkspace, - mockCreateWorkspaceKind, - mockDeleteWorkspace, - mockDeleteWorkspaceKind, - mockGetHealthCheck, - mockGetWorkspace, - mockGetWorkspaceKind, - mockListAllWorkspaces, - mockListNamespaces, - mockListWorkspaceKinds, - mockListWorkspaces, - mockPatchWorkspace, - mockPatchWorkspaceKind, - mockPauseWorkspace, - mockUpdateWorkspace, - mockUpdateWorkspaceKind, -} from '~/shared/mock/mockNotebookService'; +import { mockNotebookApisImpl } from '~/shared/mock/mockNotebookApis'; -export type NotebookAPIState = APIState; +export type NotebookAPIState = APIState; const MOCK_API_ENABLED = process.env.WEBPACK_REPLACE__mockApiEnabled === 'true'; const useNotebookAPIState = ( hostPath: string | null, ): [apiState: NotebookAPIState, refreshAPIState: () => void] => { - const createApi = useCallback( - (path: string): NotebookAPIs => ({ - // Health - getHealthCheck: getHealthCheck(path), - // Namespace - listNamespaces: listNamespaces(path), - // Workspace - listAllWorkspaces: listAllWorkspaces(path), - listWorkspaces: listWorkspaces(path), - createWorkspace: createWorkspace(path), - getWorkspace: getWorkspace(path), - updateWorkspace: updateWorkspace(path), - patchWorkspace: patchWorkspace(path), - deleteWorkspace: deleteWorkspace(path), - pauseWorkspace: pauseWorkspace(path), - // WorkspaceKind - listWorkspaceKinds: listWorkspaceKinds(path), - createWorkspaceKind: createWorkspaceKind(path), - getWorkspaceKind: getWorkspaceKind(path), - patchWorkspaceKind: patchWorkspaceKind(path), - deleteWorkspaceKind: deleteWorkspaceKind(path), - updateWorkspaceKind: updateWorkspaceKind(path), - }), - [], - ); - - const createMockApi = useCallback( - (path: string): NotebookAPIs => ({ - // Health - getHealthCheck: mockGetHealthCheck(path), - // Namespace - listNamespaces: mockListNamespaces(path), - // Workspace - listAllWorkspaces: mockListAllWorkspaces(path), - listWorkspaces: mockListWorkspaces(path), - createWorkspace: mockCreateWorkspace(path), - getWorkspace: mockGetWorkspace(path), - updateWorkspace: mockUpdateWorkspace(path), - patchWorkspace: mockPatchWorkspace(path), - deleteWorkspace: mockDeleteWorkspace(path), - pauseWorkspace: mockPauseWorkspace(path), - // WorkspaceKind - listWorkspaceKinds: mockListWorkspaceKinds(path), - createWorkspaceKind: mockCreateWorkspaceKind(path), - getWorkspaceKind: mockGetWorkspaceKind(path), - patchWorkspaceKind: mockPatchWorkspaceKind(path), - deleteWorkspaceKind: mockDeleteWorkspaceKind(path), - updateWorkspaceKind: mockUpdateWorkspaceKind(path), - }), - [], - ); - + const createApi = useCallback((path: string) => notebookApisImpl(path), []); + const createMockApi = useCallback(() => mockNotebookApisImpl(), []); return useAPIState(hostPath, MOCK_API_ENABLED ? createMockApi : createApi); }; diff --git a/workspaces/frontend/src/app/hooks/__tests__/useWorkspaceCountPerKind.spec.tsx b/workspaces/frontend/src/app/hooks/__tests__/useWorkspaceCountPerKind.spec.tsx index ff740970..4e0ec931 100644 --- a/workspaces/frontend/src/app/hooks/__tests__/useWorkspaceCountPerKind.spec.tsx +++ b/workspaces/frontend/src/app/hooks/__tests__/useWorkspaceCountPerKind.spec.tsx @@ -3,13 +3,13 @@ import { renderHook } from '~/__tests__/unit/testUtils/hooks'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; import { useWorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; import { - Workspace, - WorkspaceImageConfigValue, - WorkspaceKind, - WorkspaceKindInfo, - WorkspacePodConfigValue, -} from '~/shared/api/backendApiTypes'; -import { NotebookAPIs } from '~/shared/api/notebookApi'; + WorkspacekindsImageConfigValue, + WorkspacekindsPodConfigValue, + WorkspacekindsWorkspaceKind, + WorkspacesWorkspace, + WorkspacesWorkspaceKindInfo, +} from '~/generated/data-contracts'; +import { NotebookApis } from '~/shared/api/notebookApi'; import { buildMockWorkspace, buildMockWorkspaceKind } from '~/shared/mock/mockBuilder'; jest.mock('~/app/hooks/useNotebookAPI', () => ({ @@ -18,7 +18,7 @@ jest.mock('~/app/hooks/useNotebookAPI', () => ({ const mockUseNotebookAPI = useNotebookAPI as jest.MockedFunction; -const baseWorkspaceKindInfoTest: WorkspaceKindInfo = { +const baseWorkspaceKindInfoTest: WorkspacesWorkspaceKindInfo = { name: 'jupyter', missing: false, icon: { url: '' }, @@ -31,7 +31,7 @@ const baseWorkspaceTest = buildMockWorkspace({ workspaceKind: baseWorkspaceKindInfoTest, }); -const baseImageConfigTest: WorkspaceImageConfigValue = { +const baseImageConfigTest: WorkspacekindsImageConfigValue = { id: 'image', displayName: 'Image', description: 'Test image', @@ -40,7 +40,7 @@ const baseImageConfigTest: WorkspaceImageConfigValue = { clusterMetrics: undefined, }; -const basePodConfigTest: WorkspacePodConfigValue = { +const basePodConfigTest: WorkspacekindsPodConfigValue = { id: 'podConfig', displayName: 'Pod Config', description: 'Test pod config', @@ -53,23 +53,34 @@ describe('useWorkspaceCountPerKind', () => { const mockListAllWorkspaces = jest.fn(); const mockListWorkspaceKinds = jest.fn(); - const mockApi: Partial = { - listAllWorkspaces: mockListAllWorkspaces, - listWorkspaceKinds: mockListWorkspaceKinds, + const mockApi: Partial = { + workspaces: { + listAllWorkspaces: mockListAllWorkspaces, + listWorkspacesByNamespace: jest.fn(), + createWorkspace: jest.fn(), + updateWorkspacePauseState: jest.fn(), + getWorkspace: jest.fn(), + deleteWorkspace: jest.fn(), + }, + workspaceKinds: { + listWorkspaceKinds: mockListWorkspaceKinds, + createWorkspaceKind: jest.fn(), + getWorkspaceKind: jest.fn(), + }, }; beforeEach(() => { jest.clearAllMocks(); mockUseNotebookAPI.mockReturnValue({ - api: mockApi as NotebookAPIs, + api: mockApi as NotebookApis, apiAvailable: true, refreshAllAPI: jest.fn(), }); }); it('should return empty object initially', () => { - mockListAllWorkspaces.mockResolvedValue([]); - mockListWorkspaceKinds.mockResolvedValue([]); + mockListAllWorkspaces.mockResolvedValue({ ok: true, data: [] }); + mockListWorkspaceKinds.mockResolvedValue({ ok: true, data: [] }); const { result } = renderHook(() => useWorkspaceCountPerKind()); @@ -79,7 +90,7 @@ describe('useWorkspaceCountPerKind', () => { }); it('should fetch and calculate workspace counts on mount', async () => { - const mockWorkspaces: Workspace[] = [ + const mockWorkspaces: WorkspacesWorkspace[] = [ { ...baseWorkspaceTest, name: 'workspace1', @@ -100,7 +111,7 @@ describe('useWorkspaceCountPerKind', () => { }, ]; - const mockWorkspaceKinds: WorkspaceKind[] = [ + const mockWorkspaceKinds: WorkspacekindsWorkspaceKind[] = [ buildMockWorkspaceKind({ name: 'jupyter1', clusterMetrics: { workspacesCount: 10 }, @@ -173,8 +184,8 @@ describe('useWorkspaceCountPerKind', () => { }), ]; - mockListAllWorkspaces.mockResolvedValue(mockWorkspaces); - mockListWorkspaceKinds.mockResolvedValue(mockWorkspaceKinds); + mockListAllWorkspaces.mockResolvedValue({ ok: true, data: mockWorkspaces }); + mockListWorkspaceKinds.mockResolvedValue({ ok: true, data: mockWorkspaceKinds }); const { result } = renderHook(() => useWorkspaceCountPerKind()); @@ -211,8 +222,8 @@ describe('useWorkspaceCountPerKind', () => { }); it('should handle missing cluster metrics gracefully', async () => { - const mockEmptyWorkspaces: Workspace[] = []; - const mockWorkspaceKinds: WorkspaceKind[] = [ + const mockEmptyWorkspaces: WorkspacesWorkspace[] = []; + const mockWorkspaceKinds: WorkspacekindsWorkspaceKind[] = [ buildMockWorkspaceKind({ name: 'no-metrics', clusterMetrics: undefined, @@ -251,8 +262,8 @@ describe('useWorkspaceCountPerKind', () => { }), ]; - mockListAllWorkspaces.mockResolvedValue(mockEmptyWorkspaces); - mockListWorkspaceKinds.mockResolvedValue(mockWorkspaceKinds); + mockListAllWorkspaces.mockResolvedValue({ ok: true, data: mockEmptyWorkspaces }); + mockListWorkspaceKinds.mockResolvedValue({ ok: true, data: mockWorkspaceKinds }); const { result } = renderHook(() => useWorkspaceCountPerKind()); @@ -290,7 +301,8 @@ describe('useWorkspaceCountPerKind', () => { }); it('should handle empty workspace kinds array', async () => { - mockListWorkspaceKinds.mockResolvedValue([]); + mockListAllWorkspaces.mockResolvedValue({ ok: true, data: [] }); + mockListWorkspaceKinds.mockResolvedValue({ ok: true, data: [] }); const { result } = renderHook(() => useWorkspaceCountPerKind()); @@ -300,7 +312,7 @@ describe('useWorkspaceCountPerKind', () => { }); it('should handle workspaces with no matching kinds', async () => { - const mockWorkspaces: Workspace[] = [baseWorkspaceTest]; + const mockWorkspaces: WorkspacesWorkspace[] = [baseWorkspaceTest]; const workspaceKind = buildMockWorkspaceKind({ name: 'nomatch', clusterMetrics: { workspacesCount: 0 }, @@ -320,10 +332,10 @@ describe('useWorkspaceCountPerKind', () => { }, }); - const mockWorkspaceKinds: WorkspaceKind[] = [workspaceKind]; + const mockWorkspaceKinds: WorkspacekindsWorkspaceKind[] = [workspaceKind]; - mockListAllWorkspaces.mockResolvedValue(mockWorkspaces); - mockListWorkspaceKinds.mockResolvedValue(mockWorkspaceKinds); + mockListAllWorkspaces.mockResolvedValue({ ok: true, data: mockWorkspaces }); + mockListWorkspaceKinds.mockResolvedValue({ ok: true, data: mockWorkspaceKinds }); const { result } = renderHook(() => useWorkspaceCountPerKind()); diff --git a/workspaces/frontend/src/app/hooks/useNamespaces.ts b/workspaces/frontend/src/app/hooks/useNamespaces.ts index 1f62afeb..0e160713 100644 --- a/workspaces/frontend/src/app/hooks/useNamespaces.ts +++ b/workspaces/frontend/src/app/hooks/useNamespaces.ts @@ -1,24 +1,24 @@ import { useCallback } from 'react'; +import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; +import { ApiNamespaceListEnvelope } from '~/generated/data-contracts'; import useFetchState, { FetchState, FetchStateCallbackPromise, } from '~/shared/utilities/useFetchState'; -import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; -import { Namespace } from '~/shared/api/backendApiTypes'; -const useNamespaces = (): FetchState => { +const useNamespaces = (): FetchState => { const { api, apiAvailable } = useNotebookAPI(); - const call = useCallback>( - (opts) => { - if (!apiAvailable) { - return Promise.reject(new Error('API not yet available')); - } + const call = useCallback< + FetchStateCallbackPromise + >(async () => { + if (!apiAvailable) { + throw new Error('API not yet available'); + } - return api.listNamespaces(opts); - }, - [api, apiAvailable], - ); + const envelope = await api.namespaces.listNamespaces(); + return envelope.data; + }, [api, apiAvailable]); return useFetchState(call, null); }; diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts b/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts index 3a5f766a..22c06d33 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts @@ -1,10 +1,13 @@ import { useEffect, useState } from 'react'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; -import { Workspace, WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceCountPerOption } from '~/app/types'; -import { NotebookAPIs } from '~/shared/api/notebookApi'; +import { WorkspacekindsWorkspaceKind, WorkspacesWorkspace } from '~/generated/data-contracts'; +import { NotebookApis } from '~/shared/api/notebookApi'; -export type WorkspaceCountPerKind = Record; +export type WorkspaceCountPerKind = Record< + WorkspacekindsWorkspaceKind['name'], + WorkspaceCountPerOption +>; export const useWorkspaceCountPerKind = (): WorkspaceCountPerKind => { const { api } = useNotebookAPI(); @@ -27,18 +30,18 @@ export const useWorkspaceCountPerKind = (): WorkspaceCountPerKind => { return workspaceCountPerKind; }; -async function loadWorkspaceCounts(api: NotebookAPIs): Promise { +async function loadWorkspaceCounts(api: NotebookApis): Promise { const [workspaces, workspaceKinds] = await Promise.all([ - api.listAllWorkspaces({}), - api.listWorkspaceKinds({}), + api.workspaces.listAllWorkspaces({}), + api.workspaceKinds.listWorkspaceKinds({}), ]); - return extractCountPerKind({ workspaceKinds, workspaces }); + return extractCountPerKind({ workspaceKinds: workspaceKinds.data, workspaces: workspaces.data }); } function extractCountByNamespace(args: { - kind: WorkspaceKind; - workspaces: Workspace[]; + kind: WorkspacekindsWorkspaceKind; + workspaces: WorkspacesWorkspace[]; }): WorkspaceCountPerOption['countByNamespace'] { const { kind, workspaces } = args; return workspaces.reduce( @@ -53,7 +56,7 @@ function extractCountByNamespace(args: { } function extractCountByImage( - workspaceKind: WorkspaceKind, + workspaceKind: WorkspacekindsWorkspaceKind, ): WorkspaceCountPerOption['countByImage'] { return workspaceKind.podTemplate.options.imageConfig.values.reduce< WorkspaceCountPerOption['countByImage'] @@ -64,7 +67,7 @@ function extractCountByImage( } function extractCountByPodConfig( - workspaceKind: WorkspaceKind, + workspaceKind: WorkspacekindsWorkspaceKind, ): WorkspaceCountPerOption['countByPodConfig'] { return workspaceKind.podTemplate.options.podConfig.values.reduce< WorkspaceCountPerOption['countByPodConfig'] @@ -74,13 +77,13 @@ function extractCountByPodConfig( }, {}); } -function extractTotalCount(workspaceKind: WorkspaceKind): number { +function extractTotalCount(workspaceKind: WorkspacekindsWorkspaceKind): number { return workspaceKind.clusterMetrics?.workspacesCount ?? 0; } function extractCountPerKind(args: { - workspaceKinds: WorkspaceKind[]; - workspaces: Workspace[]; + workspaceKinds: WorkspacekindsWorkspaceKind[]; + workspaces: WorkspacesWorkspace[]; }): WorkspaceCountPerKind { const { workspaceKinds, workspaces } = args; diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceFormData.ts b/workspaces/frontend/src/app/hooks/useWorkspaceFormData.ts index 4a741b7a..88b9acc6 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceFormData.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceFormData.ts @@ -26,48 +26,49 @@ const useWorkspaceFormData = (args: { const { namespace, workspaceName } = args; const { api, apiAvailable } = useNotebookAPI(); - const call = useCallback>( - async (opts) => { - if (!apiAvailable) { - throw new Error('API not yet available'); - } + const call = useCallback>(async () => { + if (!apiAvailable) { + throw new Error('API not yet available'); + } - if (!namespace || !workspaceName) { - return EMPTY_FORM_DATA; - } + if (!namespace || !workspaceName) { + return EMPTY_FORM_DATA; + } - const workspace = await api.getWorkspace(opts, namespace, workspaceName); - const workspaceKind = await api.getWorkspaceKind(opts, workspace.workspaceKind.name); - const imageConfig = workspace.podTemplate.options.imageConfig.current; - const podConfig = workspace.podTemplate.options.podConfig.current; + const workspaceEnvelope = await api.workspaces.getWorkspace(namespace, workspaceName); + const workspace = workspaceEnvelope.data; + const workspaceKindEnvelope = await api.workspaceKinds.getWorkspaceKind( + workspace.workspaceKind.name, + ); + const workspaceKind = workspaceKindEnvelope.data; + const imageConfig = workspace.podTemplate.options.imageConfig.current; + const podConfig = workspace.podTemplate.options.podConfig.current; - return { - kind: workspaceKind, - image: { - id: imageConfig.id, - displayName: imageConfig.displayName, - description: imageConfig.description, - hidden: false, - labels: [], - }, - podConfig: { - id: podConfig.id, - displayName: podConfig.displayName, - description: podConfig.description, - hidden: false, - labels: [], - }, - properties: { - workspaceName: workspace.name, - deferUpdates: workspace.deferUpdates, - volumes: workspace.podTemplate.volumes.data.map((volume) => ({ ...volume })), - secrets: workspace.podTemplate.volumes.secrets?.map((secret) => ({ ...secret })) ?? [], - homeDirectory: workspace.podTemplate.volumes.home?.mountPath ?? '', - }, - }; - }, - [api, apiAvailable, namespace, workspaceName], - ); + return { + kind: workspaceKind, + image: { + id: imageConfig.id, + displayName: imageConfig.displayName, + description: imageConfig.description, + hidden: false, + labels: [], + }, + podConfig: { + id: podConfig.id, + displayName: podConfig.displayName, + description: podConfig.description, + hidden: false, + labels: [], + }, + properties: { + workspaceName: workspace.name, + deferUpdates: workspace.deferUpdates, + volumes: workspace.podTemplate.volumes.data.map((volume) => ({ ...volume })), + secrets: workspace.podTemplate.volumes.secrets?.map((secret) => ({ ...secret })) ?? [], + homeDirectory: workspace.podTemplate.volumes.home?.mountPath ?? '', + }, + }; + }, [api, apiAvailable, namespace, workspaceName]); return useFetchState(call, EMPTY_FORM_DATA); }; diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts b/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts index 1c575b24..b5ebac44 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts @@ -4,21 +4,27 @@ import useFetchState, { FetchStateCallbackPromise, } from '~/shared/utilities/useFetchState'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; +import { ApiWorkspaceKindEnvelope } from '~/generated/data-contracts'; -const useWorkspaceKindByName = (kind: string): FetchState => { +const useWorkspaceKindByName = ( + kind: string | undefined, +): FetchState => { const { api, apiAvailable } = useNotebookAPI(); - const call = useCallback>( - (opts) => { - if (!apiAvailable) { - return Promise.reject(new Error('API not yet available')); - } + const call = useCallback< + FetchStateCallbackPromise + >(async () => { + if (!apiAvailable) { + return Promise.reject(new Error('API not yet available')); + } - return api.getWorkspaceKind(opts, kind); - }, - [api, apiAvailable, kind], - ); + if (!kind) { + return null; + } + + const envelope = await api.workspaceKinds.getWorkspaceKind(kind); + return envelope.data; + }, [api, apiAvailable, kind]); return useFetchState(call, null); }; diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts b/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts index d654bd92..99d64e9b 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts @@ -3,20 +3,23 @@ import useFetchState, { FetchState, FetchStateCallbackPromise, } from '~/shared/utilities/useFetchState'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; +import { + ApiWorkspaceKindListEnvelope, + WorkspacekindsWorkspaceKind, +} from '~/generated/data-contracts'; -const useWorkspaceKinds = (): FetchState => { +const useWorkspaceKinds = (): FetchState => { const { api, apiAvailable } = useNotebookAPI(); - const call = useCallback>( - (opts) => { - if (!apiAvailable) { - return Promise.reject(new Error('API not yet available')); - } - return api.listWorkspaceKinds(opts); - }, - [api, apiAvailable], - ); + const call = useCallback< + FetchStateCallbackPromise + >(async () => { + if (!apiAvailable) { + return Promise.reject(new Error('API not yet available')); + } + const envelope = await api.workspaceKinds.listWorkspaceKinds(); + return envelope.data; + }, [api, apiAvailable]); return useFetchState(call, []); }; diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceRowActions.ts b/workspaces/frontend/src/app/hooks/useWorkspaceRowActions.ts index 4787107e..1b58f774 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceRowActions.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceRowActions.ts @@ -1,25 +1,25 @@ import { useCallback } from 'react'; import { IActions } from '@patternfly/react-table/dist/esm/components/Table'; -import { Workspace } from '~/shared/api/backendApiTypes'; import { useWorkspaceActionsContext, WorkspaceAction } from '~/app/context/WorkspaceActionsContext'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; export type WorkspaceRowActionId = 'viewDetails' | 'edit' | 'delete' | 'start' | 'stop' | 'restart'; interface WorkspaceRowAction { id: WorkspaceRowActionId; onActionDone?: WorkspaceAction['onActionDone']; - isVisible?: boolean | ((workspace: Workspace) => boolean); + isVisible?: boolean | ((workspace: WorkspacesWorkspace) => boolean); } type WorkspaceRowActionItem = WorkspaceRowAction | { id: 'separator' }; export const useWorkspaceRowActions = ( actionsToInclude: WorkspaceRowActionItem[], -): ((workspace: Workspace) => IActions) => { +): ((workspace: WorkspacesWorkspace) => IActions) => { const actionsContext = useWorkspaceActionsContext(); return useCallback( - (workspace: Workspace): IActions => { + (workspace: WorkspacesWorkspace): IActions => { const actions: IActions = []; for (const item of actionsToInclude) { @@ -47,7 +47,7 @@ export const useWorkspaceRowActions = ( function buildAction( id: WorkspaceRowActionId, onActionDone: WorkspaceAction['onActionDone'] | undefined, - workspace: Workspace, + workspace: WorkspacesWorkspace, actionsContext: ReturnType, ): IActions[number] { const map: Record IActions[number]> = { diff --git a/workspaces/frontend/src/app/hooks/useWorkspaces.ts b/workspaces/frontend/src/app/hooks/useWorkspaces.ts index 998f586c..5ce9d320 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaces.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaces.ts @@ -4,21 +4,23 @@ import useFetchState, { FetchStateCallbackPromise, } from '~/shared/utilities/useFetchState'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; -import { Workspace } from '~/shared/api/backendApiTypes'; +import { ApiWorkspaceListEnvelope } from '~/generated/data-contracts'; -export const useWorkspacesByNamespace = (namespace: string): FetchState => { +export const useWorkspacesByNamespace = ( + namespace: string, +): FetchState => { const { api, apiAvailable } = useNotebookAPI(); - const call = useCallback>( - (opts) => { - if (!apiAvailable) { - return Promise.reject(new Error('API not yet available')); - } + const call = useCallback< + FetchStateCallbackPromise + >(async () => { + if (!apiAvailable) { + return Promise.reject(new Error('API not yet available')); + } - return api.listWorkspaces(opts, namespace); - }, - [api, apiAvailable, namespace], - ); + const envelope = await api.workspaces.listWorkspacesByNamespace(namespace); + return envelope.data; + }, [api, apiAvailable, namespace]); return useFetchState(call, []); }; @@ -28,34 +30,33 @@ export const useWorkspacesByKind = (args: { namespace?: string; imageId?: string; podConfigId?: string; -}): FetchState => { +}): FetchState => { const { kind, namespace, imageId, podConfigId } = args; const { api, apiAvailable } = useNotebookAPI(); - const call = useCallback>( - async (opts) => { - if (!apiAvailable) { - throw new Error('API not yet available'); - } - if (!kind) { - throw new Error('Workspace kind is required'); - } + const call = useCallback< + FetchStateCallbackPromise + >(async () => { + if (!apiAvailable) { + throw new Error('API not yet available'); + } + if (!kind) { + throw new Error('Workspace kind is required'); + } - const workspaces = await api.listAllWorkspaces(opts); + const envelope = await api.workspaces.listAllWorkspaces(); - return workspaces.filter((workspace) => { - const matchesKind = workspace.workspaceKind.name === kind; - const matchesNamespace = namespace ? workspace.namespace === namespace : true; - const matchesImage = imageId - ? workspace.podTemplate.options.imageConfig.current.id === imageId - : true; - const matchesPodConfig = podConfigId - ? workspace.podTemplate.options.podConfig.current.id === podConfigId - : true; + return envelope.data.filter((workspace) => { + const matchesKind = workspace.workspaceKind.name === kind; + const matchesNamespace = namespace ? workspace.namespace === namespace : true; + const matchesImage = imageId + ? workspace.podTemplate.options.imageConfig.current.id === imageId + : true; + const matchesPodConfig = podConfigId + ? workspace.podTemplate.options.podConfig.current.id === podConfigId + : true; - return matchesKind && matchesNamespace && matchesImage && matchesPodConfig; - }); - }, - [apiAvailable, api, kind, namespace, imageId, podConfigId], - ); + return matchesKind && matchesNamespace && matchesImage && matchesPodConfig; + }); + }, [apiAvailable, api, kind, namespace, imageId, podConfigId]); return useFetchState(call, []); }; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx index a9858f9c..5f52ea78 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/EditableLabels.tsx @@ -11,12 +11,12 @@ import inlineEditStyles from '@patternfly/react-styles/css/components/InlineEdit import { css } from '@patternfly/react-styles'; import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; import { TrashAltIcon } from '@patternfly/react-icons/dist/esm/icons/trash-alt-icon'; -import { WorkspaceOptionLabel } from '~/shared/api/backendApiTypes'; +import { WorkspacekindsOptionLabel } from '~/generated/data-contracts'; interface EditableRowInterface { - data: WorkspaceOptionLabel; - columnNames: ColumnNames; - saveChanges: (editedData: WorkspaceOptionLabel) => void; + data: WorkspacekindsOptionLabel; + columnNames: ColumnNames; + saveChanges: (editedData: WorkspacekindsOptionLabel) => void; ariaLabel: string; deleteRow: () => void; } @@ -70,8 +70,8 @@ const EditableRow: React.FC = ({ type ColumnNames = { [K in keyof T]: string }; interface EditableLabelsProps { - rows: WorkspaceOptionLabel[]; - setRows: (value: WorkspaceOptionLabel[]) => void; + rows: WorkspacekindsOptionLabel[]; + setRows: (value: WorkspacekindsOptionLabel[]) => void; title?: string; description?: string; buttonLabel?: string; @@ -84,7 +84,7 @@ export const EditableLabels: React.FC = ({ description, buttonLabel = 'Label', }) => { - const columnNames: ColumnNames = { + const columnNames: ColumnNames = { key: 'Key', value: 'Value', }; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx index c17a78c2..449b49ce 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindForm.tsx @@ -9,13 +9,14 @@ import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/ex import { EmptyState, EmptyStateBody } from '@patternfly/react-core/dist/esm/components/EmptyState'; import { ValidationErrorAlert } from '~/app/components/ValidationErrorAlert'; import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName'; -import { WorkspaceKind, ValidationError } from '~/shared/api/backendApiTypes'; import { useTypedNavigate, useTypedParams } from '~/app/routerHelper'; import { useCurrentRouteKey } from '~/app/hooks/useCurrentRouteKey'; import useGenericObjectState from '~/app/hooks/useGenericObjectState'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; import { WorkspaceKindFormData } from '~/app/types'; -import { ErrorEnvelopeException } from '~/shared/api/apiUtils'; +import { safeApiCall } from '~/shared/api/apiUtils'; +import { CONTENT_TYPE_KEY, ContentType } from '~/shared/utilities/const'; +import { ApiValidationError, WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; import { WorkspaceKindFileUpload } from './fileUpload/WorkspaceKindFileUpload'; import { WorkspaceKindFormProperties } from './properties/WorkspaceKindFormProperties'; import { WorkspaceKindFormImage } from './image/WorkspaceKindFormImage'; @@ -31,7 +32,7 @@ export enum WorkspaceKindFormView { export type ValidationStatus = 'success' | 'error' | 'default'; export type FormMode = 'edit' | 'create'; -const convertToFormData = (initialData: WorkspaceKind): WorkspaceKindFormData => { +const convertToFormData = (initialData: WorkspacekindsWorkspaceKind): WorkspaceKindFormData => { const { podTemplate, ...properties } = initialData; const { options, ...spec } = podTemplate; const { podConfig, imageConfig } = options; @@ -51,11 +52,12 @@ export const WorkspaceKindForm: React.FC = () => { const [isSubmitting, setIsSubmitting] = useState(false); const [validated, setValidated] = useState('default'); const mode: FormMode = useCurrentRouteKey() === 'workspaceKindCreate' ? 'create' : 'edit'; - const [specErrors, setSpecErrors] = useState<(ValidationError | ErrorEnvelopeException)[]>([]); + const [specErrors, setSpecErrors] = useState([]); - const { kind } = useTypedParams<'workspaceKindEdit'>(); - const [initialFormData, initialFormDataLoaded, initialFormDataError] = - useWorkspaceKindByName(kind); + const routeParams = useTypedParams<'workspaceKindEdit' | 'workspaceKindCreate'>(); + const [initialFormData, initialFormDataLoaded, initialFormDataError] = useWorkspaceKindByName( + routeParams?.kind, + ); const [data, setData, resetData, replaceData] = useGenericObjectState( initialFormData ? convertToFormData(initialFormData) : EMPTY_WORKSPACE_KIND_FORM_DATA, @@ -73,32 +75,45 @@ export const WorkspaceKindForm: React.FC = () => { // TODO: Complete handleCreate with API call to create a new WS kind try { if (mode === 'create') { - const newWorkspaceKind = await api.createWorkspaceKind({ directYAML: true }, yamlValue); - // TODO: alert user about success - console.info('New workspace kind created:', JSON.stringify(newWorkspaceKind)); - navigate('workspaceKinds'); + const createResult = await safeApiCall(() => + api.workspaceKinds.createWorkspaceKind(yamlValue, { + headers: { + [CONTENT_TYPE_KEY]: ContentType.YAML, + }, + }), + ); + + if (createResult.ok) { + // TODO: alert user about success + console.info('New workspace kind created:', JSON.stringify(createResult.data)); + navigate('workspaceKinds'); + } else { + const validationErrors = createResult.errorEnvelope.error.cause?.validation_errors; + if (validationErrors && validationErrors.length > 0) { + setSpecErrors((prev) => [...prev, ...validationErrors]); + setValidated('error'); + return; + } + // TODO: alert user about generic error with no validation errors + setValidated('error'); + console.error( + `Error while creating workspace kind: ${JSON.stringify(createResult.errorEnvelope)}`, + ); + } } // TODO: Finish when WSKind API is finalized // const updatedWorkspace = await api.updateWorkspaceKind({}, kind, { data: {} }); // console.info('Workspace Kind updated:', JSON.stringify(updatedWorkspace)); // navigate('workspaceKinds'); } catch (err) { - if (err instanceof ErrorEnvelopeException) { - const validationErrors = err.envelope.error?.cause?.validation_errors; - if (validationErrors && validationErrors.length > 0) { - setSpecErrors((prev) => [...prev, ...validationErrors]); - setValidated('error'); - return; - } - setSpecErrors((prev) => [...prev, err]); - setValidated('error'); - } - // TODO: alert user about error - console.error(`Error ${mode === 'edit' ? 'editing' : 'creating'} workspace kind: ${err}`); + // TODO: alert user about unexpected error + console.error( + `Unexpected error while ${mode === 'edit' ? 'editing' : 'creating'} workspace kind: ${err}`, + ); } finally { setIsSubmitting(false); } - }, [navigate, mode, api, yamlValue]); + }, [api, mode, navigate, yamlValue]); const canSubmit = useMemo( () => !isSubmitting && validated === 'success', diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable.tsx index c503c446..4d0ae134 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/WorkspaceKindFormPaginatedTable.tsx @@ -11,12 +11,11 @@ import { import { Radio } from '@patternfly/react-core/dist/esm/components/Radio'; import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown'; import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; - import { WorkspaceKindImageConfigValue } from '~/app/types'; -import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; +import { WorkspacekindsPodConfigValue } from '~/generated/data-contracts'; interface PaginatedTableProps { - rows: WorkspaceKindImageConfigValue[] | WorkspacePodConfigValue[]; + rows: WorkspaceKindImageConfigValue[] | WorkspacekindsPodConfigValue[]; defaultId: string; setDefaultId: (id: string) => void; handleEdit: (index: number) => void; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/helpers.ts b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/helpers.ts index 786670e7..f768d768 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/helpers.ts +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/helpers.ts @@ -1,5 +1,8 @@ import { ImagePullPolicy, WorkspaceKindImagePort, WorkspaceKindPodConfigValue } from '~/app/types'; -import { WorkspaceOptionLabel, WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; +import { + WorkspacekindsOptionLabel, + WorkspacekindsPodConfigValue, +} from '~/generated/data-contracts'; import { PodResourceEntry } from './podConfig/WorkspaceKindFormResource'; // Simple ID generator to avoid PatternFly dependency in tests @@ -79,7 +82,7 @@ export const emptyImage = { description: '', hidden: false, imagePullPolicy: ImagePullPolicy.IfNotPresent, - labels: [] as WorkspaceOptionLabel[], + labels: [] as WorkspacekindsOptionLabel[], image: '', ports: [ { @@ -94,7 +97,7 @@ export const emptyImage = { }, }; -export const emptyPodConfig: WorkspacePodConfigValue = { +export const emptyPodConfig: WorkspacekindsPodConfigValue = { id: '', displayName: '', description: '', diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImageRedirect.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImageRedirect.tsx index f250624f..62cad633 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImageRedirect.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/image/WorkspaceKindFormImageRedirect.tsx @@ -10,13 +10,13 @@ import { } from '@patternfly/react-core/dist/esm/components/FormSelect'; import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput'; import { - WorkspaceOptionRedirect, - WorkspaceRedirectMessageLevel, -} from '~/shared/api/backendApiTypes'; + WorkspacekindsOptionRedirect, + WorkspacekindsRedirectMessageLevel, +} from '~/generated/data-contracts'; interface WorkspaceKindFormImageRedirectProps { - redirect: WorkspaceOptionRedirect; - setRedirect: (obj: WorkspaceOptionRedirect) => void; + redirect: WorkspacekindsOptionRedirect; + setRedirect: (obj: WorkspacekindsOptionRedirect) => void; } export const WorkspaceKindFormImageRedirect: React.FC = ({ @@ -26,18 +26,18 @@ export const WorkspaceKindFormImageRedirect: React.FC getResources(currConfig), [currConfig]); const [resources, setResources] = useState(initialResources); - const [labels, setLabels] = useState(currConfig.labels); + const [labels, setLabels] = useState(currConfig.labels); const [id, setId] = useState(currConfig.id); const [displayName, setDisplayName] = useState(currConfig.displayName); const [description, setDescription] = useState(currConfig.description); diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podTemplate/WorkspaceKindFormPodTemplate.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podTemplate/WorkspaceKindFormPodTemplate.tsx index a6c69821..7246a881 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podTemplate/WorkspaceKindFormPodTemplate.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/Form/podTemplate/WorkspaceKindFormPodTemplate.tsx @@ -10,9 +10,9 @@ import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/comp import { Switch } from '@patternfly/react-core/dist/esm/components/Switch'; import { WorkspaceKindPodTemplateData } from '~/app/types'; import { EditableLabels } from '~/app/pages/WorkspaceKinds/Form/EditableLabels'; -import { WorkspacePodVolumeMount } from '~/shared/api/backendApiTypes'; import { ResourceInputWrapper } from '~/app/pages/WorkspaceKinds/Form/podConfig/ResourceInputWrapper'; import { WorkspaceFormPropertiesVolumes } from '~/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes'; +import { WorkspacesPodVolumeMount } from '~/generated/data-contracts'; interface WorkspaceKindFormPodTemplateProps { podTemplate: WorkspaceKindPodTemplateData; @@ -24,7 +24,7 @@ export const WorkspaceKindFormPodTemplate: React.FC { const [isExpanded, setIsExpanded] = useState(false); - const [volumes, setVolumes] = useState([]); + const [volumes, setVolumes] = useState([]); const toggleCullingEnabled = useCallback( (checked: boolean) => { @@ -42,7 +42,7 @@ export const WorkspaceKindFormPodTemplate: React.FC { + (newVolumes: WorkspacesPodVolumeMount[]) => { setVolumes(newVolumes); updatePodTemplate({ ...podTemplate, diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx index ea3bff29..3f212a17 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx @@ -36,7 +36,6 @@ import { IActions, } from '@patternfly/react-table/dist/esm/components/Table'; import { FilterIcon } from '@patternfly/react-icons/dist/esm/icons/filter-icon'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; import { useWorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; import { WorkspaceKindsColumns } from '~/app/types'; @@ -45,6 +44,7 @@ import CustomEmptyState from '~/shared/components/CustomEmptyState'; import WithValidImage from '~/shared/components/WithValidImage'; import ImageFallback from '~/shared/components/ImageFallback'; import { useTypedNavigate } from '~/app/routerHelper'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; import { WorkspaceKindDetails } from './details/WorkspaceKindDetails'; export enum ActionType { @@ -74,7 +74,8 @@ export const WorkspaceKinds: React.FunctionComponent = () => { }, [navigate]); const [workspaceKinds, workspaceKindsLoaded, workspaceKindsError] = useWorkspaceKinds(); const workspaceCountPerKind = useWorkspaceCountPerKind(); - const [selectedWorkspaceKind, setSelectedWorkspaceKind] = useState(null); + const [selectedWorkspaceKind, setSelectedWorkspaceKind] = + useState(null); const [activeActionType, setActiveActionType] = useState(null); // Column sorting @@ -82,7 +83,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => { const [activeSortDirection, setActiveSortDirection] = useState<'asc' | 'desc' | null>(null); const getSortableRowValues = useCallback( - (workspaceKind: WorkspaceKind): (string | boolean | number)[] => { + (workspaceKind: WorkspacekindsWorkspaceKind): (string | boolean | number)[] => { const { icon, name, @@ -151,7 +152,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => { }, []); const onFilter = useCallback( - (workspaceKind: WorkspaceKind) => { + (workspaceKind: WorkspacekindsWorkspaceKind) => { let nameRegex: RegExp; let descriptionRegex: RegExp; @@ -275,13 +276,13 @@ export const WorkspaceKinds: React.FunctionComponent = () => { // Actions - const viewDetailsClick = useCallback((workspaceKind: WorkspaceKind) => { + const viewDetailsClick = useCallback((workspaceKind: WorkspacekindsWorkspaceKind) => { setSelectedWorkspaceKind(workspaceKind); setActiveActionType(ActionType.ViewDetails); }, []); const workspaceKindsDefaultActions = useCallback( - (workspaceKind: WorkspaceKind): IActions => [ + (workspaceKind: WorkspacekindsWorkspaceKind): IActions => [ { id: 'view-details', title: 'View Details', diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetails.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetails.tsx index aaa7a150..23eb9155 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetails.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetails.tsx @@ -14,15 +14,15 @@ import { TabContent, } from '@patternfly/react-core/dist/esm/components/Tabs'; import { Title } from '@patternfly/react-core/dist/esm/components/Title'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; import { WorkspaceKindDetailsNamespaces } from '~/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsNamespaces'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; import { WorkspaceKindDetailsOverview } from './WorkspaceKindDetailsOverview'; import { WorkspaceKindDetailsImages } from './WorkspaceKindDetailsImages'; import { WorkspaceKindDetailsPodConfigs } from './WorkspaceKindDetailsPodConfigs'; type WorkspaceKindDetailsProps = { - workspaceKind: WorkspaceKind; + workspaceKind: WorkspacekindsWorkspaceKind; workspaceCountPerKind: WorkspaceCountPerKind; onCloseClick: React.MouseEventHandler; }; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsImages.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsImages.tsx index 6822982a..09e4cab3 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsImages.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsImages.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; import { WorkspaceKindDetailsTable } from './WorkspaceKindDetailsTable'; type WorkspaceDetailsImagesProps = { - workspaceKind: WorkspaceKind; + workspaceKind: WorkspacekindsWorkspaceKind; workspaceCountPerKind: WorkspaceCountPerKind; }; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsNamespaces.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsNamespaces.tsx index ca6c76fb..336bce1b 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsNamespaces.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsNamespaces.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; import { WorkspaceKindDetailsTable } from './WorkspaceKindDetailsTable'; type WorkspaceDetailsNamespacesProps = { - workspaceKind: WorkspaceKind; + workspaceKind: WorkspacekindsWorkspaceKind; workspaceCountPerKind: WorkspaceCountPerKind; }; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsOverview.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsOverview.tsx index 7a0b58d8..c5bf8c1f 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsOverview.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsOverview.tsx @@ -6,12 +6,12 @@ import { DescriptionListDescription, } from '@patternfly/react-core/dist/esm/components/DescriptionList'; import { Divider } from '@patternfly/react-core/dist/esm/components/Divider'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import ImageFallback from '~/shared/components/ImageFallback'; import WithValidImage from '~/shared/components/WithValidImage'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; type WorkspaceDetailsOverviewProps = { - workspaceKind: WorkspaceKind; + workspaceKind: WorkspacekindsWorkspaceKind; }; export const WorkspaceKindDetailsOverview: React.FunctionComponent< diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsPodConfigs.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsPodConfigs.tsx index 7ca13f4b..0abea53f 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsPodConfigs.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/details/WorkspaceKindDetailsPodConfigs.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; import { WorkspaceKindDetailsTable } from './WorkspaceKindDetailsTable'; type WorkspaceDetailsPodConfigsProps = { - workspaceKind: WorkspaceKind; + workspaceKind: WorkspacekindsWorkspaceKind; workspaceCountPerKind: WorkspaceCountPerKind; }; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/summary/WorkspaceKindSummaryExpandableCard.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/summary/WorkspaceKindSummaryExpandableCard.tsx index 6373c059..7c3a82f5 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/summary/WorkspaceKindSummaryExpandableCard.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/summary/WorkspaceKindSummaryExpandableCard.tsx @@ -12,7 +12,6 @@ import { Divider } from '@patternfly/react-core/dist/esm/components/Divider'; import { Flex, FlexItem } from '@patternfly/react-core/dist/esm/layouts/Flex'; import { Stack, StackItem } from '@patternfly/react-core/dist/esm/layouts/Stack'; import { t_global_spacer_md as MediumPadding } from '@patternfly/react-tokens'; -import { Workspace } from '~/shared/api/backendApiTypes'; import { countGpusFromWorkspaces, filterIdleWorkspacesWithGpu, @@ -20,11 +19,12 @@ import { groupWorkspacesByNamespaceAndGpu, YesNoValue, } from '~/shared/utilities/WorkspaceUtils'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; const TOP_GPU_CONSUMERS_LIMIT = 2; interface WorkspaceKindSummaryExpandableCardProps { - workspaces: Workspace[]; + workspaces: WorkspacesWorkspace[]; isExpanded: boolean; onExpandToggle: () => void; onAddFilter: (columnKey: string, value: string) => void; diff --git a/workspaces/frontend/src/app/pages/Workspaces/DataVolumesList.tsx b/workspaces/frontend/src/app/pages/Workspaces/DataVolumesList.tsx index d509e078..c9f27f69 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/DataVolumesList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/DataVolumesList.tsx @@ -15,10 +15,10 @@ import { List, ListItem } from '@patternfly/react-core/dist/esm/components/List' import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip'; import { DatabaseIcon } from '@patternfly/react-icons/dist/esm/icons/database-icon'; import { LockedIcon } from '@patternfly/react-icons/dist/esm/icons/locked-icon'; -import { Workspace } from '~/shared/api/backendApiTypes'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; interface DataVolumesListProps { - workspace: Workspace; + workspace: WorkspacesWorkspace; } export const DataVolumesList: React.FC = ({ workspace }) => { diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx index b5598a2f..9907f5d7 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx @@ -14,14 +14,14 @@ import { TabContent, } from '@patternfly/react-core/dist/esm/components/Tabs'; import { Title } from '@patternfly/react-core/dist/esm/components/Title'; -import { Workspace } from '~/shared/api/backendApiTypes'; import { WorkspaceDetailsOverview } from '~/app/pages/Workspaces/Details/WorkspaceDetailsOverview'; import { WorkspaceDetailsActions } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActions'; import { WorkspaceDetailsActivity } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActivity'; import { WorkspaceDetailsPodTemplate } from '~/app/pages/Workspaces/Details/WorkspaceDetailsPodTemplate'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; type WorkspaceDetailsProps = { - workspace: Workspace; + workspace: WorkspacesWorkspace; onCloseClick: React.MouseEventHandler; // TODO: Uncomment when edit action is fully supported // onEditClick: React.MouseEventHandler; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx index 17a4b411..f47c0729 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx @@ -7,12 +7,12 @@ import { DescriptionListDescription, } from '@patternfly/react-core/dist/esm/components/DescriptionList'; import { Divider } from '@patternfly/react-core/dist/esm/components/Divider'; -import { Workspace } from '~/shared/api/backendApiTypes'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; const DATE_FORMAT = 'PPpp'; type WorkspaceDetailsActivityProps = { - workspace: Workspace; + workspace: WorkspacesWorkspace; }; export const WorkspaceDetailsActivity: React.FunctionComponent = ({ diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx index 55f5ae92..af902b20 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx @@ -6,10 +6,10 @@ import { DescriptionListDescription, } from '@patternfly/react-core/dist/esm/components/DescriptionList'; import { Divider } from '@patternfly/react-core/dist/esm/components/Divider'; -import { Workspace } from '~/shared/api/backendApiTypes'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; type WorkspaceDetailsOverviewProps = { - workspace: Workspace; + workspace: WorkspacesWorkspace; }; export const WorkspaceDetailsOverview: React.FunctionComponent = ({ diff --git a/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx b/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx index 38c1b02a..f8edeb80 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table/dist/esm/components/Table'; -import { Workspace } from '~/shared/api/backendApiTypes'; import { WorkspaceTableColumnKeys } from '~/app/components/WorkspaceTable'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; import { WorkspaceStorage } from './WorkspaceStorage'; import { WorkspacePackageDetails } from './WorkspacePackageDetails'; import { WorkspaceConfigDetails } from './WorkspaceConfigDetails'; interface ExpandedWorkspaceRowProps { - workspace: Workspace; + workspace: WorkspacesWorkspace; visibleColumnKeys: WorkspaceTableColumnKeys[]; canExpandRows: boolean; } diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/WorkspaceForm.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/WorkspaceForm.tsx index ae39ca00..f4cdb8b2 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/WorkspaceForm.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/WorkspaceForm.tsx @@ -26,14 +26,14 @@ import { WorkspaceFormKindSelection } from '~/app/pages/Workspaces/Form/kind/Wor import { WorkspaceFormPodConfigSelection } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection'; import { WorkspaceFormPropertiesSelection } from '~/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSelection'; import { WorkspaceFormData } from '~/app/types'; -import { - WorkspaceCreate, - WorkspaceKind, - WorkspaceImageConfigValue, - WorkspacePodConfigValue, -} from '~/shared/api/backendApiTypes'; import useWorkspaceFormData from '~/app/hooks/useWorkspaceFormData'; import { useTypedNavigate } from '~/app/routerHelper'; +import { + WorkspacekindsImageConfigValue, + WorkspacekindsPodConfigValue, + WorkspacekindsWorkspaceKind, + WorkspacesWorkspaceCreate, +} from '~/generated/data-contracts'; import { useWorkspaceFormLocationData } from '~/app/hooks/useWorkspaceFormLocationData'; import { WorkspaceFormKindDetails } from '~/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails'; import { WorkspaceFormImageDetails } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails'; @@ -151,7 +151,7 @@ const WorkspaceForm: React.FC = () => { } // TODO: Prepare WorkspaceUpdate data accordingly when BE supports it - const submitData: WorkspaceCreate = { + const submitData: WorkspacesWorkspaceCreate = { name: data.properties.workspaceName, kind: data.kind.name, deferUpdates: data.properties.deferUpdates, @@ -177,15 +177,13 @@ const WorkspaceForm: React.FC = () => { try { if (mode === 'edit') { - const updateWorkspace = await api.updateWorkspace({}, submitData.name, namespace, { + // TODO: call api to update workspace when implemented in backend + } else { + const workspaceEnvelope = await api.workspaces.createWorkspace(namespace, { data: submitData, }); // TODO: alert user about success - console.info('Workspace updated:', JSON.stringify(updateWorkspace)); - } else { - const newWorkspace = await api.createWorkspace({}, namespace, { data: submitData }); - // TODO: alert user about success - console.info('New workspace created:', JSON.stringify(newWorkspace)); + console.info('New workspace created:', JSON.stringify(workspaceEnvelope.data)); } navigate('workspaces'); @@ -202,7 +200,7 @@ const WorkspaceForm: React.FC = () => { }, [navigate]); const handleKindSelect = useCallback( - (kind: WorkspaceKind | undefined) => { + (kind: WorkspacekindsWorkspaceKind | undefined) => { if (kind) { resetData(); setData('kind', kind); @@ -213,7 +211,7 @@ const WorkspaceForm: React.FC = () => { ); const handleImageSelect = useCallback( - (image: WorkspaceImageConfigValue | undefined) => { + (image: WorkspacekindsImageConfigValue | undefined) => { if (image) { setData('image', image); setDrawerExpanded(true); @@ -223,7 +221,7 @@ const WorkspaceForm: React.FC = () => { ); const handlePodConfigSelect = useCallback( - (podConfig: WorkspacePodConfigValue | undefined) => { + (podConfig: WorkspacekindsPodConfigValue | undefined) => { if (podConfig) { setData('podConfig', podConfig); setDrawerExpanded(true); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails.tsx index d47a85d2..d40c4581 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails.tsx @@ -6,11 +6,11 @@ import { DescriptionListDescription, } from '@patternfly/react-core/dist/esm/components/DescriptionList'; import { Title } from '@patternfly/react-core/dist/esm/components/Title'; -import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; import { formatLabelKey } from '~/shared/utilities/WorkspaceUtils'; +import { WorkspacekindsPodConfigValue } from '~/generated/data-contracts'; type WorkspaceFormImageDetailsProps = { - workspaceImage?: WorkspacePodConfigValue; + workspaceImage?: WorkspacekindsPodConfigValue; }; export const WorkspaceFormImageDetails: React.FunctionComponent = ({ diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageList.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageList.tsx index 08e39205..e36d1d5f 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageList.tsx @@ -9,9 +9,9 @@ import { Gallery } from '@patternfly/react-core/dist/esm/layouts/Gallery'; import { PageSection } from '@patternfly/react-core/dist/esm/components/Page'; import { Toolbar, ToolbarContent } from '@patternfly/react-core/dist/esm/components/Toolbar'; import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter'; -import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes'; import CustomEmptyState from '~/shared/components/CustomEmptyState'; import { defineDataFields, FilterableDataFieldKey } from '~/app/filterableDataHelper'; +import { WorkspacekindsImageConfigValue } from '~/generated/data-contracts'; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { fields, filterableLabelMap } = defineDataFields({ @@ -21,10 +21,10 @@ const { fields, filterableLabelMap } = defineDataFields({ type FilterableDataFieldKeys = FilterableDataFieldKey; type WorkspaceFormImageListProps = { - images: WorkspaceImageConfigValue[]; + images: WorkspacekindsImageConfigValue[]; selectedLabels: Map>; - selectedImage: WorkspaceImageConfigValue | undefined; - onSelect: (workspaceImage: WorkspaceImageConfigValue | undefined) => void; + selectedImage: WorkspacekindsImageConfigValue | undefined; + onSelect: (workspaceImage: WorkspacekindsImageConfigValue | undefined) => void; }; export const WorkspaceFormImageList: React.FunctionComponent = ({ @@ -37,7 +37,7 @@ export const WorkspaceFormImageList: React.FunctionComponent(null); const getFilteredWorkspaceImagesByLabels = useCallback( - (unfilteredImages: WorkspaceImageConfigValue[]) => + (unfilteredImages: WorkspacekindsImageConfigValue[]) => unfilteredImages.filter((image) => image.labels.reduce((accumulator, label) => { if (selectedLabels.has(label.key)) { diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection.tsx index aaf807c9..6123161f 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/image/WorkspaceFormImageSelection.tsx @@ -3,12 +3,12 @@ import { Content } from '@patternfly/react-core/dist/esm/components/Content'; import { Split, SplitItem } from '@patternfly/react-core/dist/esm/layouts/Split'; import { WorkspaceFormImageList } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageList'; import { FilterByLabels } from '~/app/pages/Workspaces/Form/labelFilter/FilterByLabels'; -import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes'; +import { WorkspacekindsImageConfigValue } from '~/generated/data-contracts'; interface WorkspaceFormImageSelectionProps { - images: WorkspaceImageConfigValue[]; - selectedImage: WorkspaceImageConfigValue | undefined; - onSelect: (image: WorkspaceImageConfigValue | undefined) => void; + images: WorkspacekindsImageConfigValue[]; + selectedImage: WorkspacekindsImageConfigValue | undefined; + onSelect: (image: WorkspacekindsImageConfigValue | undefined) => void; } const WorkspaceFormImageSelection: React.FunctionComponent = ({ diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails.tsx index 2363bf97..7a161efc 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindDetails.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Title } from '@patternfly/react-core/dist/esm/components/Title'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; type WorkspaceFormKindDetailsProps = { - workspaceKind?: WorkspaceKind; + workspaceKind?: WorkspacekindsWorkspaceKind; }; export const WorkspaceFormKindDetails: React.FunctionComponent = ({ diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindList.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindList.tsx index 8530bcab..bac0154f 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindList.tsx @@ -8,12 +8,12 @@ import { import { Gallery } from '@patternfly/react-core/dist/esm/layouts/Gallery'; import { PageSection } from '@patternfly/react-core/dist/esm/components/Page'; import { Toolbar, ToolbarContent } from '@patternfly/react-core/dist/esm/components/Toolbar'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter'; import CustomEmptyState from '~/shared/components/CustomEmptyState'; import ImageFallback from '~/shared/components/ImageFallback'; import WithValidImage from '~/shared/components/WithValidImage'; import { defineDataFields, FilterableDataFieldKey } from '~/app/filterableDataHelper'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { fields, filterableLabelMap } = defineDataFields({ @@ -23,9 +23,9 @@ const { fields, filterableLabelMap } = defineDataFields({ type FilterableDataFieldKeys = FilterableDataFieldKey; type WorkspaceFormKindListProps = { - allWorkspaceKinds: WorkspaceKind[]; - selectedKind: WorkspaceKind | undefined; - onSelect: (workspaceKind: WorkspaceKind | undefined) => void; + allWorkspaceKinds: WorkspacekindsWorkspaceKind[]; + selectedKind: WorkspacekindsWorkspaceKind | undefined; + onSelect: (workspaceKind: WorkspacekindsWorkspaceKind | undefined) => void; }; export const WorkspaceFormKindList: React.FunctionComponent = ({ diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindSelection.tsx index 9903abaf..30db818d 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/kind/WorkspaceFormKindSelection.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Content } from '@patternfly/react-core/dist/esm/components/Content'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; import { WorkspaceFormKindList } from '~/app/pages/Workspaces/Form/kind/WorkspaceFormKindList'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; interface WorkspaceFormKindSelectionProps { - selectedKind: WorkspaceKind | undefined; - onSelect: (kind: WorkspaceKind | undefined) => void; + selectedKind: WorkspacekindsWorkspaceKind | undefined; + onSelect: (kind: WorkspacekindsWorkspaceKind | undefined) => void; } const WorkspaceFormKindSelection: React.FunctionComponent = ({ diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/labelFilter/FilterByLabels.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/labelFilter/FilterByLabels.tsx index 6303747a..8ab9586e 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/labelFilter/FilterByLabels.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/labelFilter/FilterByLabels.tsx @@ -5,11 +5,11 @@ import { FilterSidePanelCategoryItem, } from '@patternfly/react-catalog-view-extension'; import '@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css'; -import { WorkspaceOptionLabel } from '~/shared/api/backendApiTypes'; import { formatLabelKey } from '~/shared/utilities/WorkspaceUtils'; +import { WorkspacesOptionLabel } from '~/generated/data-contracts'; type FilterByLabelsProps = { - labelledObjects: WorkspaceOptionLabel[]; + labelledObjects: WorkspacesOptionLabel[]; selectedLabels: Map>; onSelect: (labels: Map>) => void; }; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails.tsx index d3a9ba8e..fe829a4e 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigDetails.tsx @@ -7,11 +7,11 @@ import { } from '@patternfly/react-core/dist/esm/components/DescriptionList'; import { Title } from '@patternfly/react-core/dist/esm/components/Title'; import { Divider } from '@patternfly/react-core/dist/esm/components/Divider'; -import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; import { formatLabelKey } from '~/shared/utilities/WorkspaceUtils'; +import { WorkspacekindsPodConfigValue } from '~/generated/data-contracts'; type WorkspaceFormPodConfigDetailsProps = { - workspacePodConfig?: WorkspacePodConfigValue; + workspacePodConfig?: WorkspacekindsPodConfigValue; }; export const WorkspaceFormPodConfigDetails: React.FunctionComponent< diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigList.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigList.tsx index 06b9aa8b..7c200ec7 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigList.tsx @@ -8,10 +8,10 @@ import { import { Gallery } from '@patternfly/react-core/dist/esm/layouts/Gallery'; import { PageSection } from '@patternfly/react-core/dist/esm/components/Page'; import { Toolbar, ToolbarContent } from '@patternfly/react-core/dist/esm/components/Toolbar'; -import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter'; import CustomEmptyState from '~/shared/components/CustomEmptyState'; import { defineDataFields, FilterableDataFieldKey } from '~/app/filterableDataHelper'; +import { WorkspacekindsPodConfigValue } from '~/generated/data-contracts'; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { fields, filterableLabelMap } = defineDataFields({ @@ -21,10 +21,10 @@ const { fields, filterableLabelMap } = defineDataFields({ type FilterableDataFieldKeys = FilterableDataFieldKey; type WorkspaceFormPodConfigListProps = { - podConfigs: WorkspacePodConfigValue[]; + podConfigs: WorkspacekindsPodConfigValue[]; selectedLabels: Map>; - selectedPodConfig: WorkspacePodConfigValue | undefined; - onSelect: (workspacePodConfig: WorkspacePodConfigValue | undefined) => void; + selectedPodConfig: WorkspacekindsPodConfigValue | undefined; + onSelect: (workspacePodConfig: WorkspacekindsPodConfigValue | undefined) => void; }; export const WorkspaceFormPodConfigList: React.FunctionComponent< @@ -34,7 +34,7 @@ export const WorkspaceFormPodConfigList: React.FunctionComponent< const filterRef = useRef(null); const getFilteredWorkspacePodConfigsByLabels = useCallback( - (unfilteredPodConfigs: WorkspacePodConfigValue[]) => + (unfilteredPodConfigs: WorkspacekindsPodConfigValue[]) => unfilteredPodConfigs.filter((podConfig) => podConfig.labels.reduce((accumulator, label) => { if (selectedLabels.has(label.key)) { diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection.tsx index dfe4b843..a82a2870 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigSelection.tsx @@ -3,12 +3,12 @@ import { Content } from '@patternfly/react-core/dist/esm/components/Content'; import { Split, SplitItem } from '@patternfly/react-core/dist/esm/layouts/Split'; import { WorkspaceFormPodConfigList } from '~/app/pages/Workspaces/Form/podConfig/WorkspaceFormPodConfigList'; import { FilterByLabels } from '~/app/pages/Workspaces/Form/labelFilter/FilterByLabels'; -import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; +import { WorkspacekindsPodConfigValue } from '~/generated/data-contracts'; interface WorkspaceFormPodConfigSelectionProps { - podConfigs: WorkspacePodConfigValue[]; - selectedPodConfig: WorkspacePodConfigValue | undefined; - onSelect: (podConfig: WorkspacePodConfigValue | undefined) => void; + podConfigs: WorkspacekindsPodConfigValue[]; + selectedPodConfig: WorkspacekindsPodConfigValue | undefined; + onSelect: (podConfig: WorkspacekindsPodConfigValue | undefined) => void; } const WorkspaceFormPodConfigSelection: React.FunctionComponent< diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx index 96f1db95..66e07ab4 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSecrets.tsx @@ -24,11 +24,11 @@ import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggl import { Form, FormGroup } from '@patternfly/react-core/dist/esm/components/Form'; import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText'; import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; -import { WorkspacePodSecretMount } from '~/shared/api/backendApiTypes'; +import { WorkspacesPodSecretMount } from '~/generated/data-contracts'; interface WorkspaceFormPropertiesSecretsProps { - secrets: WorkspacePodSecretMount[]; - setSecrets: (secrets: WorkspacePodSecretMount[]) => void; + secrets: WorkspacesPodSecretMount[]; + setSecrets: (secrets: WorkspacesPodSecretMount[]) => void; } const DEFAULT_MODE_OCTAL = (420).toString(8); @@ -39,7 +39,7 @@ export const WorkspaceFormPropertiesSecrets: React.FC { const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ secretName: '', mountPath: '', defaultMode: parseInt(DEFAULT_MODE_OCTAL, 8), diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSelection.tsx index d3c64b83..f1e6e3ae 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesSelection.tsx @@ -8,12 +8,12 @@ import { Split, SplitItem } from '@patternfly/react-core/dist/esm/layouts/Split' import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput'; import { WorkspaceFormImageDetails } from '~/app/pages/Workspaces/Form/image/WorkspaceFormImageDetails'; import { WorkspaceFormPropertiesVolumes } from '~/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes'; +import { WorkspacekindsImageConfigValue } from '~/generated/data-contracts'; import { WorkspaceFormProperties } from '~/app/types'; -import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes'; import { WorkspaceFormPropertiesSecrets } from './WorkspaceFormPropertiesSecrets'; interface WorkspaceFormPropertiesSelectionProps { - selectedImage: WorkspaceImageConfigValue | undefined; + selectedImage: WorkspacekindsImageConfigValue | undefined; selectedProperties: WorkspaceFormProperties; onSelect: (properties: WorkspaceFormProperties) => void; } diff --git a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes.tsx b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes.tsx index 780e4ca7..4b44a76d 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Form/properties/WorkspaceFormPropertiesVolumes.tsx @@ -23,11 +23,11 @@ import { Tr, } from '@patternfly/react-table/dist/esm/components/Table'; import { PlusCircleIcon } from '@patternfly/react-icons/dist/esm/icons/plus-circle-icon'; -import { WorkspacePodVolumeMount } from '~/shared/api/backendApiTypes'; +import { WorkspacesPodVolumeMount } from '~/generated/data-contracts'; interface WorkspaceFormPropertiesVolumesProps { - volumes: WorkspacePodVolumeMount[]; - setVolumes: (volumes: WorkspacePodVolumeMount[]) => void; + volumes: WorkspacesPodVolumeMount[]; + setVolumes: (volumes: WorkspacesPodVolumeMount[]) => void; } export const WorkspaceFormPropertiesVolumes: React.FC = ({ @@ -36,7 +36,7 @@ export const WorkspaceFormPropertiesVolumes: React.FC { const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ pvcName: '', mountPath: '', readOnly: false, diff --git a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConfigDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConfigDetails.tsx index 1270943c..83485ff1 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConfigDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConfigDetails.tsx @@ -5,11 +5,11 @@ import { DescriptionListGroup, DescriptionListDescription, } from '@patternfly/react-core/dist/esm/components/DescriptionList'; -import { Workspace } from '~/shared/api/backendApiTypes'; import { formatResourceFromWorkspace } from '~/shared/utilities/WorkspaceUtils'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; interface WorkspaceConfigDetailsProps { - workspace: Workspace; + workspace: WorkspacesWorkspace; } export const WorkspaceConfigDetails: React.FC = ({ workspace }) => ( diff --git a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx index 4b231354..428a8502 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx @@ -9,10 +9,10 @@ import { MenuToggleElement, MenuToggleAction, } from '@patternfly/react-core/dist/esm/components/MenuToggle'; -import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes'; +import { WorkspacesWorkspace, WorkspacesWorkspaceState } from '~/generated/data-contracts'; type WorkspaceConnectActionProps = { - workspace: Workspace; + workspace: WorkspacesWorkspace; }; export const WorkspaceConnectAction: React.FunctionComponent = ({ @@ -57,7 +57,7 @@ export const WorkspaceConnectAction: React.FunctionComponent = ({ workspace }) => { diff --git a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceStorage.tsx b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceStorage.tsx index 9f55940a..a6bd3da4 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceStorage.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceStorage.tsx @@ -5,11 +5,11 @@ import { DescriptionListGroup, DescriptionListDescription, } from '@patternfly/react-core/dist/esm/components/DescriptionList'; -import { Workspace } from '~/shared/api/backendApiTypes'; import { DataVolumesList } from '~/app/pages/Workspaces/DataVolumesList'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; interface WorkspaceStorageProps { - workspace: Workspace; + workspace: WorkspacesWorkspace; } export const WorkspaceStorage: React.FC = ({ workspace }) => ( diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index 0c76e373..3e461396 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -5,12 +5,12 @@ import { Stack, StackItem } from '@patternfly/react-core/dist/esm/layouts/Stack' import WorkspaceTable from '~/app/components/WorkspaceTable'; import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; import { useWorkspacesByNamespace } from '~/app/hooks/useWorkspaces'; -import { WorkspaceState } from '~/shared/api/backendApiTypes'; import { DEFAULT_POLLING_RATE_MS } from '~/app/const'; import { LoadingSpinner } from '~/app/components/LoadingSpinner'; import { LoadError } from '~/app/components/LoadError'; import { useWorkspaceRowActions } from '~/app/hooks/useWorkspaceRowActions'; import { usePolling } from '~/app/hooks/usePolling'; +import { WorkspacesWorkspaceState } from '~/generated/data-contracts'; export const Workspaces: React.FunctionComponent = () => { const { selectedNamespace } = useNamespaceContext(); @@ -27,17 +27,17 @@ export const Workspaces: React.FunctionComponent = () => { { id: 'separator' }, { id: 'stop', - isVisible: (w) => w.state === WorkspaceState.WorkspaceStateRunning, + isVisible: (w) => w.state === WorkspacesWorkspaceState.WorkspaceStateRunning, onActionDone: refreshWorkspaces, }, { id: 'start', - isVisible: (w) => w.state !== WorkspaceState.WorkspaceStateRunning, + isVisible: (w) => w.state !== WorkspacesWorkspaceState.WorkspaceStateRunning, onActionDone: refreshWorkspaces, }, { id: 'restart', - isVisible: (w) => w.state === WorkspaceState.WorkspaceStateRunning, + isVisible: (w) => w.state === WorkspacesWorkspaceState.WorkspaceStateRunning, onActionDone: refreshWorkspaces, }, ]); diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx index 72b2786f..4d50ff5a 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx @@ -7,7 +7,7 @@ import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/ex import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; import { InfoCircleIcon } from '@patternfly/react-icons/dist/esm/icons/info-circle-icon'; import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName'; -import { WorkspaceKind } from '~/shared/api/backendApiTypes'; +import { WorkspacekindsWorkspaceKind } from '~/generated/data-contracts'; const getLevelIcon = (level: string | undefined) => { switch (level) { @@ -48,9 +48,9 @@ export const WorkspaceRedirectInformationView: React.FC(0); const [workspaceKind, workspaceKindLoaded] = useWorkspaceKindByName(kind); const [imageConfig, setImageConfig] = - useState(); + useState(); const [podConfig, setPodConfig] = - useState(); + useState(); useEffect(() => { if (!workspaceKindLoaded) { diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx index 3e341701..8a641a5d 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx @@ -8,13 +8,13 @@ import { ModalHeader, } from '@patternfly/react-core/dist/esm/components/Modal'; import { TabTitleText } from '@patternfly/react-core/dist/esm/components/Tabs'; -import { Workspace } from '~/shared/api/backendApiTypes'; import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; +import { WorkspacesWorkspace } from '~/generated/data-contracts'; interface RestartActionAlertProps { onClose: () => void; isOpen: boolean; - workspace: Workspace | null; + workspace: WorkspacesWorkspace | null; } export const WorkspaceRestartActionModal: React.FC = ({ diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx index bbd0c532..1bbfcaf4 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx @@ -8,14 +8,14 @@ import { } from '@patternfly/react-core/dist/esm/components/Modal'; import { TabTitleText } from '@patternfly/react-core/dist/esm/components/Tabs'; import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; -import { Workspace, WorkspacePauseState } from '~/shared/api/backendApiTypes'; import { ActionButton } from '~/shared/components/ActionButton'; +import { ApiWorkspaceActionPauseEnvelope, WorkspacesWorkspace } from '~/generated/data-contracts'; interface StartActionAlertProps { onClose: () => void; isOpen: boolean; - workspace: Workspace | null; - onStart: () => Promise; + workspace: WorkspacesWorkspace | null; + onStart: () => Promise; onUpdateAndStart: () => Promise; onActionDone?: () => void; } @@ -33,10 +33,16 @@ export const WorkspaceStartActionModal: React.FC = ({ const [actionOnGoing, setActionOnGoing] = useState(null); const executeAction = useCallback( - (args: { action: StartAction; callback: () => ReturnType }) => { - setActionOnGoing(args.action); + async ({ + action, + callback, + }: { + action: StartAction; + callback: () => Promise; + }): Promise => { + setActionOnGoing(action); try { - return args.callback(); + return await callback(); } finally { setActionOnGoing(null); } @@ -48,7 +54,7 @@ export const WorkspaceStartActionModal: React.FC = ({ try { const response = await executeAction({ action: 'start', callback: onStart }); // TODO: alert user about success - console.info('Workspace started successfully:', JSON.stringify(response)); + console.info('Workspace started successfully:', JSON.stringify(response.data)); onActionDone?.(); onClose(); } catch (error) { diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx index 631724ba..793efddc 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx @@ -9,14 +9,14 @@ import { } from '@patternfly/react-core/dist/esm/components/Modal'; import { TabTitleText } from '@patternfly/react-core/dist/esm/components/Tabs'; import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; -import { Workspace, WorkspacePauseState } from '~/shared/api/backendApiTypes'; import { ActionButton } from '~/shared/components/ActionButton'; +import { ApiWorkspaceActionPauseEnvelope, WorkspacesWorkspace } from '~/generated/data-contracts'; interface StopActionAlertProps { onClose: () => void; isOpen: boolean; - workspace: Workspace | null; - onStop: () => Promise; + workspace: WorkspacesWorkspace | null; + onStop: () => Promise; onUpdateAndStop: () => Promise; onActionDone?: () => void; } @@ -35,10 +35,16 @@ export const WorkspaceStopActionModal: React.FC = ({ const [actionOnGoing, setActionOnGoing] = useState(null); const executeAction = useCallback( - (args: { action: StopAction; callback: () => ReturnType }) => { - setActionOnGoing(args.action); + async ({ + action, + callback, + }: { + action: StopAction; + callback: () => Promise; + }): Promise => { + setActionOnGoing(action); try { - return args.callback(); + return await callback(); } finally { setActionOnGoing(null); } @@ -50,7 +56,7 @@ export const WorkspaceStopActionModal: React.FC = ({ try { const response = await executeAction({ action: 'stop', callback: onStop }); // TODO: alert user about success - console.info('Workspace stopped successfully:', JSON.stringify(response)); + console.info('Workspace stopped successfully:', JSON.stringify(response.data)); onActionDone?.(); onClose(); } catch (error) { diff --git a/workspaces/frontend/src/app/types.ts b/workspaces/frontend/src/app/types.ts index b87f7f8a..b9c68f39 100644 --- a/workspaces/frontend/src/app/types.ts +++ b/workspaces/frontend/src/app/types.ts @@ -1,14 +1,14 @@ import { - WorkspaceImageConfigValue, - WorkspaceKind, - WorkspacePodConfigValue, - WorkspacePodVolumeMount, - WorkspacePodSecretMount, - Workspace, - WorkspaceImageRef, - WorkspacePodVolumeMounts, - WorkspaceKindPodMetadata, -} from '~/shared/api/backendApiTypes'; + WorkspacekindsImageConfigValue, + WorkspacekindsImageRef, + WorkspacekindsPodConfigValue, + WorkspacekindsPodMetadata, + WorkspacekindsPodVolumeMounts, + WorkspacekindsWorkspaceKind, + WorkspacesPodSecretMount, + WorkspacesPodVolumeMount, + WorkspacesWorkspace, +} from '~/generated/data-contracts'; export interface WorkspaceColumnDefinition { name: string; @@ -27,22 +27,22 @@ export interface WorkspaceFormProperties { workspaceName: string; deferUpdates: boolean; homeDirectory: string; - volumes: WorkspacePodVolumeMount[]; - secrets: WorkspacePodSecretMount[]; + volumes: WorkspacesPodVolumeMount[]; + secrets: WorkspacesPodSecretMount[]; } export interface WorkspaceFormData { - kind: WorkspaceKind | undefined; - image: WorkspaceImageConfigValue | undefined; - podConfig: WorkspacePodConfigValue | undefined; + kind: WorkspacekindsWorkspaceKind | undefined; + image: WorkspacekindsImageConfigValue | undefined; + podConfig: WorkspacekindsPodConfigValue | undefined; properties: WorkspaceFormProperties; } export interface WorkspaceCountPerOption { count: number; - countByImage: Record; - countByPodConfig: Record; - countByNamespace: Record; + countByImage: Record; + countByPodConfig: Record; + countByNamespace: Record; } export interface WorkspaceKindProperties { @@ -51,11 +51,11 @@ export interface WorkspaceKindProperties { deprecated: boolean; deprecationMessage: string; hidden: boolean; - icon: WorkspaceImageRef; - logo: WorkspaceImageRef; + icon: WorkspacekindsImageRef; + logo: WorkspacekindsImageRef; } -export interface WorkspaceKindImageConfigValue extends WorkspaceImageConfigValue { +export interface WorkspaceKindImageConfigValue extends WorkspacekindsImageConfigValue { imagePullPolicy?: ImagePullPolicy.IfNotPresent | ImagePullPolicy.Always | ImagePullPolicy.Never; ports?: WorkspaceKindImagePort[]; image?: string; @@ -74,7 +74,7 @@ export interface WorkspaceKindImagePort { protocol: 'HTTP'; // ONLY HTTP is supported at the moment, per https://github.com/thesuperzapper/kubeflow-notebooks-v2-design/blob/main/crds/workspace-kind.yaml#L275 } -export interface WorkspaceKindPodConfigValue extends WorkspacePodConfigValue { +export interface WorkspaceKindPodConfigValue extends WorkspacekindsPodConfigValue { resources?: { requests: { [key: string]: string; @@ -105,10 +105,10 @@ export interface WorkspaceKindPodCulling { } export interface WorkspaceKindPodTemplateData { - podMetadata: WorkspaceKindPodMetadata; - volumeMounts: WorkspacePodVolumeMounts; + podMetadata: WorkspacekindsPodMetadata; + volumeMounts: WorkspacekindsPodVolumeMounts; culling?: WorkspaceKindPodCulling; - extraVolumeMounts?: WorkspacePodVolumeMount[]; + extraVolumeMounts?: WorkspacesPodVolumeMount[]; } export interface WorkspaceKindFormData { diff --git a/workspaces/frontend/src/generated/Healthcheck.ts b/workspaces/frontend/src/generated/Healthcheck.ts new file mode 100644 index 00000000..d8b10945 --- /dev/null +++ b/workspaces/frontend/src/generated/Healthcheck.ts @@ -0,0 +1,34 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +import { ApiErrorEnvelope, HealthCheckHealthCheck } from './data-contracts'; +import { HttpClient, RequestParams } from './http-client'; + +export class Healthcheck extends HttpClient { + /** + * @description Provides a healthcheck response indicating the status of key services. + * + * @tags healthcheck + * @name GetHealthcheck + * @summary Returns the health status of the application + * @request GET:/healthcheck + * @response `200` `HealthCheckHealthCheck` Successful healthcheck response + * @response `500` `ApiErrorEnvelope` Internal server error + */ + getHealthcheck = (params: RequestParams = {}) => + this.request({ + path: `/healthcheck`, + method: 'GET', + format: 'json', + ...params, + }); +} diff --git a/workspaces/frontend/src/generated/Namespaces.ts b/workspaces/frontend/src/generated/Namespaces.ts new file mode 100644 index 00000000..f626fc92 --- /dev/null +++ b/workspaces/frontend/src/generated/Namespaces.ts @@ -0,0 +1,36 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +import { ApiErrorEnvelope, ApiNamespaceListEnvelope } from './data-contracts'; +import { HttpClient, RequestParams } from './http-client'; + +export class Namespaces extends HttpClient { + /** + * @description Provides a list of all namespaces that the user has access to + * + * @tags namespaces + * @name ListNamespaces + * @summary Returns a list of all namespaces + * @request GET:/namespaces + * @response `200` `ApiNamespaceListEnvelope` Successful namespaces response + * @response `401` `ApiErrorEnvelope` Unauthorized + * @response `403` `ApiErrorEnvelope` Forbidden + * @response `500` `ApiErrorEnvelope` Internal server error + */ + listNamespaces = (params: RequestParams = {}) => + this.request({ + path: `/namespaces`, + method: 'GET', + format: 'json', + ...params, + }); +} diff --git a/workspaces/frontend/src/generated/Workspacekinds.ts b/workspaces/frontend/src/generated/Workspacekinds.ts new file mode 100644 index 00000000..4b250e17 --- /dev/null +++ b/workspaces/frontend/src/generated/Workspacekinds.ts @@ -0,0 +1,89 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +import { + ApiErrorEnvelope, + ApiWorkspaceKindEnvelope, + ApiWorkspaceKindListEnvelope, + CreateWorkspaceKindPayload, +} from './data-contracts'; +import { ContentType, HttpClient, RequestParams } from './http-client'; + +export class Workspacekinds extends HttpClient { + /** + * @description Returns a list of all available workspace kinds. Workspace kinds define the different types of workspaces that can be created in the system. + * + * @tags workspacekinds + * @name ListWorkspaceKinds + * @summary List workspace kinds + * @request GET:/workspacekinds + * @response `200` `ApiWorkspaceKindListEnvelope` Successful operation. Returns a list of all available workspace kinds. + * @response `401` `ApiErrorEnvelope` Unauthorized. Authentication is required. + * @response `403` `ApiErrorEnvelope` Forbidden. User does not have permission to list workspace kinds. + * @response `500` `ApiErrorEnvelope` Internal server error. An unexpected error occurred on the server. + */ + listWorkspaceKinds = (params: RequestParams = {}) => + this.request({ + path: `/workspacekinds`, + method: 'GET', + type: ContentType.Json, + format: 'json', + ...params, + }); + /** + * @description Creates a new workspace kind. + * + * @tags workspacekinds + * @name CreateWorkspaceKind + * @summary Create workspace kind + * @request POST:/workspacekinds + * @response `201` `ApiWorkspaceKindEnvelope` WorkspaceKind created successfully + * @response `400` `ApiErrorEnvelope` Bad Request. + * @response `401` `ApiErrorEnvelope` Unauthorized. Authentication is required. + * @response `403` `ApiErrorEnvelope` Forbidden. User does not have permission to create WorkspaceKind. + * @response `409` `ApiErrorEnvelope` Conflict. WorkspaceKind with the same name already exists. + * @response `413` `ApiErrorEnvelope` Request Entity Too Large. The request body is too large. + * @response `415` `ApiErrorEnvelope` Unsupported Media Type. Content-Type header is not correct. + * @response `422` `ApiErrorEnvelope` Unprocessable Entity. Validation error. + * @response `500` `ApiErrorEnvelope` Internal server error. An unexpected error occurred on the server. + */ + createWorkspaceKind = (body: CreateWorkspaceKindPayload, params: RequestParams = {}) => + this.request({ + path: `/workspacekinds`, + method: 'POST', + body: body, + format: 'json', + ...params, + }); + /** + * @description Returns details of a specific workspace kind identified by its name. Workspace kinds define the available types of workspaces that can be created. + * + * @tags workspacekinds + * @name GetWorkspaceKind + * @summary Get workspace kind + * @request GET:/workspacekinds/{name} + * @response `200` `ApiWorkspaceKindEnvelope` Successful operation. Returns the requested workspace kind details. + * @response `400` `ApiErrorEnvelope` Bad Request. Invalid workspace kind name format. + * @response `401` `ApiErrorEnvelope` Unauthorized. Authentication is required. + * @response `403` `ApiErrorEnvelope` Forbidden. User does not have permission to access the workspace kind. + * @response `404` `ApiErrorEnvelope` Not Found. Workspace kind does not exist. + * @response `500` `ApiErrorEnvelope` Internal server error. An unexpected error occurred on the server. + */ + getWorkspaceKind = (name: string, params: RequestParams = {}) => + this.request({ + path: `/workspacekinds/${name}`, + method: 'GET', + type: ContentType.Json, + format: 'json', + ...params, + }); +} diff --git a/workspaces/frontend/src/generated/Workspaces.ts b/workspaces/frontend/src/generated/Workspaces.ts new file mode 100644 index 00000000..4c616f6c --- /dev/null +++ b/workspaces/frontend/src/generated/Workspaces.ts @@ -0,0 +1,167 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +import { + ApiErrorEnvelope, + ApiWorkspaceActionPauseEnvelope, + ApiWorkspaceCreateEnvelope, + ApiWorkspaceEnvelope, + ApiWorkspaceListEnvelope, +} from './data-contracts'; +import { ContentType, HttpClient, RequestParams } from './http-client'; + +export class Workspaces extends HttpClient { + /** + * @description Returns a list of all workspaces across all namespaces. + * + * @tags workspaces + * @name ListAllWorkspaces + * @summary List all workspaces + * @request GET:/workspaces + * @response `200` `ApiWorkspaceListEnvelope` Successful operation. Returns a list of all workspaces. + * @response `401` `ApiErrorEnvelope` Unauthorized. Authentication is required. + * @response `403` `ApiErrorEnvelope` Forbidden. User does not have permission to list workspaces. + * @response `500` `ApiErrorEnvelope` Internal server error. An unexpected error occurred on the server. + */ + listAllWorkspaces = (params: RequestParams = {}) => + this.request({ + path: `/workspaces`, + method: 'GET', + type: ContentType.Json, + format: 'json', + ...params, + }); + /** + * @description Returns a list of workspaces in a specific namespace. + * + * @tags workspaces + * @name ListWorkspacesByNamespace + * @summary List workspaces by namespace + * @request GET:/workspaces/{namespace} + * @response `200` `ApiWorkspaceListEnvelope` Successful operation. Returns a list of workspaces in the specified namespace. + * @response `400` `ApiErrorEnvelope` Bad Request. Invalid namespace format. + * @response `401` `ApiErrorEnvelope` Unauthorized. Authentication is required. + * @response `403` `ApiErrorEnvelope` Forbidden. User does not have permission to list workspaces. + * @response `500` `ApiErrorEnvelope` Internal server error. An unexpected error occurred on the server. + */ + listWorkspacesByNamespace = (namespace: string, params: RequestParams = {}) => + this.request({ + path: `/workspaces/${namespace}`, + method: 'GET', + type: ContentType.Json, + format: 'json', + ...params, + }); + /** + * @description Creates a new workspace in the specified namespace. + * + * @tags workspaces + * @name CreateWorkspace + * @summary Create workspace + * @request POST:/workspaces/{namespace} + * @response `201` `ApiWorkspaceEnvelope` Workspace created successfully + * @response `400` `ApiErrorEnvelope` Bad Request. Invalid request body or namespace format. + * @response `401` `ApiErrorEnvelope` Unauthorized. Authentication is required. + * @response `403` `ApiErrorEnvelope` Forbidden. User does not have permission to create workspace. + * @response `409` `ApiErrorEnvelope` Conflict. Workspace with the same name already exists. + * @response `413` `ApiErrorEnvelope` Request Entity Too Large. The request body is too large. + * @response `415` `ApiErrorEnvelope` Unsupported Media Type. Content-Type header is not correct. + * @response `500` `ApiErrorEnvelope` Internal server error. An unexpected error occurred on the server. + */ + createWorkspace = ( + namespace: string, + body: ApiWorkspaceCreateEnvelope, + params: RequestParams = {}, + ) => + this.request({ + path: `/workspaces/${namespace}`, + method: 'POST', + body: body, + type: ContentType.Json, + format: 'json', + ...params, + }); + /** + * @description Pauses or unpauses a workspace, stopping or resuming all associated pods. + * + * @tags workspaces + * @name UpdateWorkspacePauseState + * @summary Pause or unpause a workspace + * @request POST:/workspaces/{namespace}/{workspaceName}/actions/pause + * @response `200` `ApiWorkspaceActionPauseEnvelope` Successful action. Returns the current pause state. + * @response `400` `ApiErrorEnvelope` Bad Request. + * @response `401` `ApiErrorEnvelope` Unauthorized. Authentication is required. + * @response `403` `ApiErrorEnvelope` Forbidden. User does not have permission to access the workspace. + * @response `404` `ApiErrorEnvelope` Not Found. Workspace does not exist. + * @response `413` `ApiErrorEnvelope` Request Entity Too Large. The request body is too large. + * @response `415` `ApiErrorEnvelope` Unsupported Media Type. Content-Type header is not correct. + * @response `422` `ApiErrorEnvelope` Unprocessable Entity. Workspace is not in appropriate state. + * @response `500` `ApiErrorEnvelope` Internal server error. An unexpected error occurred on the server. + */ + updateWorkspacePauseState = ( + namespace: string, + workspaceName: string, + body: ApiWorkspaceActionPauseEnvelope, + params: RequestParams = {}, + ) => + this.request({ + path: `/workspaces/${namespace}/${workspaceName}/actions/pause`, + method: 'POST', + body: body, + type: ContentType.Json, + format: 'json', + ...params, + }); + /** + * @description Returns details of a specific workspace identified by namespace and workspace name. + * + * @tags workspaces + * @name GetWorkspace + * @summary Get workspace + * @request GET:/workspaces/{namespace}/{workspace_name} + * @response `200` `ApiWorkspaceEnvelope` Successful operation. Returns the requested workspace details. + * @response `400` `ApiErrorEnvelope` Bad Request. Invalid namespace or workspace name format. + * @response `401` `ApiErrorEnvelope` Unauthorized. Authentication is required. + * @response `403` `ApiErrorEnvelope` Forbidden. User does not have permission to access the workspace. + * @response `404` `ApiErrorEnvelope` Not Found. Workspace does not exist. + * @response `500` `ApiErrorEnvelope` Internal server error. An unexpected error occurred on the server. + */ + getWorkspace = (namespace: string, workspaceName: string, params: RequestParams = {}) => + this.request({ + path: `/workspaces/${namespace}/${workspaceName}`, + method: 'GET', + type: ContentType.Json, + format: 'json', + ...params, + }); + /** + * @description Deletes a specific workspace identified by namespace and workspace name. + * + * @tags workspaces + * @name DeleteWorkspace + * @summary Delete workspace + * @request DELETE:/workspaces/{namespace}/{workspace_name} + * @response `204` `void` Workspace deleted successfully + * @response `400` `ApiErrorEnvelope` Bad Request. Invalid namespace or workspace name format. + * @response `401` `ApiErrorEnvelope` Unauthorized. Authentication is required. + * @response `403` `ApiErrorEnvelope` Forbidden. User does not have permission to delete the workspace. + * @response `404` `ApiErrorEnvelope` Not Found. Workspace does not exist. + * @response `500` `ApiErrorEnvelope` Internal server error. An unexpected error occurred on the server. + */ + deleteWorkspace = (namespace: string, workspaceName: string, params: RequestParams = {}) => + this.request({ + path: `/workspaces/${namespace}/${workspaceName}`, + method: 'DELETE', + type: ContentType.Json, + ...params, + }); +} diff --git a/workspaces/frontend/src/generated/data-contracts.ts b/workspaces/frontend/src/generated/data-contracts.ts new file mode 100644 index 00000000..12a6a45e --- /dev/null +++ b/workspaces/frontend/src/generated/data-contracts.ts @@ -0,0 +1,373 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +export enum WorkspacesWorkspaceState { + WorkspaceStateRunning = 'Running', + WorkspaceStateTerminating = 'Terminating', + WorkspaceStatePaused = 'Paused', + WorkspaceStatePending = 'Pending', + WorkspaceStateError = 'Error', + WorkspaceStateUnknown = 'Unknown', +} + +export enum WorkspacesRedirectMessageLevel { + RedirectMessageLevelInfo = 'Info', + RedirectMessageLevelWarning = 'Warning', + RedirectMessageLevelDanger = 'Danger', +} + +export enum WorkspacesProbeResult { + ProbeResultSuccess = 'Success', + ProbeResultFailure = 'Failure', + ProbeResultTimeout = 'Timeout', +} + +export enum WorkspacekindsRedirectMessageLevel { + RedirectMessageLevelInfo = 'Info', + RedirectMessageLevelWarning = 'Warning', + RedirectMessageLevelDanger = 'Danger', +} + +export enum HealthCheckServiceStatus { + ServiceStatusHealthy = 'Healthy', + ServiceStatusUnhealthy = 'Unhealthy', +} + +export enum FieldErrorType { + ErrorTypeNotFound = 'FieldValueNotFound', + ErrorTypeRequired = 'FieldValueRequired', + ErrorTypeDuplicate = 'FieldValueDuplicate', + ErrorTypeInvalid = 'FieldValueInvalid', + ErrorTypeNotSupported = 'FieldValueNotSupported', + ErrorTypeForbidden = 'FieldValueForbidden', + ErrorTypeTooLong = 'FieldValueTooLong', + ErrorTypeTooMany = 'FieldValueTooMany', + ErrorTypeInternal = 'InternalError', + ErrorTypeTypeInvalid = 'FieldValueTypeInvalid', +} + +export interface ActionsWorkspaceActionPause { + paused: boolean; +} + +export interface ApiErrorCause { + validation_errors?: ApiValidationError[]; +} + +export interface ApiErrorEnvelope { + error: ApiHTTPError; +} + +export interface ApiHTTPError { + cause?: ApiErrorCause; + code: string; + message: string; +} + +export interface ApiNamespaceListEnvelope { + data: NamespacesNamespace[]; +} + +export interface ApiValidationError { + field: string; + message: string; + type: FieldErrorType; +} + +export interface ApiWorkspaceActionPauseEnvelope { + data: ActionsWorkspaceActionPause; +} + +export interface ApiWorkspaceCreateEnvelope { + data: WorkspacesWorkspaceCreate; +} + +export interface ApiWorkspaceEnvelope { + data: WorkspacesWorkspace; +} + +export interface ApiWorkspaceKindEnvelope { + data: WorkspacekindsWorkspaceKind; +} + +export interface ApiWorkspaceKindListEnvelope { + data: WorkspacekindsWorkspaceKind[]; +} + +export interface ApiWorkspaceListEnvelope { + data: WorkspacesWorkspace[]; +} + +export interface HealthCheckHealthCheck { + status: HealthCheckServiceStatus; + systemInfo: HealthCheckSystemInfo; +} + +export interface HealthCheckSystemInfo { + version: string; +} + +export interface NamespacesNamespace { + name: string; +} + +export interface WorkspacekindsImageConfig { + default: string; + values: WorkspacekindsImageConfigValue[]; +} + +export interface WorkspacekindsImageConfigValue { + clusterMetrics?: WorkspacekindsClusterMetrics; + description: string; + displayName: string; + hidden: boolean; + id: string; + labels: WorkspacekindsOptionLabel[]; + redirect?: WorkspacekindsOptionRedirect; +} + +export interface WorkspacekindsImageRef { + url: string; +} + +export interface WorkspacekindsOptionLabel { + key: string; + value: string; +} + +export interface WorkspacekindsOptionRedirect { + message?: WorkspacekindsRedirectMessage; + to: string; +} + +export interface WorkspacekindsPodConfig { + default: string; + values: WorkspacekindsPodConfigValue[]; +} + +export interface WorkspacekindsPodConfigValue { + clusterMetrics?: WorkspacekindsClusterMetrics; + description: string; + displayName: string; + hidden: boolean; + id: string; + labels: WorkspacekindsOptionLabel[]; + redirect?: WorkspacekindsOptionRedirect; +} + +export interface WorkspacekindsPodMetadata { + annotations: Record; + labels: Record; +} + +export interface WorkspacekindsPodTemplate { + options: WorkspacekindsPodTemplateOptions; + podMetadata: WorkspacekindsPodMetadata; + volumeMounts: WorkspacekindsPodVolumeMounts; +} + +export interface WorkspacekindsPodTemplateOptions { + imageConfig: WorkspacekindsImageConfig; + podConfig: WorkspacekindsPodConfig; +} + +export interface WorkspacekindsPodVolumeMounts { + home: string; +} + +export interface WorkspacekindsRedirectMessage { + level: WorkspacekindsRedirectMessageLevel; + text: string; +} + +export interface WorkspacekindsWorkspaceKind { + clusterMetrics?: WorkspacekindsClusterMetrics; + deprecated: boolean; + deprecationMessage: string; + description: string; + displayName: string; + hidden: boolean; + icon: WorkspacekindsImageRef; + logo: WorkspacekindsImageRef; + name: string; + podTemplate: WorkspacekindsPodTemplate; +} + +export interface WorkspacekindsClusterMetrics { + workspacesCount: number; +} + +export interface WorkspacesActivity { + /** Unix Epoch time */ + lastActivity: number; + lastProbe?: WorkspacesLastProbeInfo; + /** Unix Epoch time */ + lastUpdate: number; +} + +export interface WorkspacesHttpService { + displayName: string; + httpPath: string; +} + +export interface WorkspacesImageConfig { + current: WorkspacesOptionInfo; + desired?: WorkspacesOptionInfo; + redirectChain?: WorkspacesRedirectStep[]; +} + +export interface WorkspacesImageRef { + url: string; +} + +export interface WorkspacesLastProbeInfo { + /** Unix Epoch time in milliseconds */ + endTimeMs: number; + message: string; + result: WorkspacesProbeResult; + /** Unix Epoch time in milliseconds */ + startTimeMs: number; +} + +export interface WorkspacesOptionInfo { + description: string; + displayName: string; + id: string; + labels: WorkspacesOptionLabel[]; +} + +export interface WorkspacesOptionLabel { + key: string; + value: string; +} + +export interface WorkspacesPodConfig { + current: WorkspacesOptionInfo; + desired?: WorkspacesOptionInfo; + redirectChain?: WorkspacesRedirectStep[]; +} + +export interface WorkspacesPodMetadata { + annotations: Record; + labels: Record; +} + +export interface WorkspacesPodMetadataMutate { + annotations: Record; + labels: Record; +} + +export interface WorkspacesPodSecretInfo { + defaultMode?: number; + mountPath: string; + secretName: string; +} + +export interface WorkspacesPodSecretMount { + defaultMode?: number; + mountPath: string; + secretName: string; +} + +export interface WorkspacesPodTemplate { + options: WorkspacesPodTemplateOptions; + podMetadata: WorkspacesPodMetadata; + volumes: WorkspacesPodVolumes; +} + +export interface WorkspacesPodTemplateMutate { + options: WorkspacesPodTemplateOptionsMutate; + podMetadata: WorkspacesPodMetadataMutate; + volumes: WorkspacesPodVolumesMutate; +} + +export interface WorkspacesPodTemplateOptions { + imageConfig: WorkspacesImageConfig; + podConfig: WorkspacesPodConfig; +} + +export interface WorkspacesPodTemplateOptionsMutate { + imageConfig: string; + podConfig: string; +} + +export interface WorkspacesPodVolumeInfo { + mountPath: string; + pvcName: string; + readOnly: boolean; +} + +export interface WorkspacesPodVolumeMount { + mountPath: string; + pvcName: string; + readOnly?: boolean; +} + +export interface WorkspacesPodVolumes { + data: WorkspacesPodVolumeInfo[]; + home?: WorkspacesPodVolumeInfo; + secrets?: WorkspacesPodSecretInfo[]; +} + +export interface WorkspacesPodVolumesMutate { + data: WorkspacesPodVolumeMount[]; + home?: string; + secrets?: WorkspacesPodSecretMount[]; +} + +export interface WorkspacesRedirectMessage { + level: WorkspacesRedirectMessageLevel; + text: string; +} + +export interface WorkspacesRedirectStep { + message?: WorkspacesRedirectMessage; + sourceId: string; + targetId: string; +} + +export interface WorkspacesService { + httpService?: WorkspacesHttpService; +} + +export interface WorkspacesWorkspace { + activity: WorkspacesActivity; + deferUpdates: boolean; + name: string; + namespace: string; + paused: boolean; + pausedTime: number; + pendingRestart: boolean; + podTemplate: WorkspacesPodTemplate; + services: WorkspacesService[]; + state: WorkspacesWorkspaceState; + stateMessage: string; + workspaceKind: WorkspacesWorkspaceKindInfo; +} + +export interface WorkspacesWorkspaceCreate { + deferUpdates: boolean; + kind: string; + name: string; + paused: boolean; + podTemplate: WorkspacesPodTemplateMutate; +} + +export interface WorkspacesWorkspaceKindInfo { + icon: WorkspacesImageRef; + logo: WorkspacesImageRef; + missing: boolean; + name: string; +} + +/** Kubernetes YAML manifest of a WorkspaceKind */ +export type CreateWorkspaceKindPayload = string; diff --git a/workspaces/frontend/src/generated/http-client.ts b/workspaces/frontend/src/generated/http-client.ts new file mode 100644 index 00000000..911e057f --- /dev/null +++ b/workspaces/frontend/src/generated/http-client.ts @@ -0,0 +1,163 @@ +/* eslint-disable */ +/* tslint:disable */ +// @ts-nocheck +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +import type { AxiosInstance, AxiosRequestConfig, HeadersDefaults, ResponseType } from 'axios'; +import axios from 'axios'; + +export type QueryParamsType = Record; + +export interface FullRequestParams + extends Omit { + /** set parameter to `true` for call `securityWorker` for this request */ + secure?: boolean; + /** request path */ + path: string; + /** content type of request body */ + type?: ContentType; + /** query params */ + query?: QueryParamsType; + /** format of response (i.e. response.json() -> format: "json") */ + format?: ResponseType; + /** request body */ + body?: unknown; +} + +export type RequestParams = Omit; + +export interface ApiConfig + extends Omit { + securityWorker?: ( + securityData: SecurityDataType | null, + ) => Promise | AxiosRequestConfig | void; + secure?: boolean; + format?: ResponseType; +} + +export enum ContentType { + Json = 'application/json', + JsonApi = 'application/vnd.api+json', + FormData = 'multipart/form-data', + UrlEncoded = 'application/x-www-form-urlencoded', + Text = 'text/plain', +} + +export class HttpClient { + public instance: AxiosInstance; + private securityData: SecurityDataType | null = null; + private securityWorker?: ApiConfig['securityWorker']; + private secure?: boolean; + private format?: ResponseType; + + constructor({ + securityWorker, + secure, + format, + ...axiosConfig + }: ApiConfig = {}) { + this.instance = axios.create({ + ...axiosConfig, + baseURL: axiosConfig.baseURL || 'http://localhost:4000/api/v1', + }); + this.secure = secure; + this.format = format; + this.securityWorker = securityWorker; + } + + public setSecurityData = (data: SecurityDataType | null) => { + this.securityData = data; + }; + + protected mergeRequestParams( + params1: AxiosRequestConfig, + params2?: AxiosRequestConfig, + ): AxiosRequestConfig { + const method = params1.method || (params2 && params2.method); + + return { + ...this.instance.defaults, + ...params1, + ...(params2 || {}), + headers: { + ...((method && + this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || + {}), + ...(params1.headers || {}), + ...((params2 && params2.headers) || {}), + }, + }; + } + + protected stringifyFormItem(formItem: unknown) { + if (typeof formItem === 'object' && formItem !== null) { + return JSON.stringify(formItem); + } else { + return `${formItem}`; + } + } + + protected createFormData(input: Record): FormData { + if (input instanceof FormData) { + return input; + } + return Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + const propertyContent: any[] = property instanceof Array ? property : [property]; + + for (const formItem of propertyContent) { + const isFileType = formItem instanceof Blob || formItem instanceof File; + formData.append(key, isFileType ? formItem : this.stringifyFormItem(formItem)); + } + + return formData; + }, new FormData()); + } + + public request = async ({ + secure, + path, + type, + query, + format, + body, + ...params + }: FullRequestParams): Promise => { + const secureParams = + ((typeof secure === 'boolean' ? secure : this.secure) && + this.securityWorker && + (await this.securityWorker(this.securityData))) || + {}; + const requestParams = this.mergeRequestParams(params, secureParams); + const responseFormat = format || this.format || undefined; + + if (type === ContentType.FormData && body && body !== null && typeof body === 'object') { + body = this.createFormData(body as Record); + } + + if (type === ContentType.Text && body && body !== null && typeof body !== 'string') { + body = JSON.stringify(body); + } + + return this.instance + .request({ + ...requestParams, + headers: { + ...(requestParams.headers || {}), + ...(type ? { 'Content-Type': type } : {}), + }, + params: query, + responseType: responseFormat, + data: body, + url: path, + }) + .then((response) => response.data); + }; +} diff --git a/workspaces/frontend/src/shared/api/__tests__/errorUtils.spec.ts b/workspaces/frontend/src/shared/api/__tests__/errorUtils.spec.ts deleted file mode 100644 index 099905e5..00000000 --- a/workspaces/frontend/src/shared/api/__tests__/errorUtils.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { mockNamespaces } from '~/__mocks__/mockNamespaces'; -import { mockBFFResponse } from '~/__mocks__/utils'; -import { ErrorEnvelopeException } from '~/shared/api/apiUtils'; -import { ErrorEnvelope } from '~/shared/api/backendApiTypes'; -import { handleRestFailures } from '~/shared/api/errorUtils'; -import { NotReadyError } from '~/shared/utilities/useFetchState'; - -describe('handleRestFailures', () => { - it('should successfully return namespaces', async () => { - const result = await handleRestFailures(Promise.resolve(mockBFFResponse(mockNamespaces))); - expect(result.data).toStrictEqual(mockNamespaces); - }); - - it('should handle and throw notebook errors', async () => { - const errorEnvelope: ErrorEnvelope = { - error: { - code: '', - message: '', - }, - }; - const expectedError = new ErrorEnvelopeException(errorEnvelope); - await expect(handleRestFailures(Promise.reject(errorEnvelope))).rejects.toThrow(expectedError); - }); - - it('should handle common state errors ', async () => { - await expect(handleRestFailures(Promise.reject(new NotReadyError('error')))).rejects.toThrow( - 'error', - ); - }); - - it('should handle other errors', async () => { - await expect(handleRestFailures(Promise.reject(new Error('error')))).rejects.toThrow( - 'Error communicating with server', - ); - }); -}); diff --git a/workspaces/frontend/src/shared/api/__tests__/notebookService.spec.ts b/workspaces/frontend/src/shared/api/__tests__/notebookService.spec.ts deleted file mode 100644 index 27a650b5..00000000 --- a/workspaces/frontend/src/shared/api/__tests__/notebookService.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BFF_API_VERSION } from '~/app/const'; -import { restGET, wrapRequest } from '~/shared/api/apiUtils'; -import { listNamespaces } from '~/shared/api/notebookService'; - -const mockRestResponse = { data: {} }; -const mockRestPromise = Promise.resolve(mockRestResponse); - -jest.mock('~/shared/api/apiUtils', () => ({ - restCREATE: jest.fn(() => mockRestPromise), - restGET: jest.fn(() => mockRestPromise), - restPATCH: jest.fn(() => mockRestPromise), - isNotebookResponse: jest.fn(() => true), - extractNotebookResponse: jest.fn(() => mockRestResponse), - wrapRequest: jest.fn(() => mockRestPromise), -})); - -const wrapRequestMock = jest.mocked(wrapRequest); -const restGETMock = jest.mocked(restGET); -const APIOptionsMock = {}; - -describe('getNamespaces', () => { - it('should call restGET and handleRestFailures to fetch namespaces', async () => { - const response = await listNamespaces(`/api/${BFF_API_VERSION}/namespaces`)(APIOptionsMock); - expect(response).toEqual(mockRestResponse); - expect(restGETMock).toHaveBeenCalledTimes(1); - expect(restGETMock).toHaveBeenCalledWith( - `/api/${BFF_API_VERSION}/namespaces`, - `/namespaces`, - {}, - APIOptionsMock, - ); - expect(wrapRequestMock).toHaveBeenCalledTimes(1); - expect(wrapRequestMock).toHaveBeenCalledWith(mockRestPromise); - }); -}); diff --git a/workspaces/frontend/src/shared/api/apiUtils.ts b/workspaces/frontend/src/shared/api/apiUtils.ts index 09c925e8..196f9ea9 100644 --- a/workspaces/frontend/src/shared/api/apiUtils.ts +++ b/workspaces/frontend/src/shared/api/apiUtils.ts @@ -1,243 +1,34 @@ -import { ErrorEnvelope } from '~/shared/api/backendApiTypes'; -import { handleRestFailures } from '~/shared/api/errorUtils'; -import { APIOptions, ResponseBody } from '~/shared/api/types'; -import { EitherOrNone } from '~/shared/typeHelpers'; -import { AUTH_HEADER, DEV_MODE } from '~/shared/utilities/const'; +import axios from 'axios'; +import { ApiErrorEnvelope } from '~/generated/data-contracts'; +import { ApiCallResult } from '~/shared/api/types'; -export const mergeRequestInit = ( - opts: APIOptions = {}, - specificOpts: RequestInit = {}, -): RequestInit => ({ - ...specificOpts, - ...(opts.signal && { signal: opts.signal }), - headers: { - ...(opts.headers ?? {}), - ...(specificOpts.headers ?? {}), - }, -}); - -type CallRestJSONOptions = { - queryParams?: Record; - parseJSON?: boolean; - directYAML?: boolean; -} & EitherOrNone< - { - fileContents: string; - }, - { - data: Record; - } ->; - -const callRestJSON = ( - host: string, - path: string, - requestInit: RequestInit, - { data, fileContents, queryParams, parseJSON = true, directYAML = false }: CallRestJSONOptions, -): Promise => { - const { method, ...otherOptions } = requestInit; - - const sanitizedQueryParams = queryParams - ? Object.entries(queryParams).reduce((acc, [key, value]) => { - if (value) { - return { ...acc, [key]: value }; - } - - return acc; - }, {}) - : null; - - const searchParams = sanitizedQueryParams - ? new URLSearchParams(sanitizedQueryParams).toString() - : null; - - let requestData: string | undefined; - let contentType: string | undefined; - let formData: FormData | undefined; - if (fileContents) { - if (directYAML) { - requestData = fileContents; - contentType = 'application/yaml'; - } else { - formData = new FormData(); - formData.append( - 'uploadfile', - new Blob([fileContents], { type: 'application/x-yaml' }), - 'uploadedFile.yml', - ); - } - } else if (data) { - // It's OK for contentType and requestData to BOTH be undefined for e.g. a GET request or POST with no body. - contentType = 'application/json;charset=UTF-8'; - requestData = JSON.stringify(data); +function isApiErrorEnvelope(data: unknown): data is ApiErrorEnvelope { + if (typeof data !== 'object' || data === null || !('error' in data)) { + return false; } - return fetch(`${host}${path}${searchParams ? `?${searchParams}` : ''}`, { - ...otherOptions, - headers: { - ...otherOptions.headers, - ...(DEV_MODE && { [AUTH_HEADER]: localStorage.getItem(AUTH_HEADER) }), - ...(contentType && { 'Content-Type': contentType }), - }, - method, - body: formData ?? requestData, - }).then((response) => - response.text().then((fetchedData) => { - if (parseJSON) { - return JSON.parse(fetchedData); - } - return fetchedData; - }), - ); -}; + const { error } = data as { error: unknown }; -export const restGET = ( - host: string, - path: string, - queryParams: Record = {}, - options?: APIOptions, -): Promise => - callRestJSON(host, path, mergeRequestInit(options, { method: 'GET' }), { - queryParams, - parseJSON: options?.parseJSON, - }); - -/** Standard POST */ -export const restCREATE = ( - host: string, - path: string, - data: Record, - queryParams: Record = {}, - options?: APIOptions, -): Promise => - callRestJSON(host, path, mergeRequestInit(options, { method: 'POST' }), { - data, - queryParams, - parseJSON: options?.parseJSON, - }); - -/** POST -- but with file content instead of body data */ -export const restFILE = ( - host: string, - path: string, - fileContents: string, - queryParams: Record = {}, - options?: APIOptions, -): Promise => - callRestJSON(host, path, mergeRequestInit(options, { method: 'POST' }), { - fileContents, - queryParams, - parseJSON: options?.parseJSON, - directYAML: options?.directYAML, - }); - -/** POST -- but no body data -- targets simple endpoints */ -export const restENDPOINT = ( - host: string, - path: string, - queryParams: Record = {}, - options?: APIOptions, -): Promise => - callRestJSON(host, path, mergeRequestInit(options, { method: 'POST' }), { - queryParams, - parseJSON: options?.parseJSON, - }); - -export const restUPDATE = ( - host: string, - path: string, - data: Record, - queryParams: Record = {}, - options?: APIOptions, -): Promise => - callRestJSON(host, path, mergeRequestInit(options, { method: 'PUT' }), { - data, - queryParams, - parseJSON: options?.parseJSON, - }); - -export const restPATCH = ( - host: string, - path: string, - data: Record, - options?: APIOptions, -): Promise => - callRestJSON(host, path, mergeRequestInit(options, { method: 'PATCH' }), { - data, - parseJSON: options?.parseJSON, - }); - -export const restDELETE = ( - host: string, - path: string, - data: Record, - queryParams: Record = {}, - options?: APIOptions, -): Promise => - callRestJSON(host, path, mergeRequestInit(options, { method: 'DELETE' }), { - data, - queryParams, - parseJSON: options?.parseJSON, - }); - -export const isNotebookResponse = (response: unknown): response is ResponseBody => { - if (typeof response === 'object' && response !== null) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const notebookBody = response as { data?: T }; - return notebookBody.data !== undefined; + if (typeof error !== 'object' || error === null || !('message' in error)) { + return false; } - return false; -}; -export const isErrorEnvelope = (e: unknown): e is ErrorEnvelope => - typeof e === 'object' && - e !== null && - 'error' in e && - typeof (e as Record).error === 'object' && - (e as { error: unknown }).error !== null && - typeof (e as { error: { message: unknown } }).error.message === 'string'; + const { message } = error as { message?: unknown }; -export function extractNotebookResponse(response: unknown): T { - // Check if this is an error envelope first - if (isErrorEnvelope(response)) { - throw new ErrorEnvelopeException(response); - } - if (isNotebookResponse(response)) { - return response.data; - } - throw new Error('Invalid response format'); + return typeof message === 'string'; } -export function extractErrorEnvelope(error: unknown): ErrorEnvelope { - if (isErrorEnvelope(error)) { - return error; - } - - const message = - error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unexpected error'; - - return { - error: { - message, - code: 'UNKNOWN_ERROR', - }, - }; -} - -export async function wrapRequest(promise: Promise, extractData = true): Promise { +export async function safeApiCall(fn: () => Promise): Promise> { try { - const res = await handleRestFailures(promise); - return extractData ? extractNotebookResponse(res) : res; - } catch (error) { - if (error instanceof ErrorEnvelopeException) { - throw error; + const data = await fn(); + return { ok: true, data }; + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + const apiError = error.response?.data; + if (apiError && isApiErrorEnvelope(apiError)) { + return { ok: false, errorEnvelope: apiError }; + } } - throw new ErrorEnvelopeException(extractErrorEnvelope(error)); - } -} - -export class ErrorEnvelopeException extends Error { - constructor(public envelope: ErrorEnvelope) { - super(envelope.error?.message ?? 'Unknown error'); + throw error; } } diff --git a/workspaces/frontend/src/shared/api/backendApiTypes.ts b/workspaces/frontend/src/shared/api/backendApiTypes.ts deleted file mode 100644 index 643c300a..00000000 --- a/workspaces/frontend/src/shared/api/backendApiTypes.ts +++ /dev/null @@ -1,322 +0,0 @@ -export enum WorkspaceServiceStatus { - ServiceStatusHealthy = 'Healthy', - ServiceStatusUnhealthy = 'Unhealthy', -} - -export interface WorkspaceSystemInfo { - version: string; -} - -export interface HealthCheckResponse { - status: WorkspaceServiceStatus; - systemInfo: WorkspaceSystemInfo; -} - -export interface Namespace { - name: string; -} - -export interface WorkspaceImageRef { - url: string; -} - -export interface WorkspacePodConfigValue { - id: string; - displayName: string; - description: string; - labels: WorkspaceOptionLabel[]; - hidden: boolean; - redirect?: WorkspaceOptionRedirect; - clusterMetrics?: WorkspaceKindClusterMetrics; -} - -export interface WorkspaceKindPodConfig { - default: string; - values: WorkspacePodConfigValue[]; -} - -export interface WorkspaceKindPodMetadata { - labels: Record; - annotations: Record; -} - -export interface WorkspacePodVolumeMounts { - home: string; -} - -export interface WorkspaceOptionLabel { - key: string; - value: string; -} - -export enum WorkspaceRedirectMessageLevel { - RedirectMessageLevelInfo = 'Info', - RedirectMessageLevelWarning = 'Warning', - RedirectMessageLevelDanger = 'Danger', -} - -export interface WorkspaceRedirectMessage { - text: string; - level: WorkspaceRedirectMessageLevel; -} - -export interface WorkspaceOptionRedirect { - to: string; - message?: WorkspaceRedirectMessage; -} - -export interface WorkspaceImageConfigValue { - id: string; - displayName: string; - description: string; - labels: WorkspaceOptionLabel[]; - hidden: boolean; - redirect?: WorkspaceOptionRedirect; - clusterMetrics?: WorkspaceKindClusterMetrics; -} - -export interface WorkspaceKindImageConfig { - default: string; - values: WorkspaceImageConfigValue[]; -} - -export interface WorkspaceKindPodTemplateOptions { - imageConfig: WorkspaceKindImageConfig; - podConfig: WorkspaceKindPodConfig; -} - -export interface WorkspaceKindPodTemplate { - podMetadata: WorkspaceKindPodMetadata; - volumeMounts: WorkspacePodVolumeMounts; - options: WorkspaceKindPodTemplateOptions; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export type WorkspaceKindCreate = string; - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface WorkspaceKindUpdate {} - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface WorkspaceKindPatch {} - -export interface WorkspaceKind { - name: string; - displayName: string; - description: string; - deprecated: boolean; - deprecationMessage: string; - hidden: boolean; - icon: WorkspaceImageRef; - logo: WorkspaceImageRef; - clusterMetrics?: WorkspaceKindClusterMetrics; - podTemplate: WorkspaceKindPodTemplate; -} - -export interface WorkspaceKindClusterMetrics { - workspacesCount: number; -} - -export enum WorkspaceState { - WorkspaceStateRunning = 'Running', - WorkspaceStateTerminating = 'Terminating', - WorkspaceStatePaused = 'Paused', - WorkspaceStatePending = 'Pending', - WorkspaceStateError = 'Error', - WorkspaceStateUnknown = 'Unknown', -} - -export interface WorkspaceKindInfo { - name: string; - missing: boolean; - icon: WorkspaceImageRef; - logo: WorkspaceImageRef; -} - -export interface WorkspacePodMetadata { - labels: Record; - annotations: Record; -} - -export interface WorkspacePodVolumeInfo { - pvcName: string; - mountPath: string; - readOnly: boolean; -} - -export interface WorkspacePodSecretInfo { - secretName: string; - mountPath: string; - defaultMode?: number; -} - -export interface WorkspaceOptionInfo { - id: string; - displayName: string; - description: string; - labels: WorkspaceOptionLabel[]; -} - -export interface WorkspaceRedirectStep { - sourceId: string; - targetId: string; - message?: WorkspaceRedirectMessage; -} - -export interface WorkspaceImageConfig { - current: WorkspaceOptionInfo; - desired?: WorkspaceOptionInfo; - redirectChain?: WorkspaceRedirectStep[]; -} - -export interface WorkspacePodConfig { - current: WorkspaceOptionInfo; - desired?: WorkspaceOptionInfo; - redirectChain?: WorkspaceRedirectStep[]; -} - -export interface WorkspacePodTemplateOptions { - imageConfig: WorkspaceImageConfig; - podConfig: WorkspacePodConfig; -} - -export interface WorkspacePodVolumes { - home?: WorkspacePodVolumeInfo; - data: WorkspacePodVolumeInfo[]; - secrets?: WorkspacePodSecretInfo[]; -} - -export interface WorkspacePodTemplate { - podMetadata: WorkspacePodMetadata; - volumes: WorkspacePodVolumes; - options: WorkspacePodTemplateOptions; -} - -export enum WorkspaceProbeResult { - ProbeResultSuccess = 'Success', - ProbeResultFailure = 'Failure', - ProbeResultTimeout = 'Timeout', -} - -export interface WorkspaceLastProbeInfo { - startTimeMs: number; - endTimeMs: number; - result: WorkspaceProbeResult; - message: string; -} - -export interface WorkspaceActivity { - lastActivity: number; - lastUpdate: number; - lastProbe?: WorkspaceLastProbeInfo; -} - -export interface WorkspaceHttpService { - displayName: string; - httpPath: string; -} - -export interface WorkspaceService { - httpService?: WorkspaceHttpService; -} - -export interface WorkspacePodMetadataMutate { - labels: Record; - annotations: Record; -} - -export interface WorkspacePodVolumeMount { - pvcName: string; - mountPath: string; - readOnly?: boolean; -} - -export interface WorkspacePodSecretMount { - secretName: string; - mountPath: string; - defaultMode?: number; -} - -export interface WorkspacePodVolumesMutate { - home?: string; - data?: WorkspacePodVolumeMount[]; - secrets?: WorkspacePodSecretMount[]; -} - -export interface WorkspacePodTemplateOptionsMutate { - imageConfig: string; - podConfig: string; -} - -export interface WorkspacePodTemplateMutate { - podMetadata: WorkspacePodMetadataMutate; - volumes: WorkspacePodVolumesMutate; - options: WorkspacePodTemplateOptionsMutate; -} - -export interface Workspace { - name: string; - namespace: string; - workspaceKind: WorkspaceKindInfo; - deferUpdates: boolean; - paused: boolean; - pausedTime: number; - pendingRestart: boolean; - state: WorkspaceState; - stateMessage: string; - podTemplate: WorkspacePodTemplate; - activity: WorkspaceActivity; - services: WorkspaceService[]; -} - -export interface WorkspaceCreate { - name: string; - kind: string; - paused: boolean; - deferUpdates: boolean; - podTemplate: WorkspacePodTemplateMutate; -} - -// TODO: Update this type when applicable; meanwhile, it inherits from WorkspaceCreate -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface WorkspaceUpdate extends WorkspaceCreate {} - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface WorkspacePatch {} - -export interface WorkspacePauseState { - paused: boolean; -} - -export enum FieldErrorType { - FieldValueRequired = 'FieldValueRequired', - FieldValueInvalid = 'FieldValueInvalid', - FieldValueNotSupported = 'FieldValueNotSupported', - FieldValueDuplicate = 'FieldValueDuplicate', - FieldValueTooLong = 'FieldValueTooLong', - FieldValueForbidden = 'FieldValueForbidden', - FieldValueNotFound = 'FieldValueNotFound', - FieldValueConflict = 'FieldValueConflict', - FieldValueTooShort = 'FieldValueTooShort', - FieldValueUnknown = 'FieldValueUnknown', -} - -export interface ValidationError { - type: FieldErrorType; - field: string; - message: string; -} - -export interface ErrorCause { - validation_errors?: ValidationError[]; // TODO: backend is not using camelCase for this field -} - -export type HTTPError = { - code: string; - message: string; - cause?: ErrorCause; -}; - -export type ErrorEnvelope = { - error: HTTPError | null; -}; diff --git a/workspaces/frontend/src/shared/api/callTypes.ts b/workspaces/frontend/src/shared/api/callTypes.ts deleted file mode 100644 index 72a8d6bc..00000000 --- a/workspaces/frontend/src/shared/api/callTypes.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - CreateWorkspace, - CreateWorkspaceKind, - DeleteWorkspace, - DeleteWorkspaceKind, - GetHealthCheck, - GetWorkspace, - GetWorkspaceKind, - ListAllWorkspaces, - ListNamespaces, - ListWorkspaceKinds, - ListWorkspaces, - PatchWorkspace, - PatchWorkspaceKind, - PauseWorkspace, - UpdateWorkspace, - UpdateWorkspaceKind, -} from '~/shared/api/notebookApi'; -import { APIOptions } from '~/shared/api/types'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type KubeflowSpecificAPICall = (opts: APIOptions, ...args: any[]) => Promise; -type KubeflowAPICall = (hostPath: string) => ActualCall; - -// Health -export type GetHealthCheckAPI = KubeflowAPICall; - -// Namespace -export type ListNamespacesAPI = KubeflowAPICall; - -// Workspace -export type ListAllWorkspacesAPI = KubeflowAPICall; -export type ListWorkspacesAPI = KubeflowAPICall; -export type CreateWorkspaceAPI = KubeflowAPICall; -export type GetWorkspaceAPI = KubeflowAPICall; -export type UpdateWorkspaceAPI = KubeflowAPICall; -export type PatchWorkspaceAPI = KubeflowAPICall; -export type DeleteWorkspaceAPI = KubeflowAPICall; -export type PauseWorkspaceAPI = KubeflowAPICall; - -// WorkspaceKind -export type ListWorkspaceKindsAPI = KubeflowAPICall; -export type CreateWorkspaceKindAPI = KubeflowAPICall; -export type GetWorkspaceKindAPI = KubeflowAPICall; -export type UpdateWorkspaceKindAPI = KubeflowAPICall; -export type PatchWorkspaceKindAPI = KubeflowAPICall; -export type DeleteWorkspaceKindAPI = KubeflowAPICall; diff --git a/workspaces/frontend/src/shared/api/errorUtils.ts b/workspaces/frontend/src/shared/api/errorUtils.ts deleted file mode 100644 index 9205a8f0..00000000 --- a/workspaces/frontend/src/shared/api/errorUtils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ErrorEnvelopeException, isErrorEnvelope } from '~/shared/api//apiUtils'; -import { isCommonStateError } from '~/shared/utilities/useFetchState'; - -export const handleRestFailures = (promise: Promise): Promise => - promise.catch((e) => { - if (isErrorEnvelope(e)) { - throw new ErrorEnvelopeException(e); - } - if (isCommonStateError(e)) { - // Common state errors are handled by useFetchState at storage level, let them deal with it - throw e; - } - // eslint-disable-next-line no-console - console.error('Unknown API error', e); - throw new Error('Error communicating with server'); - }); diff --git a/workspaces/frontend/src/shared/api/experimental.ts b/workspaces/frontend/src/shared/api/experimental.ts new file mode 100644 index 00000000..14e7d1c4 --- /dev/null +++ b/workspaces/frontend/src/shared/api/experimental.ts @@ -0,0 +1,23 @@ +/** + * ----------------------------------------------------------------------------- + * Experimental API Extensions + * ----------------------------------------------------------------------------- + * + * This file contains manually implemented API endpoints that are not yet + * available in the official Swagger specification provided by the backend. + * + * The structure, naming, and typing follow the same conventions as the + * `swagger-typescript-api` generated clients (HttpClient, RequestParams, etc.) + * under `src/generated` folder to ensure consistency across the codebase + * and future compatibility. + * + * These endpoints are "experimental" in the sense that they either: + * - Reflect endpoints that exist but are not documented in the Swagger spec. + * - Represent planned or internal APIs not yet formalized by the backend. + * + * Once the backend Swagger specification includes these endpoints, code in this + * file should be removed, and the corresponding generated modules should be + * used instead. + */ + +// NOTE: All available endpoints are already implemented in the generated code. diff --git a/workspaces/frontend/src/shared/api/notebookApi.ts b/workspaces/frontend/src/shared/api/notebookApi.ts index bf2f2bce..ba4efb0b 100644 --- a/workspaces/frontend/src/shared/api/notebookApi.ts +++ b/workspaces/frontend/src/shared/api/notebookApi.ts @@ -1,95 +1,23 @@ -import { - HealthCheckResponse, - Namespace, - Workspace, - WorkspaceCreate, - WorkspaceKind, - WorkspaceKindPatch, - WorkspaceKindUpdate, - WorkspacePatch, - WorkspacePauseState, - WorkspaceUpdate, -} from '~/shared/api/backendApiTypes'; -import { APIOptions, RequestData } from '~/shared/api/types'; +import { Healthcheck } from '~/generated/Healthcheck'; +import { Namespaces } from '~/generated/Namespaces'; +import { Workspacekinds } from '~/generated/Workspacekinds'; +import { Workspaces } from '~/generated/Workspaces'; +import { ApiInstance } from '~/shared/api/types'; -// Health -export type GetHealthCheck = (opts: APIOptions) => Promise; +export interface NotebookApis { + healthCheck: ApiInstance; + namespaces: ApiInstance; + workspaces: ApiInstance; + workspaceKinds: ApiInstance; +} -// Namespace -export type ListNamespaces = (opts: APIOptions) => Promise; +export const notebookApisImpl = (path: string): NotebookApis => { + const commonConfig = { baseURL: path }; -// Workspace -export type ListAllWorkspaces = (opts: APIOptions) => Promise; -export type ListWorkspaces = (opts: APIOptions, namespace: string) => Promise; -export type GetWorkspace = ( - opts: APIOptions, - namespace: string, - workspace: string, -) => Promise; -export type CreateWorkspace = ( - opts: APIOptions, - namespace: string, - data: RequestData, -) => Promise; -export type UpdateWorkspace = ( - opts: APIOptions, - namespace: string, - workspace: string, - data: RequestData, -) => Promise; -export type PatchWorkspace = ( - opts: APIOptions, - namespace: string, - workspace: string, - data: RequestData, -) => Promise; -export type DeleteWorkspace = ( - opts: APIOptions, - namespace: string, - workspace: string, -) => Promise; -export type PauseWorkspace = ( - opts: APIOptions, - namespace: string, - workspace: string, - data: RequestData, -) => Promise; - -// WorkspaceKind -export type ListWorkspaceKinds = (opts: APIOptions) => Promise; -export type GetWorkspaceKind = (opts: APIOptions, kind: string) => Promise; -export type CreateWorkspaceKind = (opts: APIOptions, data: string) => Promise; -export type UpdateWorkspaceKind = ( - opts: APIOptions, - kind: string, - data: RequestData, -) => Promise; -export type PatchWorkspaceKind = ( - opts: APIOptions, - kind: string, - data: RequestData, -) => Promise; -export type DeleteWorkspaceKind = (opts: APIOptions, kind: string) => Promise; - -export type NotebookAPIs = { - // Health - getHealthCheck: GetHealthCheck; - // Namespace - listNamespaces: ListNamespaces; - // Workspace - listAllWorkspaces: ListAllWorkspaces; - listWorkspaces: ListWorkspaces; - getWorkspace: GetWorkspace; - createWorkspace: CreateWorkspace; - updateWorkspace: UpdateWorkspace; - patchWorkspace: PatchWorkspace; - deleteWorkspace: DeleteWorkspace; - pauseWorkspace: PauseWorkspace; - // WorkspaceKind - listWorkspaceKinds: ListWorkspaceKinds; - getWorkspaceKind: GetWorkspaceKind; - createWorkspaceKind: CreateWorkspaceKind; - updateWorkspaceKind: UpdateWorkspaceKind; - patchWorkspaceKind: PatchWorkspaceKind; - deleteWorkspaceKind: DeleteWorkspaceKind; + return { + healthCheck: new Healthcheck(commonConfig), + namespaces: new Namespaces(commonConfig), + workspaces: new Workspaces(commonConfig), + workspaceKinds: new Workspacekinds(commonConfig), + }; }; diff --git a/workspaces/frontend/src/shared/api/notebookService.ts b/workspaces/frontend/src/shared/api/notebookService.ts deleted file mode 100644 index 7df0890d..00000000 --- a/workspaces/frontend/src/shared/api/notebookService.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - restCREATE, - restDELETE, - restFILE, - restGET, - restPATCH, - restUPDATE, - wrapRequest, -} from '~/shared/api/apiUtils'; -import { - CreateWorkspaceAPI, - CreateWorkspaceKindAPI, - DeleteWorkspaceAPI, - DeleteWorkspaceKindAPI, - GetHealthCheckAPI, - GetWorkspaceAPI, - GetWorkspaceKindAPI, - ListAllWorkspacesAPI, - ListNamespacesAPI, - ListWorkspaceKindsAPI, - ListWorkspacesAPI, - PatchWorkspaceAPI, - PatchWorkspaceKindAPI, - PauseWorkspaceAPI, - UpdateWorkspaceAPI, - UpdateWorkspaceKindAPI, -} from '~/shared/api/callTypes'; - -export const getHealthCheck: GetHealthCheckAPI = (hostPath) => (opts) => - wrapRequest(restGET(hostPath, `/healthcheck`, {}, opts), false); - -export const listNamespaces: ListNamespacesAPI = (hostPath) => (opts) => - wrapRequest(restGET(hostPath, `/namespaces`, {}, opts)); - -export const listAllWorkspaces: ListAllWorkspacesAPI = (hostPath) => (opts) => - wrapRequest(restGET(hostPath, `/workspaces`, {}, opts)); - -export const listWorkspaces: ListWorkspacesAPI = (hostPath) => (opts, namespace) => - wrapRequest(restGET(hostPath, `/workspaces/${namespace}`, {}, opts)); - -export const getWorkspace: GetWorkspaceAPI = (hostPath) => (opts, namespace, workspace) => - wrapRequest(restGET(hostPath, `/workspaces/${namespace}/${workspace}`, {}, opts)); - -export const createWorkspace: CreateWorkspaceAPI = (hostPath) => (opts, namespace, data) => - wrapRequest(restCREATE(hostPath, `/workspaces/${namespace}`, data, {}, opts)); - -export const updateWorkspace: UpdateWorkspaceAPI = - (hostPath) => (opts, namespace, workspace, data) => - wrapRequest(restUPDATE(hostPath, `/workspaces/${namespace}/${workspace}`, data, {}, opts)); - -export const patchWorkspace: PatchWorkspaceAPI = (hostPath) => (opts, namespace, workspace, data) => - wrapRequest(restPATCH(hostPath, `/workspaces/${namespace}/${workspace}`, data, opts)); - -export const deleteWorkspace: DeleteWorkspaceAPI = (hostPath) => (opts, namespace, workspace) => - wrapRequest(restDELETE(hostPath, `/workspaces/${namespace}/${workspace}`, {}, {}, opts), false); - -export const pauseWorkspace: PauseWorkspaceAPI = (hostPath) => (opts, namespace, workspace, data) => - wrapRequest( - restCREATE(hostPath, `/workspaces/${namespace}/${workspace}/actions/pause`, data, {}, opts), - ); - -export const listWorkspaceKinds: ListWorkspaceKindsAPI = (hostPath) => (opts) => - wrapRequest(restGET(hostPath, `/workspacekinds`, {}, opts)); - -export const getWorkspaceKind: GetWorkspaceKindAPI = (hostPath) => (opts, kind) => - wrapRequest(restGET(hostPath, `/workspacekinds/${kind}`, {}, opts)); - -export const createWorkspaceKind: CreateWorkspaceKindAPI = (hostPath) => (opts, data) => - wrapRequest(restFILE(hostPath, `/workspacekinds`, data, {}, opts)); - -export const updateWorkspaceKind: UpdateWorkspaceKindAPI = (hostPath) => (opts, kind, data) => - wrapRequest(restUPDATE(hostPath, `/workspacekinds/${kind}`, data, {}, opts)); - -export const patchWorkspaceKind: PatchWorkspaceKindAPI = (hostPath) => (opts, kind, data) => - wrapRequest(restPATCH(hostPath, `/workspacekinds/${kind}`, data, opts)); - -export const deleteWorkspaceKind: DeleteWorkspaceKindAPI = (hostPath) => (opts, kind) => - wrapRequest(restDELETE(hostPath, `/workspacekinds/${kind}`, {}, {}, opts), false); diff --git a/workspaces/frontend/src/shared/api/types.ts b/workspaces/frontend/src/shared/api/types.ts index 7c2bad1a..6685d177 100644 --- a/workspaces/frontend/src/shared/api/types.ts +++ b/workspaces/frontend/src/shared/api/types.ts @@ -1,9 +1,11 @@ +import { ApiErrorEnvelope } from '~/generated/data-contracts'; +import { ApiConfig, HttpClient } from '~/generated/http-client'; + export type APIOptions = { dryRun?: boolean; signal?: AbortSignal; parseJSON?: boolean; headers?: Record; - directYAML?: boolean; }; export type APIState = { @@ -13,11 +15,14 @@ export type APIState = { api: T; }; -export type ResponseBody = { - data: T; - metadata?: Record; +export type RemoveHttpClient = Omit>; + +export type WithExperimental = TBase & { + experimental: TExperimental; }; -export type RequestData = { - data: T; -}; +export type ApiClass = abstract new (config?: ApiConfig) => object; +export type ApiInstance = RemoveHttpClient>; +export type ApiCallResult = + | { ok: true; data: T } + | { ok: false; errorEnvelope: ApiErrorEnvelope }; diff --git a/workspaces/frontend/src/shared/mock/mockBuilder.ts b/workspaces/frontend/src/shared/mock/mockBuilder.ts index 8b6eeccc..ef4434b2 100644 --- a/workspaces/frontend/src/shared/mock/mockBuilder.ts +++ b/workspaces/frontend/src/shared/mock/mockBuilder.ts @@ -1,31 +1,33 @@ import { - HealthCheckResponse, - Namespace, - Workspace, - WorkspaceKind, - WorkspaceKindInfo, - WorkspacePauseState, - WorkspaceRedirectMessageLevel, - WorkspaceServiceStatus, - WorkspaceState, -} from '~/shared/api/backendApiTypes'; + ActionsWorkspaceActionPause, + HealthCheckHealthCheck, + HealthCheckServiceStatus, + NamespacesNamespace, + WorkspacekindsRedirectMessageLevel, + WorkspacekindsWorkspaceKind, + WorkspacesWorkspace, + WorkspacesWorkspaceKindInfo, + WorkspacesWorkspaceState, +} from '~/generated/data-contracts'; export const buildMockHealthCheckResponse = ( - healthCheckResponse?: Partial, -): HealthCheckResponse => ({ - status: WorkspaceServiceStatus.ServiceStatusHealthy, + healthCheckResponse?: Partial, +): HealthCheckHealthCheck => ({ + status: HealthCheckServiceStatus.ServiceStatusHealthy, systemInfo: { version: '1.0.0' }, ...healthCheckResponse, }); -export const buildMockNamespace = (namespace?: Partial): Namespace => ({ +export const buildMockNamespace = ( + namespace?: Partial, +): NamespacesNamespace => ({ name: 'default', ...namespace, }); export const buildMockWorkspaceKindInfo = ( - workspaceKindInfo?: Partial, -): WorkspaceKindInfo => ({ + workspaceKindInfo?: Partial, +): WorkspacesWorkspaceKindInfo => ({ name: 'jupyterlab', missing: false, icon: { @@ -37,14 +39,16 @@ export const buildMockWorkspaceKindInfo = ( ...workspaceKindInfo, }); -export const buildMockWorkspace = (workspace?: Partial): Workspace => ({ +export const buildMockWorkspace = ( + workspace?: Partial, +): WorkspacesWorkspace => ({ name: 'My First Jupyter Notebook', namespace: 'default', workspaceKind: buildMockWorkspaceKindInfo(), paused: true, deferUpdates: true, pausedTime: new Date(2025, 3, 1).getTime(), - state: WorkspaceState.WorkspaceStateRunning, + state: WorkspacesWorkspaceState.WorkspaceStateRunning, stateMessage: 'Workspace is running', podTemplate: { podMetadata: { @@ -133,7 +137,9 @@ export const buildMockWorkspace = (workspace?: Partial): Workspace => ...workspace, }); -export const buildMockWorkspaceKind = (workspaceKind?: Partial): WorkspaceKind => ({ +export const buildMockWorkspaceKind = ( + workspaceKind?: Partial, +): WorkspacekindsWorkspaceKind => ({ name: 'jupyterlab', displayName: 'JupyterLab Notebook', description: 'A Workspace which runs JupyterLab in a Pod', @@ -182,7 +188,7 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): to: 'jupyterlab_scipy_190', message: { text: 'This update will change...', - level: WorkspaceRedirectMessageLevel.RedirectMessageLevelInfo, + level: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelInfo, }, }, }, @@ -199,7 +205,7 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): to: 'jupyterlab_scipy_200', message: { text: 'This update will change...', - level: WorkspaceRedirectMessageLevel.RedirectMessageLevelWarning, + level: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelWarning, }, }, clusterMetrics: { @@ -219,7 +225,7 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): to: 'jupyterlab_scipy_210', message: { text: 'This update will change...', - level: WorkspaceRedirectMessageLevel.RedirectMessageLevelWarning, + level: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelWarning, }, }, clusterMetrics: { @@ -239,7 +245,7 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): to: 'jupyterlab_scipy_220', message: { text: 'This update will change...', - level: WorkspaceRedirectMessageLevel.RedirectMessageLevelWarning, + level: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelWarning, }, }, clusterMetrics: { @@ -264,7 +270,7 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): to: 'small_cpu', message: { text: 'This update will change...', - level: WorkspaceRedirectMessageLevel.RedirectMessageLevelDanger, + level: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelDanger, }, }, clusterMetrics: { @@ -285,7 +291,7 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): to: 'large_cpu', message: { text: 'This update will change...', - level: WorkspaceRedirectMessageLevel.RedirectMessageLevelDanger, + level: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelDanger, }, }, clusterMetrics: { @@ -299,9 +305,9 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): ...workspaceKind, }); -export const buildMockPauseStateResponse = ( - pauseState?: Partial, -): WorkspacePauseState => ({ +export const buildMockActionsWorkspaceActionPause = ( + pauseState?: Partial, +): ActionsWorkspaceActionPause => ({ paused: true, ...pauseState, }); @@ -309,9 +315,9 @@ export const buildMockPauseStateResponse = ( export const buildMockWorkspaceList = (args: { count: number; namespace: string; - kind: WorkspaceKindInfo; -}): Workspace[] => { - const states = Object.values(WorkspaceState); + kind: WorkspacesWorkspaceKindInfo; +}): WorkspacesWorkspace[] => { + const states = Object.values(WorkspacesWorkspaceState); const imageConfigs = [ { id: 'jupyterlab_scipy_190', @@ -345,7 +351,7 @@ export const buildMockWorkspaceList = (args: { { id: 'large_cpu', displayName: 'Large CPU' }, ]; - const workspaces: Workspace[] = []; + const workspaces: WorkspacesWorkspace[] = []; for (let i = 1; i <= args.count; i++) { const state = states[(i - 1) % states.length]; const labels = { @@ -368,7 +374,7 @@ export const buildMockWorkspaceList = (args: { workspaceKind: args.kind, state, stateMessage: `Workspace is in ${state} state`, - paused: state === WorkspaceState.WorkspaceStatePaused, + paused: state === WorkspacesWorkspaceState.WorkspaceStatePaused, pendingRestart: booleanValue, podTemplate: { podMetadata: { labels, annotations }, diff --git a/workspaces/frontend/src/shared/mock/mockNotebookApis.ts b/workspaces/frontend/src/shared/mock/mockNotebookApis.ts new file mode 100644 index 00000000..3947b6ee --- /dev/null +++ b/workspaces/frontend/src/shared/mock/mockNotebookApis.ts @@ -0,0 +1,83 @@ +import { ApiErrorEnvelope, FieldErrorType } from '~/generated/data-contracts'; +import { NotebookApis } from '~/shared/api/notebookApi'; +import { + mockAllWorkspaces, + mockedHealthCheckResponse, + mockNamespaces, + mockWorkspace1, + mockWorkspaceKind1, + mockWorkspaceKinds, +} from '~/shared/mock/mockNotebookServiceData'; +import { buildAxiosError, isInvalidYaml } from '~/shared/mock/mockUtils'; + +const delay = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +export const mockNotebookApisImpl = (): NotebookApis => ({ + healthCheck: { + getHealthcheck: async () => mockedHealthCheckResponse, + }, + namespaces: { + listNamespaces: async () => ({ data: mockNamespaces }), + }, + workspaces: { + listAllWorkspaces: async () => ({ data: mockAllWorkspaces }), + listWorkspacesByNamespace: async (namespace) => ({ + data: mockAllWorkspaces.filter((w) => w.namespace === namespace), + }), + getWorkspace: async (namespace, workspace) => ({ + data: mockAllWorkspaces.find((w) => w.name === workspace && w.namespace === namespace)!, + }), + createWorkspace: async () => ({ data: mockWorkspace1 }), + deleteWorkspace: async () => { + await delay(1500); + }, + updateWorkspacePauseState: async (_namespace, _workspaceName, body) => { + await delay(1500); + return { + data: { paused: body.data.paused }, + }; + }, + }, + workspaceKinds: { + listWorkspaceKinds: async () => ({ data: mockWorkspaceKinds }), + getWorkspaceKind: async (kind) => ({ + data: mockWorkspaceKinds.find((w) => w.name === kind)!, + }), + createWorkspaceKind: async (body) => { + if (isInvalidYaml(body)) { + const apiErrorEnvelope: ApiErrorEnvelope = { + error: { + code: 'invalid_yaml', + message: 'Invalid YAML provided', + cause: { + // eslint-disable-next-line camelcase + validation_errors: [ + { + type: FieldErrorType.ErrorTypeRequired, + field: 'spec.spawner.displayName', + message: "Missing required 'spec.spawner.displayName' property", + }, + { + type: FieldErrorType.ErrorTypeInvalid, + field: 'spec.spawner.xyz', + message: "Unknown property 'spec.spawner.xyz'", + }, + { + type: FieldErrorType.ErrorTypeNotSupported, + field: 'spec.spawner.hidden', + message: "Invalid data type for 'spec.spawner.hidden', expected 'boolean'", + }, + ], + }, + }, + }; + + throw buildAxiosError(apiErrorEnvelope); + } + return { data: mockWorkspaceKind1 }; + }, + }, +}); diff --git a/workspaces/frontend/src/shared/mock/mockNotebookService.ts b/workspaces/frontend/src/shared/mock/mockNotebookService.ts deleted file mode 100644 index 78767f03..00000000 --- a/workspaces/frontend/src/shared/mock/mockNotebookService.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { ErrorEnvelopeException } from '~/shared/api/apiUtils'; -import { FieldErrorType } from '~/shared/api/backendApiTypes'; -import { - CreateWorkspaceAPI, - CreateWorkspaceKindAPI, - DeleteWorkspaceAPI, - DeleteWorkspaceKindAPI, - GetHealthCheckAPI, - GetWorkspaceAPI, - GetWorkspaceKindAPI, - ListAllWorkspacesAPI, - ListNamespacesAPI, - ListWorkspaceKindsAPI, - ListWorkspacesAPI, - PatchWorkspaceAPI, - PatchWorkspaceKindAPI, - PauseWorkspaceAPI, - UpdateWorkspaceAPI, - UpdateWorkspaceKindAPI, -} from '~/shared/api/callTypes'; -import { - mockAllWorkspaces, - mockedHealthCheckResponse, - mockNamespaces, - mockWorkspace1, - mockWorkspaceKind1, - mockWorkspaceKinds, -} from '~/shared/mock/mockNotebookServiceData'; -import { isInvalidYaml } from '~/shared/mock/mockUtils'; - -const delay = (ms: number) => - new Promise((resolve) => { - setTimeout(resolve, ms); - }); - -export const mockGetHealthCheck: GetHealthCheckAPI = () => async () => mockedHealthCheckResponse; - -export const mockListNamespaces: ListNamespacesAPI = () => async () => mockNamespaces; - -export const mockListAllWorkspaces: ListAllWorkspacesAPI = () => async () => mockAllWorkspaces; - -export const mockListWorkspaces: ListWorkspacesAPI = () => async (_opts, namespace) => - mockAllWorkspaces.filter((workspace) => workspace.namespace === namespace); - -export const mockGetWorkspace: GetWorkspaceAPI = () => async (_opts, namespace, workspace) => - mockAllWorkspaces.find((w) => w.name === workspace && w.namespace === namespace)!; - -export const mockCreateWorkspace: CreateWorkspaceAPI = () => async () => mockWorkspace1; - -export const mockUpdateWorkspace: UpdateWorkspaceAPI = () => async () => mockWorkspace1; - -export const mockPatchWorkspace: PatchWorkspaceAPI = () => async () => mockWorkspace1; - -export const mockDeleteWorkspace: DeleteWorkspaceAPI = () => async () => { - await delay(1500); -}; - -export const mockPauseWorkspace: PauseWorkspaceAPI = - () => async (_opts, _namespace, _workspace, requestData) => { - await delay(1500); - return { paused: requestData.data.paused }; - }; - -export const mockListWorkspaceKinds: ListWorkspaceKindsAPI = () => async () => mockWorkspaceKinds; - -export const mockGetWorkspaceKind: GetWorkspaceKindAPI = () => async (_opts, kind) => - mockWorkspaceKinds.find((w) => w.name === kind)!; - -export const mockCreateWorkspaceKind: CreateWorkspaceKindAPI = () => async (_opts, data) => { - if (isInvalidYaml(data)) { - throw new ErrorEnvelopeException({ - error: { - code: 'invalid_yaml', - message: 'Invalid YAML provided', - cause: { - // eslint-disable-next-line camelcase - validation_errors: [ - { - type: FieldErrorType.FieldValueRequired, - field: 'spec.spawner.displayName', - message: "Missing required 'spec.spawner.displayName' property", - }, - { - type: FieldErrorType.FieldValueUnknown, - field: 'spec.spawner.xyz', - message: "Unknown property 'spec.spawner.xyz'", - }, - { - type: FieldErrorType.FieldValueNotSupported, - field: 'spec.spawner.hidden', - message: "Invalid data type for 'spec.spawner.hidden', expected 'boolean'", - }, - ], - }, - }, - }); - } - return mockWorkspaceKind1; -}; - -export const mockUpdateWorkspaceKind: UpdateWorkspaceKindAPI = () => async () => mockWorkspaceKind1; - -export const mockPatchWorkspaceKind: PatchWorkspaceKindAPI = () => async () => mockWorkspaceKind1; - -export const mockDeleteWorkspaceKind: DeleteWorkspaceKindAPI = () => async () => { - await delay(1500); -}; diff --git a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts index fdbf7de0..8a2cf687 100644 --- a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts +++ b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts @@ -1,9 +1,9 @@ import { - Workspace, - WorkspaceKind, - WorkspaceKindInfo, - WorkspaceState, -} from '~/shared/api/backendApiTypes'; + WorkspacekindsWorkspaceKind, + WorkspacesWorkspace, + WorkspacesWorkspaceKindInfo, + WorkspacesWorkspaceState, +} from '~/generated/data-contracts'; import { buildMockHealthCheckResponse, buildMockNamespace, @@ -24,7 +24,7 @@ export const mockNamespace3 = buildMockNamespace({ name: 'workspace-test-3' }); export const mockNamespaces = [mockNamespace1, mockNamespace2, mockNamespace3]; // WorkspaceKind -export const mockWorkspaceKind1: WorkspaceKind = buildMockWorkspaceKind({ +export const mockWorkspaceKind1: WorkspacekindsWorkspaceKind = buildMockWorkspaceKind({ name: 'jupyterlab1', displayName: 'JupyterLab Notebook 1', clusterMetrics: { @@ -32,7 +32,7 @@ export const mockWorkspaceKind1: WorkspaceKind = buildMockWorkspaceKind({ }, }); -export const mockWorkspaceKind2: WorkspaceKind = buildMockWorkspaceKind({ +export const mockWorkspaceKind2: WorkspacekindsWorkspaceKind = buildMockWorkspaceKind({ name: 'jupyterlab2', displayName: 'JupyterLab Notebook 2', clusterMetrics: { @@ -40,7 +40,7 @@ export const mockWorkspaceKind2: WorkspaceKind = buildMockWorkspaceKind({ }, }); -export const mockWorkspaceKind3: WorkspaceKind = buildMockWorkspaceKind({ +export const mockWorkspaceKind3: WorkspacekindsWorkspaceKind = buildMockWorkspaceKind({ name: 'jupyterlab3', displayName: 'JupyterLab Notebook 3', clusterMetrics: { @@ -50,25 +50,25 @@ export const mockWorkspaceKind3: WorkspaceKind = buildMockWorkspaceKind({ export const mockWorkspaceKinds = [mockWorkspaceKind1, mockWorkspaceKind2, mockWorkspaceKind3]; -export const mockWorkspaceKindInfo1: WorkspaceKindInfo = buildMockWorkspaceKindInfo({ +export const mockWorkspaceKindInfo1: WorkspacesWorkspaceKindInfo = buildMockWorkspaceKindInfo({ name: mockWorkspaceKind1.name, }); -export const mockWorkspaceKindInfo2: WorkspaceKindInfo = buildMockWorkspaceKindInfo({ +export const mockWorkspaceKindInfo2: WorkspacesWorkspaceKindInfo = buildMockWorkspaceKindInfo({ name: mockWorkspaceKind2.name, }); // Workspace -export const mockWorkspace1: Workspace = buildMockWorkspace({ +export const mockWorkspace1: WorkspacesWorkspace = buildMockWorkspace({ workspaceKind: mockWorkspaceKindInfo1, namespace: mockNamespace1.name, }); -export const mockWorkspace2: Workspace = buildMockWorkspace({ +export const mockWorkspace2: WorkspacesWorkspace = buildMockWorkspace({ name: 'My Second Jupyter Notebook', workspaceKind: mockWorkspaceKindInfo1, namespace: mockNamespace2.name, - state: WorkspaceState.WorkspaceStatePaused, + state: WorkspacesWorkspaceState.WorkspaceStatePaused, paused: false, deferUpdates: false, activity: { @@ -133,11 +133,11 @@ export const mockWorkspace2: Workspace = buildMockWorkspace({ }, }); -export const mockWorkspace3: Workspace = buildMockWorkspace({ +export const mockWorkspace3: WorkspacesWorkspace = buildMockWorkspace({ name: 'My Third Jupyter Notebook', namespace: mockNamespace1.name, workspaceKind: mockWorkspaceKindInfo1, - state: WorkspaceState.WorkspaceStateRunning, + state: WorkspacesWorkspaceState.WorkspaceStateRunning, pendingRestart: true, activity: { lastActivity: new Date(2025, 2, 15).getTime(), @@ -145,17 +145,17 @@ export const mockWorkspace3: Workspace = buildMockWorkspace({ }, }); -export const mockWorkspace4 = buildMockWorkspace({ +export const mockWorkspace4: WorkspacesWorkspace = buildMockWorkspace({ name: 'My Fourth Jupyter Notebook', namespace: mockNamespace2.name, - state: WorkspaceState.WorkspaceStateError, + state: WorkspacesWorkspaceState.WorkspaceStateError, workspaceKind: mockWorkspaceKindInfo2, }); -export const mockWorkspace5 = buildMockWorkspace({ +export const mockWorkspace5: WorkspacesWorkspace = buildMockWorkspace({ name: 'My Fifth Jupyter Notebook', namespace: mockNamespace2.name, - state: WorkspaceState.WorkspaceStateTerminating, + state: WorkspacesWorkspaceState.WorkspaceStateTerminating, workspaceKind: mockWorkspaceKindInfo2, }); diff --git a/workspaces/frontend/src/shared/mock/mockUtils.ts b/workspaces/frontend/src/shared/mock/mockUtils.ts index c4af8e1a..789ab2f6 100644 --- a/workspaces/frontend/src/shared/mock/mockUtils.ts +++ b/workspaces/frontend/src/shared/mock/mockUtils.ts @@ -1,7 +1,38 @@ +import { AxiosError, AxiosHeaders, AxiosResponse } from 'axios'; import yaml from 'js-yaml'; +import { ApiErrorEnvelope } from '~/generated/data-contracts'; // For testing purposes, a YAML string is considered invalid if it contains a specific pattern in the metadata name. export function isInvalidYaml(yamlString: string): boolean { const parsed = yaml.load(yamlString) as { metadata?: { name?: string } }; return parsed.metadata?.name?.includes('-invalid') ?? false; } + +export function buildAxiosError( + envelope: ApiErrorEnvelope, + status = 400, + configOverrides: Partial = {}, +): AxiosError { + const config = { + url: '', + method: 'GET', + headers: new AxiosHeaders(), + ...configOverrides, + }; + + const response: AxiosResponse = { + data: envelope, + status, + statusText: 'Bad Request', + headers: {}, + config, + }; + + return new AxiosError( + envelope.error.message, + envelope.error.code, + config, + undefined, + response, + ); +} diff --git a/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts b/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts index f1ee0bc9..8da8f10a 100644 --- a/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts +++ b/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts @@ -1,4 +1,8 @@ -import { Workspace, WorkspaceState, WorkspaceOptionLabel } from '~/shared/api/backendApiTypes'; +import { + WorkspacesOptionLabel, + WorkspacesWorkspace, + WorkspacesWorkspaceState, +} from '~/generated/data-contracts'; import { CPU_UNITS, MEMORY_UNITS_FOR_PARSING, @@ -28,7 +32,7 @@ export const parseResourceValue = ( }; export const extractResourceValue = ( - workspace: Workspace, + workspace: WorkspacesWorkspace, resourceType: ResourceType, ): string | undefined => workspace.podTemplate.options.podConfig.current.labels.find((label) => label.key === resourceType) @@ -48,35 +52,40 @@ export const formatResourceValue = (v: string | undefined, resourceType?: Resour }; export const formatResourceFromWorkspace = ( - workspace: Workspace, + workspace: WorkspacesWorkspace, resourceType: ResourceType, ): string => formatResourceValue(extractResourceValue(workspace, resourceType), resourceType); -export const formatWorkspaceIdleState = (workspace: Workspace): string => - workspace.state !== WorkspaceState.WorkspaceStateRunning ? YesNoValue.Yes : YesNoValue.No; +export const formatWorkspaceIdleState = (workspace: WorkspacesWorkspace): string => + workspace.state !== WorkspacesWorkspaceState.WorkspaceStateRunning + ? YesNoValue.Yes + : YesNoValue.No; -export const isWorkspaceWithGpu = (workspace: Workspace): boolean => +export const isWorkspaceWithGpu = (workspace: WorkspacesWorkspace): boolean => workspace.podTemplate.options.podConfig.current.labels.some((label) => label.key === 'gpu'); -export const isWorkspaceIdle = (workspace: Workspace): boolean => - workspace.state !== WorkspaceState.WorkspaceStateRunning; +export const isWorkspaceIdle = (workspace: WorkspacesWorkspace): boolean => + workspace.state !== WorkspacesWorkspaceState.WorkspaceStateRunning; -export const filterWorkspacesWithGpu = (workspaces: Workspace[]): Workspace[] => +export const filterWorkspacesWithGpu = (workspaces: WorkspacesWorkspace[]): WorkspacesWorkspace[] => workspaces.filter(isWorkspaceWithGpu); -export const filterIdleWorkspaces = (workspaces: Workspace[]): Workspace[] => +export const filterIdleWorkspaces = (workspaces: WorkspacesWorkspace[]): WorkspacesWorkspace[] => workspaces.filter(isWorkspaceIdle); -export const filterRunningWorkspaces = (workspaces: Workspace[]): Workspace[] => - workspaces.filter((workspace) => workspace.state === WorkspaceState.WorkspaceStateRunning); +export const filterRunningWorkspaces = (workspaces: WorkspacesWorkspace[]): WorkspacesWorkspace[] => + workspaces.filter( + (workspace) => workspace.state === WorkspacesWorkspaceState.WorkspaceStateRunning, + ); -export const filterIdleWorkspacesWithGpu = (workspaces: Workspace[]): Workspace[] => - filterIdleWorkspaces(filterWorkspacesWithGpu(workspaces)); +export const filterIdleWorkspacesWithGpu = ( + workspaces: WorkspacesWorkspace[], +): WorkspacesWorkspace[] => filterIdleWorkspaces(filterWorkspacesWithGpu(workspaces)); -export type WorkspaceGpuCountRecord = { workspaces: Workspace[]; gpuCount: number }; +export type WorkspaceGpuCountRecord = { workspaces: WorkspacesWorkspace[]; gpuCount: number }; export const groupWorkspacesByNamespaceAndGpu = ( - workspaces: Workspace[], + workspaces: WorkspacesWorkspace[], order: 'ASC' | 'DESC' = 'DESC', ): Record => { const grouped: Record = {}; @@ -97,7 +106,7 @@ export const groupWorkspacesByNamespaceAndGpu = ( ); }; -export const countGpusFromWorkspaces = (workspaces: Workspace[]): number => +export const countGpusFromWorkspaces = (workspaces: WorkspacesWorkspace[]): number => workspaces.reduce((total, workspace) => { const [gpuValue] = splitValueUnit(extractResourceValue(workspace, 'gpu') || '0', OTHER); return total + (gpuValue ?? 0); @@ -124,7 +133,7 @@ export const formatLabelKey = (key: string): string => { export const isPackageLabel = (key: string): boolean => key.endsWith('Version'); // Extract package labels from workspace image config -export const extractPackageLabels = (workspace: Workspace): WorkspaceOptionLabel[] => +export const extractPackageLabels = (workspace: WorkspacesWorkspace): WorkspacesOptionLabel[] => workspace.podTemplate.options.imageConfig.current.labels.filter((label) => isPackageLabel(label.key), ); diff --git a/workspaces/frontend/src/shared/utilities/const.ts b/workspaces/frontend/src/shared/utilities/const.ts index 6b3c5d9b..1ddc2224 100644 --- a/workspaces/frontend/src/shared/utilities/const.ts +++ b/workspaces/frontend/src/shared/utilities/const.ts @@ -1,4 +1,8 @@ -const DEV_MODE = process.env.APP_ENV === 'development'; -const AUTH_HEADER = process.env.AUTH_HEADER || 'kubeflow-userid'; +export const DEV_MODE = process.env.APP_ENV === 'development'; +export const AUTH_HEADER = process.env.AUTH_HEADER || 'kubeflow-userid'; -export { DEV_MODE, AUTH_HEADER }; +export const CONTENT_TYPE_KEY = 'Content-Type'; + +export enum ContentType { + YAML = 'application/yaml', +}