feat(ws): automate generation of types and HTTP client layer from Swagger definitions (#496)

Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com>
This commit is contained in:
Guilherme Caponetto 2025-08-05 09:28:53 -03:00 committed by Bhakti Narvekar
parent 2d5b8304d7
commit dcf6b93a46
89 changed files with 2277 additions and 1572 deletions

View File

@ -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.

View File

@ -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"
}

View File

@ -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"
}
}

View File

@ -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"

View File

@ -0,0 +1 @@
4f0a29dec0d3c9f0d0f02caab4dc84101bfef8b0

View File

@ -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' }),

View File

@ -1,5 +1,7 @@
import { ResponseBody } from '~/shared/api/types';
interface Envelope<T> {
data: T;
}
export const mockBFFResponse = <T>(data: T): ResponseBody<T> => ({
export const mockBFFResponse = <T>(data: T): Envelope<T> => ({
data,
});

View File

@ -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}`;

View File

@ -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> = {}): WorkspaceKind {
function createMockWorkspaceKind(
overrides: Partial<WorkspacekindsWorkspaceKind> = {},
): WorkspacekindsWorkspaceKind {
return {
name: 'jupyter-lab',
displayName: 'JupyterLab Notebook',
@ -27,14 +32,15 @@ function createMockWorkspaceKind(overrides: Partial<WorkspaceKind> = {}): 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<WorkspaceKind> = {}): 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' },
],
},
],
},

View File

@ -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"]')

View File

@ -1,4 +1,7 @@
import { WorkspaceKind, WorkspaceOptionRedirect } from '~/shared/api/backendApiTypes';
import {
WorkspacekindsOptionRedirect,
WorkspacekindsWorkspaceKind,
} from '~/generated/data-contracts';
type KindLogoDict = Record<string, string>;
@ -7,7 +10,9 @@ type KindLogoDict = Record<string, string>;
* @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<string, WorkspaceOptionRedirect | undefined>;
type WorkspaceRedirectStatus = Record<string, WorkspacekindsOptionRedirect | undefined>;
/**
* Builds a dictionary of workspace kinds to redirect statuses.
@ -28,7 +33,7 @@ type WorkspaceRedirectStatus = Record<string, WorkspaceOptionRedirect | undefine
* @returns {WorkspaceRedirectStatus} A dictionary with kind names as keys and redirect status objects as values.
*/
export function buildWorkspaceRedirectStatus(
workspaceKinds: WorkspaceKind[] | [],
workspaceKinds: WorkspacekindsWorkspaceKind[] | [],
): WorkspaceRedirectStatus {
const workspaceRedirectStatus: WorkspaceRedirectStatus = {};
for (const workspaceKind of workspaceKinds) {

View File

@ -1,12 +1,11 @@
import React from 'react';
import { Alert } from '@patternfly/react-core/dist/esm/components/Alert';
import { List, ListItem } from '@patternfly/react-core/dist/esm/components/List';
import { ValidationError } from '~/shared/api/backendApiTypes';
import { ErrorEnvelopeException } from '~/shared/api/apiUtils';
import { ApiValidationError } from '~/generated/data-contracts';
interface ValidationErrorAlertProps {
title: string;
errors: (ValidationError | ErrorEnvelopeException)[];
errors: ApiValidationError[];
}
export const ValidationErrorAlert: React.FC<ValidationErrorAlertProps> = ({ title, errors }) => {
@ -18,7 +17,9 @@ export const ValidationErrorAlert: React.FC<ValidationErrorAlertProps> = ({ titl
<Alert variant="danger" title={title} isInline>
<List>
{errors.map((error, index) => (
<ListItem key={index}>{error.message}</ListItem>
<ListItem key={index}>
{error.message}: &apos;{error.field}&apos;
</ListItem>
))}
</List>
</Alert>

View File

@ -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<typeof wsTableColumns>;
type WorkspaceTableSortableColumnKeys = SortableDataFieldKey<typeof wsTableColumns>;
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<WorkspaceTableRef, WorkspaceTableProps>(
[clearAllFilters],
);
const filterableProperties: Record<FilterKey, (ws: Workspace) => string> = useMemo(
const filterableProperties: Record<FilterKey, (ws: WorkspacesWorkspace) => string> = useMemo(
() => ({
name: (ws) => ws.name,
kind: (ws) => ws.workspaceKind.name,
@ -245,7 +245,7 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
[],
);
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<WorkspaceTableRef, WorkspaceTableProps>(
: newExpandedWorkspacesNames;
});
const isWorkspaceExpanded = (workspace: Workspace) =>
const isWorkspaceExpanded = (workspace: WorkspacesWorkspace) =>
expandedWorkspacesNames.includes(workspace.name);
const filteredWorkspaces = useMemo(() => {
@ -289,7 +289,7 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
// Column sorting
const getSortableRowValues = (
workspace: Workspace,
workspace: WorkspacesWorkspace,
): Record<WorkspaceTableSortableColumnKeys, string | number> => ({
name: workspace.name,
kind: workspace.workspaceKind.name,
@ -374,19 +374,19 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
}
};
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';
}

View File

@ -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<WorkspaceActionsContextPr
}, []);
const createActionRequester =
(actionType: ActionType) => (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<WorkspaceActionsContextPr
}
try {
await api.deleteWorkspace({}, selectedNamespace, activeWsAction.workspace.name);
await api.workspaces.deleteWorkspace(selectedNamespace, activeWsAction.workspace.name);
// TODO: alert user about success
console.info(`Workspace '${activeWsAction.workspace.name}' deleted successfully`);
activeWsAction.onActionDone?.();
@ -179,9 +180,13 @@ export const WorkspaceActionsContextProvider: React.FC<WorkspaceActionsContextPr
onClose={onCloseActionAlertDialog}
workspace={activeWsAction.workspace}
onStart={async () =>
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<WorkspaceActionsContextPr
onClose={onCloseActionAlertDialog}
workspace={activeWsAction.workspace}
onStop={async () =>
api.pauseWorkspace({}, selectedNamespace, activeWsAction.workspace.name, {
data: { paused: true },
})
api.workspaces.updateWorkspacePauseState(
selectedNamespace,
activeWsAction.workspace.name,
{
data: { paused: true },
},
)
}
onActionDone={activeWsAction.onActionDone}
onUpdateAndStop={async () => {

View File

@ -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<NotebookAPIs>;
export type NotebookAPIState = APIState<NotebookApis>;
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);
};

View File

@ -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<typeof useNotebookAPI>;
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<NotebookAPIs> = {
listAllWorkspaces: mockListAllWorkspaces,
listWorkspaceKinds: mockListWorkspaceKinds,
const mockApi: Partial<NotebookApis> = {
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());

View File

@ -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<Namespace[] | null> => {
const useNamespaces = (): FetchState<ApiNamespaceListEnvelope['data'] | null> => {
const { api, apiAvailable } = useNotebookAPI();
const call = useCallback<FetchStateCallbackPromise<Namespace[] | null>>(
(opts) => {
if (!apiAvailable) {
return Promise.reject(new Error('API not yet available'));
}
const call = useCallback<
FetchStateCallbackPromise<ApiNamespaceListEnvelope['data'] | null>
>(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);
};

View File

@ -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<WorkspaceKind['name'], WorkspaceCountPerOption>;
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<WorkspaceCountPerKind> {
async function loadWorkspaceCounts(api: NotebookApis): Promise<WorkspaceCountPerKind> {
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<WorkspaceCountPerOption['countByNamespace']>(
@ -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;

View File

@ -26,48 +26,49 @@ const useWorkspaceFormData = (args: {
const { namespace, workspaceName } = args;
const { api, apiAvailable } = useNotebookAPI();
const call = useCallback<FetchStateCallbackPromise<WorkspaceFormData>>(
async (opts) => {
if (!apiAvailable) {
throw new Error('API not yet available');
}
const call = useCallback<FetchStateCallbackPromise<WorkspaceFormData>>(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);
};

View File

@ -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<WorkspaceKind | null> => {
const useWorkspaceKindByName = (
kind: string | undefined,
): FetchState<ApiWorkspaceKindEnvelope['data'] | null> => {
const { api, apiAvailable } = useNotebookAPI();
const call = useCallback<FetchStateCallbackPromise<WorkspaceKind | null>>(
(opts) => {
if (!apiAvailable) {
return Promise.reject(new Error('API not yet available'));
}
const call = useCallback<
FetchStateCallbackPromise<ApiWorkspaceKindEnvelope['data'] | null>
>(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);
};

View File

@ -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<WorkspaceKind[]> => {
const useWorkspaceKinds = (): FetchState<WorkspacekindsWorkspaceKind[]> => {
const { api, apiAvailable } = useNotebookAPI();
const call = useCallback<FetchStateCallbackPromise<WorkspaceKind[]>>(
(opts) => {
if (!apiAvailable) {
return Promise.reject(new Error('API not yet available'));
}
return api.listWorkspaceKinds(opts);
},
[api, apiAvailable],
);
const call = useCallback<
FetchStateCallbackPromise<ApiWorkspaceKindListEnvelope['data']>
>(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, []);
};

View File

@ -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<typeof useWorkspaceActionsContext>,
): IActions[number] {
const map: Record<WorkspaceRowActionId, () => IActions[number]> = {

View File

@ -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<Workspace[]> => {
export const useWorkspacesByNamespace = (
namespace: string,
): FetchState<ApiWorkspaceListEnvelope['data']> => {
const { api, apiAvailable } = useNotebookAPI();
const call = useCallback<FetchStateCallbackPromise<Workspace[]>>(
(opts) => {
if (!apiAvailable) {
return Promise.reject(new Error('API not yet available'));
}
const call = useCallback<
FetchStateCallbackPromise<ApiWorkspaceListEnvelope['data']>
>(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<Workspace[]> => {
}): FetchState<ApiWorkspaceListEnvelope['data']> => {
const { kind, namespace, imageId, podConfigId } = args;
const { api, apiAvailable } = useNotebookAPI();
const call = useCallback<FetchStateCallbackPromise<Workspace[]>>(
async (opts) => {
if (!apiAvailable) {
throw new Error('API not yet available');
}
if (!kind) {
throw new Error('Workspace kind is required');
}
const call = useCallback<
FetchStateCallbackPromise<ApiWorkspaceListEnvelope['data']>
>(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, []);
};

View File

@ -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<WorkspaceOptionLabel>;
saveChanges: (editedData: WorkspaceOptionLabel) => void;
data: WorkspacekindsOptionLabel;
columnNames: ColumnNames<WorkspacekindsOptionLabel>;
saveChanges: (editedData: WorkspacekindsOptionLabel) => void;
ariaLabel: string;
deleteRow: () => void;
}
@ -70,8 +70,8 @@ const EditableRow: React.FC<EditableRowInterface> = ({
type ColumnNames<T> = { [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<EditableLabelsProps> = ({
description,
buttonLabel = 'Label',
}) => {
const columnNames: ColumnNames<WorkspaceOptionLabel> = {
const columnNames: ColumnNames<WorkspacekindsOptionLabel> = {
key: 'Key',
value: 'Value',
};

View File

@ -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<ValidationStatus>('default');
const mode: FormMode = useCurrentRouteKey() === 'workspaceKindCreate' ? 'create' : 'edit';
const [specErrors, setSpecErrors] = useState<(ValidationError | ErrorEnvelopeException)[]>([]);
const [specErrors, setSpecErrors] = useState<ApiValidationError[]>([]);
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<WorkspaceKindFormData>(
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',

View File

@ -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;

View File

@ -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: '',

View File

@ -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<WorkspaceKindFormImageRedirectProps> = ({
@ -26,18 +26,18 @@ export const WorkspaceKindFormImageRedirect: React.FC<WorkspaceKindFormImageRedi
const redirectMsgOptions = [
{ value: 'please choose', label: 'Select one', disabled: true },
{
value: WorkspaceRedirectMessageLevel.RedirectMessageLevelInfo,
label: WorkspaceRedirectMessageLevel.RedirectMessageLevelInfo,
value: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelInfo,
label: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelInfo,
disabled: false,
},
{
value: WorkspaceRedirectMessageLevel.RedirectMessageLevelWarning,
label: WorkspaceRedirectMessageLevel.RedirectMessageLevelWarning,
value: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelWarning,
label: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelWarning,
disabled: false,
},
{
value: WorkspaceRedirectMessageLevel.RedirectMessageLevelDanger,
label: WorkspaceRedirectMessageLevel.RedirectMessageLevelDanger,
value: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelDanger,
label: WorkspacekindsRedirectMessageLevel.RedirectMessageLevelDanger,
disabled: false,
},
];
@ -71,7 +71,7 @@ export const WorkspaceKindFormImageRedirect: React.FC<WorkspaceKindFormImageRedi
...redirect,
message: {
text: redirect.message?.level || '',
level: val as WorkspaceRedirectMessageLevel,
level: val as WorkspacekindsRedirectMessageLevel,
},
})
}
@ -100,7 +100,8 @@ export const WorkspaceKindFormImageRedirect: React.FC<WorkspaceKindFormImageRedi
...redirect,
message: {
level:
redirect.message?.level || WorkspaceRedirectMessageLevel.RedirectMessageLevelInfo,
redirect.message?.level ||
WorkspacekindsRedirectMessageLevel.RedirectMessageLevelInfo,
text: val,
},
})

View File

@ -11,9 +11,9 @@ import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput'
import { Switch } from '@patternfly/react-core/dist/esm/components/Switch';
import { HelperText } from '@patternfly/react-core/dist/esm/components/HelperText';
import { WorkspaceKindPodConfigValue } from '~/app/types';
import { WorkspaceOptionLabel } from '~/shared/api/backendApiTypes';
import { EditableLabels } from '~/app/pages/WorkspaceKinds/Form/EditableLabels';
import { getResources } from '~/app/pages/WorkspaceKinds/Form/helpers';
import { WorkspacekindsOptionLabel } from '~/generated/data-contracts';
import { WorkspaceKindFormResource, PodResourceEntry } from './WorkspaceKindFormResource';
interface WorkspaceKindFormPodConfigModalProps {
@ -36,7 +36,7 @@ export const WorkspaceKindFormPodConfigModal: React.FC<WorkspaceKindFormPodConfi
const initialResources = useMemo(() => getResources(currConfig), [currConfig]);
const [resources, setResources] = useState<PodResourceEntry[]>(initialResources);
const [labels, setLabels] = useState<WorkspaceOptionLabel[]>(currConfig.labels);
const [labels, setLabels] = useState<WorkspacekindsOptionLabel[]>(currConfig.labels);
const [id, setId] = useState(currConfig.id);
const [displayName, setDisplayName] = useState(currConfig.displayName);
const [description, setDescription] = useState(currConfig.description);

View File

@ -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<WorkspaceKindFormPodTemplate
updatePodTemplate,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [volumes, setVolumes] = useState<WorkspacePodVolumeMount[]>([]);
const [volumes, setVolumes] = useState<WorkspacesPodVolumeMount[]>([]);
const toggleCullingEnabled = useCallback(
(checked: boolean) => {
@ -42,7 +42,7 @@ export const WorkspaceKindFormPodTemplate: React.FC<WorkspaceKindFormPodTemplate
);
const handleVolumes = useCallback(
(newVolumes: WorkspacePodVolumeMount[]) => {
(newVolumes: WorkspacesPodVolumeMount[]) => {
setVolumes(newVolumes);
updatePodTemplate({
...podTemplate,

View File

@ -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<WorkspaceKind | null>(null);
const [selectedWorkspaceKind, setSelectedWorkspaceKind] =
useState<WorkspacekindsWorkspaceKind | null>(null);
const [activeActionType, setActiveActionType] = useState<ActionType | null>(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',

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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<

View File

@ -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;
};

View File

@ -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;

View File

@ -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<DataVolumesListProps> = ({ workspace }) => {

View File

@ -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;

View File

@ -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<WorkspaceDetailsActivityProps> = ({

View File

@ -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<WorkspaceDetailsOverviewProps> = ({

View File

@ -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;
}

View File

@ -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);

View File

@ -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<WorkspaceFormImageDetailsProps> = ({

View File

@ -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<typeof fields>;
type WorkspaceFormImageListProps = {
images: WorkspaceImageConfigValue[];
images: WorkspacekindsImageConfigValue[];
selectedLabels: Map<string, Set<string>>;
selectedImage: WorkspaceImageConfigValue | undefined;
onSelect: (workspaceImage: WorkspaceImageConfigValue | undefined) => void;
selectedImage: WorkspacekindsImageConfigValue | undefined;
onSelect: (workspaceImage: WorkspacekindsImageConfigValue | undefined) => void;
};
export const WorkspaceFormImageList: React.FunctionComponent<WorkspaceFormImageListProps> = ({
@ -37,7 +37,7 @@ export const WorkspaceFormImageList: React.FunctionComponent<WorkspaceFormImageL
const filterRef = useRef<FilterRef>(null);
const getFilteredWorkspaceImagesByLabels = useCallback(
(unfilteredImages: WorkspaceImageConfigValue[]) =>
(unfilteredImages: WorkspacekindsImageConfigValue[]) =>
unfilteredImages.filter((image) =>
image.labels.reduce((accumulator, label) => {
if (selectedLabels.has(label.key)) {

View File

@ -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<WorkspaceFormImageSelectionProps> = ({

View File

@ -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<WorkspaceFormKindDetailsProps> = ({

View File

@ -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<typeof fields>;
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<WorkspaceFormKindListProps> = ({

View File

@ -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<WorkspaceFormKindSelectionProps> = ({

View File

@ -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<string, Set<string>>;
onSelect: (labels: Map<string, Set<string>>) => void;
};

View File

@ -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<

View File

@ -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<typeof fields>;
type WorkspaceFormPodConfigListProps = {
podConfigs: WorkspacePodConfigValue[];
podConfigs: WorkspacekindsPodConfigValue[];
selectedLabels: Map<string, Set<string>>;
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<FilterRef>(null);
const getFilteredWorkspacePodConfigsByLabels = useCallback(
(unfilteredPodConfigs: WorkspacePodConfigValue[]) =>
(unfilteredPodConfigs: WorkspacekindsPodConfigValue[]) =>
unfilteredPodConfigs.filter((podConfig) =>
podConfig.labels.reduce((accumulator, label) => {
if (selectedLabels.has(label.key)) {

View File

@ -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<

View File

@ -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<WorkspaceFormPropertiesSec
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [formData, setFormData] = useState<WorkspacePodSecretMount>({
const [formData, setFormData] = useState<WorkspacesPodSecretMount>({
secretName: '',
mountPath: '',
defaultMode: parseInt(DEFAULT_MODE_OCTAL, 8),

View File

@ -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;
}

View File

@ -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<WorkspaceFormPropertiesVolumesProps> = ({
@ -36,7 +36,7 @@ export const WorkspaceFormPropertiesVolumes: React.FC<WorkspaceFormPropertiesVol
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [formData, setFormData] = useState<WorkspacePodVolumeMount>({
const [formData, setFormData] = useState<WorkspacesPodVolumeMount>({
pvcName: '',
mountPath: '',
readOnly: false,

View File

@ -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<WorkspaceConfigDetailsProps> = ({ workspace }) => (

View File

@ -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<WorkspaceConnectActionProps> = ({
@ -57,7 +57,7 @@ export const WorkspaceConnectAction: React.FunctionComponent<WorkspaceConnectAct
variant="secondary"
onClick={onToggleClick}
isExpanded={open}
isDisabled={workspace.state !== WorkspaceState.WorkspaceStateRunning}
isDisabled={workspace.state !== WorkspacesWorkspaceState.WorkspaceStateRunning}
splitButtonItems={[
<MenuToggleAction
id="connect-endpoint-button"

View File

@ -6,11 +6,11 @@ import {
DescriptionListGroup,
} from '@patternfly/react-core/dist/esm/components/DescriptionList';
import { ListItem, List } from '@patternfly/react-core/dist/esm/components/List';
import { Workspace } from '~/shared/api/backendApiTypes';
import { extractPackageLabels, formatLabelKey } from '~/shared/utilities/WorkspaceUtils';
import { WorkspacesWorkspace } from '~/generated/data-contracts';
interface WorkspacePackageDetailsProps {
workspace: Workspace;
workspace: WorkspacesWorkspace;
}
export const WorkspacePackageDetails: React.FC<WorkspacePackageDetailsProps> = ({ workspace }) => {

View File

@ -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<WorkspaceStorageProps> = ({ workspace }) => (

View File

@ -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,
},
]);

View File

@ -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<WorkspaceRedirectInforma
const [activeKey, setActiveKey] = useState<string | number>(0);
const [workspaceKind, workspaceKindLoaded] = useWorkspaceKindByName(kind);
const [imageConfig, setImageConfig] =
useState<WorkspaceKind['podTemplate']['options']['imageConfig']>();
useState<WorkspacekindsWorkspaceKind['podTemplate']['options']['imageConfig']>();
const [podConfig, setPodConfig] =
useState<WorkspaceKind['podTemplate']['options']['podConfig']>();
useState<WorkspacekindsWorkspaceKind['podTemplate']['options']['podConfig']>();
useEffect(() => {
if (!workspaceKindLoaded) {

View File

@ -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<RestartActionAlertProps> = ({

View File

@ -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<WorkspacePauseState | void>;
workspace: WorkspacesWorkspace | null;
onStart: () => Promise<ApiWorkspaceActionPauseEnvelope>;
onUpdateAndStart: () => Promise<void>;
onActionDone?: () => void;
}
@ -33,10 +33,16 @@ export const WorkspaceStartActionModal: React.FC<StartActionAlertProps> = ({
const [actionOnGoing, setActionOnGoing] = useState<StartAction | null>(null);
const executeAction = useCallback(
(args: { action: StartAction; callback: () => ReturnType<typeof onStart> }) => {
setActionOnGoing(args.action);
async <T,>({
action,
callback,
}: {
action: StartAction;
callback: () => Promise<T>;
}): Promise<T> => {
setActionOnGoing(action);
try {
return args.callback();
return await callback();
} finally {
setActionOnGoing(null);
}
@ -48,7 +54,7 @@ export const WorkspaceStartActionModal: React.FC<StartActionAlertProps> = ({
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) {

View File

@ -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<WorkspacePauseState | void>;
workspace: WorkspacesWorkspace | null;
onStop: () => Promise<ApiWorkspaceActionPauseEnvelope>;
onUpdateAndStop: () => Promise<void>;
onActionDone?: () => void;
}
@ -35,10 +35,16 @@ export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
const [actionOnGoing, setActionOnGoing] = useState<StopAction | null>(null);
const executeAction = useCallback(
(args: { action: StopAction; callback: () => ReturnType<typeof onStop> }) => {
setActionOnGoing(args.action);
async <T,>({
action,
callback,
}: {
action: StopAction;
callback: () => Promise<T>;
}): Promise<T> => {
setActionOnGoing(action);
try {
return args.callback();
return await callback();
} finally {
setActionOnGoing(null);
}
@ -50,7 +56,7 @@ export const WorkspaceStopActionModal: React.FC<StopActionAlertProps> = ({
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) {

View File

@ -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<WorkspaceImageConfigValue['id'], number>;
countByPodConfig: Record<WorkspacePodConfigValue['id'], number>;
countByNamespace: Record<Workspace['namespace'], number>;
countByImage: Record<WorkspacekindsImageConfigValue['id'], number>;
countByPodConfig: Record<WorkspacekindsPodConfigValue['id'], number>;
countByNamespace: Record<WorkspacesWorkspace['namespace'], number>;
}
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 {

View File

@ -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<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
/**
* @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<HealthCheckHealthCheck, ApiErrorEnvelope>({
path: `/healthcheck`,
method: 'GET',
format: 'json',
...params,
});
}

View File

@ -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<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
/**
* @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<ApiNamespaceListEnvelope, ApiErrorEnvelope>({
path: `/namespaces`,
method: 'GET',
format: 'json',
...params,
});
}

View File

@ -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<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
/**
* @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<ApiWorkspaceKindListEnvelope, ApiErrorEnvelope>({
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<ApiWorkspaceKindEnvelope, ApiErrorEnvelope>({
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<ApiWorkspaceKindEnvelope, ApiErrorEnvelope>({
path: `/workspacekinds/${name}`,
method: 'GET',
type: ContentType.Json,
format: 'json',
...params,
});
}

View File

@ -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<SecurityDataType = unknown> extends HttpClient<SecurityDataType> {
/**
* @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<ApiWorkspaceListEnvelope, ApiErrorEnvelope>({
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<ApiWorkspaceListEnvelope, ApiErrorEnvelope>({
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<ApiWorkspaceEnvelope, ApiErrorEnvelope>({
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<ApiWorkspaceActionPauseEnvelope, ApiErrorEnvelope>({
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<ApiWorkspaceEnvelope, ApiErrorEnvelope>({
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<void, ApiErrorEnvelope>({
path: `/workspaces/${namespace}/${workspaceName}`,
method: 'DELETE',
type: ContentType.Json,
...params,
});
}

View File

@ -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<string, string>;
labels: Record<string, string>;
}
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<string, string>;
labels: Record<string, string>;
}
export interface WorkspacesPodMetadataMutate {
annotations: Record<string, string>;
labels: Record<string, string>;
}
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;

View File

@ -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<string | number, any>;
export interface FullRequestParams
extends Omit<AxiosRequestConfig, 'data' | 'params' | 'url' | 'responseType'> {
/** 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<FullRequestParams, 'body' | 'method' | 'query' | 'path'>;
export interface ApiConfig<SecurityDataType = unknown>
extends Omit<AxiosRequestConfig, 'data' | 'cancelToken'> {
securityWorker?: (
securityData: SecurityDataType | null,
) => Promise<AxiosRequestConfig | void> | 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<SecurityDataType = unknown> {
public instance: AxiosInstance;
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>['securityWorker'];
private secure?: boolean;
private format?: ResponseType;
constructor({
securityWorker,
secure,
format,
...axiosConfig
}: ApiConfig<SecurityDataType> = {}) {
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<string, unknown>): 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 <T = any, _E = any>({
secure,
path,
type,
query,
format,
body,
...params
}: FullRequestParams): Promise<T> => {
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<string, unknown>);
}
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);
};
}

View File

@ -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: '<error_code>',
message: '<error_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',
);
});
});

View File

@ -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);
});
});

View File

@ -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<string, unknown>;
parseJSON?: boolean;
directYAML?: boolean;
} & EitherOrNone<
{
fileContents: string;
},
{
data: Record<string, unknown>;
}
>;
const callRestJSON = <T>(
host: string,
path: string,
requestInit: RequestInit,
{ data, fileContents, queryParams, parseJSON = true, directYAML = false }: CallRestJSONOptions,
): Promise<T> => {
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 = <T>(
host: string,
path: string,
queryParams: Record<string, unknown> = {},
options?: APIOptions,
): Promise<T> =>
callRestJSON<T>(host, path, mergeRequestInit(options, { method: 'GET' }), {
queryParams,
parseJSON: options?.parseJSON,
});
/** Standard POST */
export const restCREATE = <T>(
host: string,
path: string,
data: Record<string, unknown>,
queryParams: Record<string, unknown> = {},
options?: APIOptions,
): Promise<T> =>
callRestJSON<T>(host, path, mergeRequestInit(options, { method: 'POST' }), {
data,
queryParams,
parseJSON: options?.parseJSON,
});
/** POST -- but with file content instead of body data */
export const restFILE = <T>(
host: string,
path: string,
fileContents: string,
queryParams: Record<string, unknown> = {},
options?: APIOptions,
): Promise<T> =>
callRestJSON<T>(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 = <T>(
host: string,
path: string,
queryParams: Record<string, unknown> = {},
options?: APIOptions,
): Promise<T> =>
callRestJSON<T>(host, path, mergeRequestInit(options, { method: 'POST' }), {
queryParams,
parseJSON: options?.parseJSON,
});
export const restUPDATE = <T>(
host: string,
path: string,
data: Record<string, unknown>,
queryParams: Record<string, unknown> = {},
options?: APIOptions,
): Promise<T> =>
callRestJSON<T>(host, path, mergeRequestInit(options, { method: 'PUT' }), {
data,
queryParams,
parseJSON: options?.parseJSON,
});
export const restPATCH = <T>(
host: string,
path: string,
data: Record<string, unknown>,
options?: APIOptions,
): Promise<T> =>
callRestJSON<T>(host, path, mergeRequestInit(options, { method: 'PATCH' }), {
data,
parseJSON: options?.parseJSON,
});
export const restDELETE = <T>(
host: string,
path: string,
data: Record<string, unknown>,
queryParams: Record<string, unknown> = {},
options?: APIOptions,
): Promise<T> =>
callRestJSON<T>(host, path, mergeRequestInit(options, { method: 'DELETE' }), {
data,
queryParams,
parseJSON: options?.parseJSON,
});
export const isNotebookResponse = <T>(response: unknown): response is ResponseBody<T> => {
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<string, unknown>).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<T>(response: unknown): T {
// Check if this is an error envelope first
if (isErrorEnvelope(response)) {
throw new ErrorEnvelopeException(response);
}
if (isNotebookResponse<T>(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<T>(promise: Promise<T>, extractData = true): Promise<T> {
export async function safeApiCall<T>(fn: () => Promise<T>): Promise<ApiCallResult<T>> {
try {
const res = await handleRestFailures<T>(promise);
return extractData ? extractNotebookResponse<T>(res) : res;
} catch (error) {
if (error instanceof ErrorEnvelopeException) {
throw error;
const data = await fn();
return { ok: true, data };
} catch (error: unknown) {
if (axios.isAxiosError<ApiErrorEnvelope>(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;
}
}

View File

@ -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<string, string>;
annotations: Record<string, string>;
}
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<string, string>;
annotations: Record<string, string>;
}
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<string, string>;
annotations: Record<string, string>;
}
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;
};

View File

@ -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<unknown>;
type KubeflowAPICall<ActualCall extends KubeflowSpecificAPICall> = (hostPath: string) => ActualCall;
// Health
export type GetHealthCheckAPI = KubeflowAPICall<GetHealthCheck>;
// Namespace
export type ListNamespacesAPI = KubeflowAPICall<ListNamespaces>;
// Workspace
export type ListAllWorkspacesAPI = KubeflowAPICall<ListAllWorkspaces>;
export type ListWorkspacesAPI = KubeflowAPICall<ListWorkspaces>;
export type CreateWorkspaceAPI = KubeflowAPICall<CreateWorkspace>;
export type GetWorkspaceAPI = KubeflowAPICall<GetWorkspace>;
export type UpdateWorkspaceAPI = KubeflowAPICall<UpdateWorkspace>;
export type PatchWorkspaceAPI = KubeflowAPICall<PatchWorkspace>;
export type DeleteWorkspaceAPI = KubeflowAPICall<DeleteWorkspace>;
export type PauseWorkspaceAPI = KubeflowAPICall<PauseWorkspace>;
// WorkspaceKind
export type ListWorkspaceKindsAPI = KubeflowAPICall<ListWorkspaceKinds>;
export type CreateWorkspaceKindAPI = KubeflowAPICall<CreateWorkspaceKind>;
export type GetWorkspaceKindAPI = KubeflowAPICall<GetWorkspaceKind>;
export type UpdateWorkspaceKindAPI = KubeflowAPICall<UpdateWorkspaceKind>;
export type PatchWorkspaceKindAPI = KubeflowAPICall<PatchWorkspaceKind>;
export type DeleteWorkspaceKindAPI = KubeflowAPICall<DeleteWorkspaceKind>;

View File

@ -1,16 +0,0 @@
import { ErrorEnvelopeException, isErrorEnvelope } from '~/shared/api//apiUtils';
import { isCommonStateError } from '~/shared/utilities/useFetchState';
export const handleRestFailures = <T>(promise: Promise<T>): Promise<T> =>
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');
});

View File

@ -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.

View File

@ -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<HealthCheckResponse>;
export interface NotebookApis {
healthCheck: ApiInstance<typeof Healthcheck>;
namespaces: ApiInstance<typeof Namespaces>;
workspaces: ApiInstance<typeof Workspaces>;
workspaceKinds: ApiInstance<typeof Workspacekinds>;
}
// Namespace
export type ListNamespaces = (opts: APIOptions) => Promise<Namespace[]>;
export const notebookApisImpl = (path: string): NotebookApis => {
const commonConfig = { baseURL: path };
// Workspace
export type ListAllWorkspaces = (opts: APIOptions) => Promise<Workspace[]>;
export type ListWorkspaces = (opts: APIOptions, namespace: string) => Promise<Workspace[]>;
export type GetWorkspace = (
opts: APIOptions,
namespace: string,
workspace: string,
) => Promise<Workspace>;
export type CreateWorkspace = (
opts: APIOptions,
namespace: string,
data: RequestData<WorkspaceCreate>,
) => Promise<Workspace>;
export type UpdateWorkspace = (
opts: APIOptions,
namespace: string,
workspace: string,
data: RequestData<WorkspaceUpdate>,
) => Promise<Workspace>;
export type PatchWorkspace = (
opts: APIOptions,
namespace: string,
workspace: string,
data: RequestData<WorkspacePatch>,
) => Promise<Workspace>;
export type DeleteWorkspace = (
opts: APIOptions,
namespace: string,
workspace: string,
) => Promise<void>;
export type PauseWorkspace = (
opts: APIOptions,
namespace: string,
workspace: string,
data: RequestData<WorkspacePauseState>,
) => Promise<WorkspacePauseState>;
// WorkspaceKind
export type ListWorkspaceKinds = (opts: APIOptions) => Promise<WorkspaceKind[]>;
export type GetWorkspaceKind = (opts: APIOptions, kind: string) => Promise<WorkspaceKind>;
export type CreateWorkspaceKind = (opts: APIOptions, data: string) => Promise<WorkspaceKind>;
export type UpdateWorkspaceKind = (
opts: APIOptions,
kind: string,
data: RequestData<WorkspaceKindUpdate>,
) => Promise<WorkspaceKind>;
export type PatchWorkspaceKind = (
opts: APIOptions,
kind: string,
data: RequestData<WorkspaceKindPatch>,
) => Promise<WorkspaceKind>;
export type DeleteWorkspaceKind = (opts: APIOptions, kind: string) => Promise<void>;
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),
};
};

View File

@ -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);

View File

@ -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<string, string>;
directYAML?: boolean;
};
export type APIState<T> = {
@ -13,11 +15,14 @@ export type APIState<T> = {
api: T;
};
export type ResponseBody<T> = {
data: T;
metadata?: Record<string, unknown>;
export type RemoveHttpClient<T> = Omit<T, keyof HttpClient<unknown>>;
export type WithExperimental<TBase, TExperimental> = TBase & {
experimental: TExperimental;
};
export type RequestData<T> = {
data: T;
};
export type ApiClass = abstract new (config?: ApiConfig) => object;
export type ApiInstance<T extends ApiClass> = RemoveHttpClient<InstanceType<T>>;
export type ApiCallResult<T> =
| { ok: true; data: T }
| { ok: false; errorEnvelope: ApiErrorEnvelope };

View File

@ -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>,
): HealthCheckResponse => ({
status: WorkspaceServiceStatus.ServiceStatusHealthy,
healthCheckResponse?: Partial<HealthCheckHealthCheck>,
): HealthCheckHealthCheck => ({
status: HealthCheckServiceStatus.ServiceStatusHealthy,
systemInfo: { version: '1.0.0' },
...healthCheckResponse,
});
export const buildMockNamespace = (namespace?: Partial<Namespace>): Namespace => ({
export const buildMockNamespace = (
namespace?: Partial<NamespacesNamespace>,
): NamespacesNamespace => ({
name: 'default',
...namespace,
});
export const buildMockWorkspaceKindInfo = (
workspaceKindInfo?: Partial<WorkspaceKindInfo>,
): WorkspaceKindInfo => ({
workspaceKindInfo?: Partial<WorkspacesWorkspaceKindInfo>,
): WorkspacesWorkspaceKindInfo => ({
name: 'jupyterlab',
missing: false,
icon: {
@ -37,14 +39,16 @@ export const buildMockWorkspaceKindInfo = (
...workspaceKindInfo,
});
export const buildMockWorkspace = (workspace?: Partial<Workspace>): Workspace => ({
export const buildMockWorkspace = (
workspace?: Partial<WorkspacesWorkspace>,
): 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 =>
...workspace,
});
export const buildMockWorkspaceKind = (workspaceKind?: Partial<WorkspaceKind>): WorkspaceKind => ({
export const buildMockWorkspaceKind = (
workspaceKind?: Partial<WorkspacekindsWorkspaceKind>,
): WorkspacekindsWorkspaceKind => ({
name: 'jupyterlab',
displayName: 'JupyterLab Notebook',
description: 'A Workspace which runs JupyterLab in a Pod',
@ -182,7 +188,7 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial<WorkspaceKind>):
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<WorkspaceKind>):
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<WorkspaceKind>):
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<WorkspaceKind>):
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<WorkspaceKind>):
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<WorkspaceKind>):
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>):
...workspaceKind,
});
export const buildMockPauseStateResponse = (
pauseState?: Partial<WorkspacePauseState>,
): WorkspacePauseState => ({
export const buildMockActionsWorkspaceActionPause = (
pauseState?: Partial<ActionsWorkspaceActionPause>,
): 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 },

View File

@ -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 };
},
},
});

View File

@ -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);
};

View File

@ -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,
});

View File

@ -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<AxiosResponse['config']> = {},
): AxiosError<ApiErrorEnvelope> {
const config = {
url: '',
method: 'GET',
headers: new AxiosHeaders(),
...configOverrides,
};
const response: AxiosResponse<ApiErrorEnvelope> = {
data: envelope,
status,
statusText: 'Bad Request',
headers: {},
config,
};
return new AxiosError<ApiErrorEnvelope>(
envelope.error.message,
envelope.error.code,
config,
undefined,
response,
);
}

View File

@ -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<string, WorkspaceGpuCountRecord> => {
const grouped: Record<string, WorkspaceGpuCountRecord> = {};
@ -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),
);

View File

@ -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',
}