From 78b3d5f4e7af3abd1b7e7b8190e0ce11ab096261 Mon Sep 17 00:00:00 2001 From: Chandrasekhar Ramakrishnan Date: Fri, 27 Oct 2023 19:56:25 +0200 Subject: [PATCH 01/15] feat: (WIP) flow for creating a renku-native project (#2870)(#2875) --- client/.eslintrc.json | 3 + client/package-lock.json | 811 ++++++++++++++++-- client/package.json | 3 + client/src/App.jsx | 29 + client/src/features/projectsV2/api/index.ts | 58 ++ .../projectsV2/api/projectV2-empty.api.ts | 26 + .../projectsV2/api/projectV2.api-config.ts | 32 + .../features/projectsV2/api/projectV2.api.ts | 190 ++++ .../projectsV2/api/projectV2.enhanced-api.ts | 94 ++ .../projectsV2/api/projectV2.openapi.json | 693 +++++++++++++++ .../edit/ProjectRepositoryFormField.tsx | 103 +++ .../projectsV2/edit/formField.types.ts | 10 + client/src/features/projectsV2/edit/index.tsx | 29 + .../projectsV2/edit/projectMemberFields.tsx | 283 ++++++ .../projectsV2/edit/simpleFormFields.tsx | 169 ++++ client/src/features/projectsV2/index.ts | 7 + .../projectsV2/list/ProjectV2List.tsx | 108 +++ .../projectsV2/list/projectV2List.module.scss | 3 + .../new/ProjectV2FormSubmitGroup.tsx | 66 ++ .../features/projectsV2/new/ProjectV2New.tsx | 205 +++++ .../projectsV2/new/ProjectV2NewForm.tsx | 230 +++++ .../projectsV2/new/projectV2New.slice.ts | 69 ++ .../features/projectsV2/projectV2.types.ts | 29 + .../projectsV2/show/ProjectV2EditForm.tsx | 398 +++++++++ .../projectsV2/show/ProjectV2Show.tsx | 210 +++++ .../projectsV2/show/projectV2Show.types.ts | 19 + .../dataServicesUser-empty.api.ts | 26 + .../dataServicesUser.api-config.ts | 32 + .../dataServicesUser.api.ts | 73 ++ .../dataServicesUser.openapi.json | 237 +++++ .../user/dataServicesUser.api/index.ts | 10 + client/src/utils/helpers/EnhancedState.ts | 16 +- client/src/utils/helpers/url/Url.js | 8 + tests/cypress/e2e/projectV2.spec.ts | 264 ++++++ .../fixtures/projectV2/create-projectV2.json | 9 + .../projectV2/list-projectV2-members.json | 14 + .../projectV2/list-projectV2-post-delete.json | 12 + .../fixtures/projectV2/list-projectV2.json | 25 + .../fixtures/projectV2/read-projectV2.json | 13 + .../projectV2/update-projectV2-metadata.json | 13 + .../update-projectV2-repositories.json | 14 + .../support/renkulab-fixtures/dataServices.ts | 25 +- .../support/renkulab-fixtures/index.ts | 11 +- .../support/renkulab-fixtures/projectV2.ts | 235 +++++ 44 files changed, 4834 insertions(+), 80 deletions(-) create mode 100644 client/src/features/projectsV2/api/index.ts create mode 100644 client/src/features/projectsV2/api/projectV2-empty.api.ts create mode 100644 client/src/features/projectsV2/api/projectV2.api-config.ts create mode 100644 client/src/features/projectsV2/api/projectV2.api.ts create mode 100644 client/src/features/projectsV2/api/projectV2.enhanced-api.ts create mode 100644 client/src/features/projectsV2/api/projectV2.openapi.json create mode 100644 client/src/features/projectsV2/edit/ProjectRepositoryFormField.tsx create mode 100644 client/src/features/projectsV2/edit/formField.types.ts create mode 100644 client/src/features/projectsV2/edit/index.tsx create mode 100644 client/src/features/projectsV2/edit/projectMemberFields.tsx create mode 100644 client/src/features/projectsV2/edit/simpleFormFields.tsx create mode 100644 client/src/features/projectsV2/index.ts create mode 100644 client/src/features/projectsV2/list/ProjectV2List.tsx create mode 100644 client/src/features/projectsV2/list/projectV2List.module.scss create mode 100644 client/src/features/projectsV2/new/ProjectV2FormSubmitGroup.tsx create mode 100644 client/src/features/projectsV2/new/ProjectV2New.tsx create mode 100644 client/src/features/projectsV2/new/ProjectV2NewForm.tsx create mode 100644 client/src/features/projectsV2/new/projectV2New.slice.ts create mode 100644 client/src/features/projectsV2/projectV2.types.ts create mode 100644 client/src/features/projectsV2/show/ProjectV2EditForm.tsx create mode 100644 client/src/features/projectsV2/show/ProjectV2Show.tsx create mode 100644 client/src/features/projectsV2/show/projectV2Show.types.ts create mode 100644 client/src/features/user/dataServicesUser.api/dataServicesUser-empty.api.ts create mode 100644 client/src/features/user/dataServicesUser.api/dataServicesUser.api-config.ts create mode 100644 client/src/features/user/dataServicesUser.api/dataServicesUser.api.ts create mode 100644 client/src/features/user/dataServicesUser.api/dataServicesUser.openapi.json create mode 100644 client/src/features/user/dataServicesUser.api/index.ts create mode 100644 tests/cypress/e2e/projectV2.spec.ts create mode 100644 tests/cypress/fixtures/projectV2/create-projectV2.json create mode 100644 tests/cypress/fixtures/projectV2/list-projectV2-members.json create mode 100644 tests/cypress/fixtures/projectV2/list-projectV2-post-delete.json create mode 100644 tests/cypress/fixtures/projectV2/list-projectV2.json create mode 100644 tests/cypress/fixtures/projectV2/read-projectV2.json create mode 100644 tests/cypress/fixtures/projectV2/update-projectV2-metadata.json create mode 100644 tests/cypress/fixtures/projectV2/update-projectV2-repositories.json create mode 100644 tests/cypress/support/renkulab-fixtures/projectV2.ts diff --git a/client/.eslintrc.json b/client/.eslintrc.json index f3b9f6014c..948dc5ca6b 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -115,6 +115,7 @@ "ckeditor", "cktextarea", "cloudstorage", + "codegen", "codemirror", "craco", "dagre", @@ -192,6 +193,7 @@ "objectstores", "onloadend", "onopen", + "openapi", "papermill", "pathname", "pdfjs", @@ -246,6 +248,7 @@ "tooltip", "tspan", "uiserver", + "ulid", "uncompress", "unicode", "unmount", diff --git a/client/package-lock.json b/client/package-lock.json index a388445e6b..0bad66d092 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -79,6 +79,7 @@ }, "devDependencies": { "@nabla/vite-plugin-eslint": "^2.0.2", + "@rtk-query/codegen-openapi": "^1.2.0", "@storybook/addon-actions": "^7.6.0", "@storybook/addon-essentials": "^7.6.0", "@storybook/addon-interactions": "^7.6.0", @@ -112,6 +113,7 @@ "@typescript-eslint/parser": "^6.13.2", "@vitejs/plugin-react": "^4.2.0", "concurrently": "^8.2.2", + "esbuild-runner": "^2.2.2", "eslint": "^8.54.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jest": "^27.6.0", @@ -126,6 +128,7 @@ "pretty-quick": "^3.1.3", "react-test-renderer": "^18.2.0", "storybook": "^7.6.0", + "ts-node": "^10.9.2", "typescript": "^5.3.2", "vite": "^5.0.12", "vite-bundle-visualizer": "^0.11.0", @@ -160,6 +163,86 @@ "node": ">=6.0.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz", + "integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==", + "dev": true, + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "dev": true + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz", + "integrity": "sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw==", + "dev": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "9.0.6", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.6.3", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/@aw-web-design/x-default-browser": { "version": "1.4.126", "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", @@ -2211,29 +2294,28 @@ "node": ">=0.1.90" } }, - "node_modules/@cspotcode/source-map-consumer": { - "version": "0.8.0", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/@cspotcode/source-map-support": { - "version": "0.7.0", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "@cspotcode/source-map-consumer": "0.8.0" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { "node": ">=12" } }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -2798,6 +2880,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==", + "dev": true + }, "node_modules/@fal-works/esbuild-plugin-global-externals": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", @@ -5353,6 +5441,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true + }, "node_modules/@juggle/resize-observer": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", @@ -6502,6 +6596,64 @@ "win32" ] }, + "node_modules/@rtk-query/codegen-openapi": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rtk-query/codegen-openapi/-/codegen-openapi-1.2.0.tgz", + "integrity": "sha512-Sru3aPHyFC0Tb7jeFh/kVMGBdQUcofb9frrHhjNSRLEoJWsG9fjaioUx3nPT5HZVbdAvAFF4xMWFQNfgJBrAGw==", + "dev": true, + "dependencies": { + "@apidevtools/swagger-parser": "^10.0.2", + "commander": "^6.2.0", + "oazapfts": "^4.8.0", + "prettier": "^2.2.1", + "semver": "^7.3.5", + "swagger2openapi": "^7.0.4", + "typescript": "^5.0.0" + }, + "bin": { + "rtk-query-codegen-openapi": "lib/bin/cli.js" + } + }, + "node_modules/@rtk-query/codegen-openapi/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@rtk-query/codegen-openapi/node_modules/oazapfts": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/oazapfts/-/oazapfts-4.12.0.tgz", + "integrity": "sha512-hNKRG4eLYceuJuqDDx7Uqsi8p3j5k83gNKSo2qnUOTiiU03sCQOjXxOqCXDbzRcuDFyK94+1PBIpotK4NoxIjw==", + "dev": true, + "dependencies": { + "@apidevtools/swagger-parser": "^10.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "swagger2openapi": "^7.0.8", + "typescript": "^5.2.2" + }, + "bin": { + "oazapfts": "lib/codegen/cli.js" + } + }, + "node_modules/@rtk-query/codegen-openapi/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@sentry-internal/feedback": { "version": "7.85.0", "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.85.0.tgz", @@ -16625,30 +16777,22 @@ "node_modules/@tsconfig/node10": { "version": "1.0.8", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.9", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.1", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.2", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@types/aria-query": { "version": "5.0.1", @@ -18722,9 +18866,7 @@ "node_modules/arg": { "version": "4.1.3", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/argparse": { "version": "1.0.10", @@ -19713,6 +19855,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==", + "dev": true + }, "node_modules/callsites": { "version": "3.1.0", "license": "MIT", @@ -20580,13 +20728,6 @@ "node": ">=8" } }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -21455,9 +21596,7 @@ "node_modules/create-require": { "version": "1.1.1", "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/crypto-random-string": { "version": "2.0.0", @@ -22761,8 +22900,6 @@ "version": "4.0.2", "dev": true, "license": "BSD-3-Clause", - "optional": true, - "peer": true, "engines": { "node": ">=0.3.1" } @@ -23321,6 +23458,28 @@ "esbuild": ">=0.12 <1" } }, + "node_modules/esbuild-runner": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/esbuild-runner/-/esbuild-runner-2.2.2.tgz", + "integrity": "sha512-fRFVXcmYVmSmtYm2mL8RlUASt2TDkGh3uRcvHFOKNr/T58VrfVeKD9uT9nlgxk96u0LS0ehS/GY7Da/bXWKkhw==", + "dev": true, + "dependencies": { + "source-map-support": "0.5.21", + "tslib": "2.4.0" + }, + "bin": { + "esr": "bin/esr.js" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/esbuild-runner/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, "node_modules/esbuild/node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -24890,6 +25049,12 @@ "version": "2.0.6", "license": "MIT" }, + "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==", + "dev": true + }, "node_modules/fastq": { "version": "1.13.0", "dev": true, @@ -26881,6 +27046,12 @@ "node": ">=12" } }, + "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==", + "dev": true + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "license": "MIT", @@ -32503,9 +32674,7 @@ "node_modules/make-error": { "version": "1.3.6", "dev": true, - "license": "ISC", - "optional": true, - "peer": true + "license": "ISC" }, "node_modules/make-event-props": { "version": "1.6.2", @@ -34042,8 +34211,12 @@ } }, "node_modules/minimist": { - "version": "1.2.6", - "license": "MIT" + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/minipass": { "version": "7.0.3", @@ -34241,6 +34414,18 @@ } } }, + "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==", + "dev": true, + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/node-fetch-native": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.4.1.tgz", @@ -34283,6 +34468,21 @@ "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==", + "dev": true, + "dependencies": { + "es6-promise": "^3.2.1" + } + }, + "node_modules/node-readfiles/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==", + "dev": true + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -34660,6 +34860,226 @@ "node": ">=6" } }, + "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==", + "dev": 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==", + "dev": 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==", + "dev": 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-resolver/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/oas-resolver/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/oas-resolver/node_modules/cliui": { + "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, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/oas-resolver/node_modules/color-convert": { + "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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/oas-resolver/node_modules/color-name": { + "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 + }, + "node_modules/oas-resolver/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/oas-resolver/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/oas-resolver/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/oas-resolver/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/oas-resolver/node_modules/wrap-ansi": { + "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, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/oas-resolver/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/oas-resolver/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/oas-resolver/node_modules/yargs-parser": { + "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, + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": 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==", + "dev": 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", "license": "MIT", @@ -34834,6 +35254,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "peer": true + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -37220,6 +37647,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "dev": 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", @@ -37812,6 +38248,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -38399,6 +38844,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==", + "dev": 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==", + "dev": 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==", + "dev": 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==", + "dev": 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==", + "dev": 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==", + "dev": true + }, "node_modules/showdown": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz", @@ -39264,6 +39763,183 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "dev": 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/swagger2openapi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/swagger2openapi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/swagger2openapi/node_modules/cliui": { + "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, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/swagger2openapi/node_modules/color-convert": { + "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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/swagger2openapi/node_modules/color-name": { + "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 + }, + "node_modules/swagger2openapi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/swagger2openapi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/swagger2openapi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swagger2openapi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swagger2openapi/node_modules/wrap-ansi": { + "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, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/swagger2openapi/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger2openapi/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/swagger2openapi/node_modules/yargs-parser": { + "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, + "engines": { + "node": ">=12" + } + }, "node_modules/swiper": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/swiper/-/swiper-9.4.1.tgz", @@ -39578,13 +40254,12 @@ } }, "node_modules/ts-node": { - "version": "10.7.0", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "@cspotcode/source-map-support": "0.7.0", + "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -39595,7 +40270,7 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "bin": { @@ -39625,8 +40300,6 @@ "version": "8.2.0", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=0.4.0" } @@ -40300,11 +40973,10 @@ } }, "node_modules/v8-compile-cache-lib": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true }, "node_modules/v8-to-istanbul": { "version": "8.1.1", @@ -40842,15 +41514,6 @@ "node": ">=12.0.0" } }, - "node_modules/wait-on/node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wait-port": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.14.tgz", @@ -41480,6 +42143,14 @@ "devOptional": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", @@ -41521,8 +42192,6 @@ "version": "3.1.1", "dev": true, "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=6" } diff --git a/client/package.json b/client/package.json index 9cc6d60664..139909e188 100644 --- a/client/package.json +++ b/client/package.json @@ -96,6 +96,7 @@ }, "devDependencies": { "@nabla/vite-plugin-eslint": "^2.0.2", + "@rtk-query/codegen-openapi": "^1.2.0", "@storybook/addon-actions": "^7.6.0", "@storybook/addon-essentials": "^7.6.0", "@storybook/addon-interactions": "^7.6.0", @@ -129,6 +130,7 @@ "@typescript-eslint/parser": "^6.13.2", "@vitejs/plugin-react": "^4.2.0", "concurrently": "^8.2.2", + "esbuild-runner": "^2.2.2", "eslint": "^8.54.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jest": "^27.6.0", @@ -143,6 +145,7 @@ "pretty-quick": "^3.1.3", "react-test-renderer": "^18.2.0", "storybook": "^7.6.0", + "ts-node": "^10.9.2", "typescript": "^5.3.2", "vite": "^5.0.12", "vite-bundle-visualizer": "^0.11.0", diff --git a/client/src/App.jsx b/client/src/App.jsx index f7f0fec0f2..c616f88573 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -51,6 +51,11 @@ import Cookie from "./privacy/Cookie"; import LazyProjectView from "./project/LazyProjectView"; import LazyProjectList from "./project/list/LazyProjectList"; import LazyNewProject from "./project/new/LazyNewProject"; +import { + ProjectV2List, + ProjectV2New, + ProjectV2Show, +} from "./features/projectsV2/"; import LazyStyleGuide from "./styleguide/LazyStyleGuide"; import AppContext from "./utils/context/appContext"; import useLegacySelector from "./utils/customHooks/useLegacySelector.hook"; @@ -271,6 +276,30 @@ function CentralContentContainer(props) { )} /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> ( diff --git a/client/src/features/projectsV2/api/index.ts b/client/src/features/projectsV2/api/index.ts new file mode 100644 index 0000000000..fa9ec5e19d --- /dev/null +++ b/client/src/features/projectsV2/api/index.ts @@ -0,0 +1,58 @@ +export { projectV2Api } from "./projectV2.enhanced-api"; + +export type { + CreationDate, + Description, + ErrorResponse, + Member, + MemberWithRole, + MembersWithRoles, + Name, + Project, + ProjectsList, + ProjectPatch, + ProjectPost, + RepositoriesList, + Role, + Slug, + Ulid, + UserId, + Visibility, +} from "./projectV2.api"; + +export type { + DeleteProjectsByProjectIdApiArg, + DeleteProjectsByProjectIdApiResponse, + DeleteProjectsByProjectIdMembersAndMemberIdApiArg, + DeleteProjectsByProjectIdMembersAndMemberIdApiResponse, + FullUsersWithRoles, + GetProjectsByProjectIdApiArg, + GetProjectsByProjectIdApiResponse, + GetProjectsByProjectIdMembersApiArg, + GetProjectsByProjectIdMembersApiResponse, + GetProjectsApiResponse, + GetProjectsApiArg, + PatchProjectsByProjectIdApiResponse, + PatchProjectsByProjectIdApiArg, + PatchProjectsByProjectIdMembersApiArg, + PatchProjectsByProjectIdMembersApiResponse, + PostProjectsApiResponse, + PostProjectsApiArg, +} from "./projectV2.api"; + +export { + useGetProjectsQuery, + usePostProjectsMutation, + useGetProjectsByProjectIdQuery, + usePatchProjectsByProjectIdMutation, + useDeleteProjectsByProjectIdMutation, + useGetProjectsByProjectIdMembersQuery, + usePatchProjectsByProjectIdMembersMutation, + useDeleteProjectsByProjectIdMembersAndMemberIdMutation, +} from "./projectV2.enhanced-api"; + +import type { ErrorResponse } from "./projectV2.api"; + +export function isErrorResponse(arg: unknown): arg is { data: ErrorResponse } { + return (arg as { data: ErrorResponse }).data?.error != null; +} diff --git a/client/src/features/projectsV2/api/projectV2-empty.api.ts b/client/src/features/projectsV2/api/projectV2-empty.api.ts new file mode 100644 index 0000000000..6b8693da4e --- /dev/null +++ b/client/src/features/projectsV2/api/projectV2-empty.api.ts @@ -0,0 +1,26 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; + +// initialize an empty api service that we'll inject endpoints into later as needed +export const projectV2EmptyApi = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: "/ui-server/api/data" }), + endpoints: () => ({}), + reducerPath: "projectV2Api", +}); diff --git a/client/src/features/projectsV2/api/projectV2.api-config.ts b/client/src/features/projectsV2/api/projectV2.api-config.ts new file mode 100644 index 0000000000..a058ec0cf0 --- /dev/null +++ b/client/src/features/projectsV2/api/projectV2.api-config.ts @@ -0,0 +1,32 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Run `npx @rtk-query/codegen-openapi projectV2.api-config.ts` in this folder to generate the API +import type { ConfigFile } from "@rtk-query/codegen-openapi"; +import path from "path"; + +const config: ConfigFile = { + apiFile: "./projectV2-empty.api.ts", + apiImport: "projectV2EmptyApi", + outputFile: "./projectV2.api.ts", + exportName: "projectV2Api", + hooks: true, + schemaFile: path.join(__dirname, "projectV2.openapi.json"), +}; + +export default config; diff --git a/client/src/features/projectsV2/api/projectV2.api.ts b/client/src/features/projectsV2/api/projectV2.api.ts new file mode 100644 index 0000000000..5c6089d583 --- /dev/null +++ b/client/src/features/projectsV2/api/projectV2.api.ts @@ -0,0 +1,190 @@ +import { projectV2EmptyApi as api } from "./projectV2-empty.api"; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + getProjects: build.query({ + query: (queryArg) => ({ + url: `/projects`, + params: { page: queryArg.page, per_page: queryArg.perPage }, + }), + }), + postProjects: build.mutation({ + query: (queryArg) => ({ + url: `/projects`, + method: "POST", + body: queryArg.projectPost, + }), + }), + getProjectsByProjectId: build.query< + GetProjectsByProjectIdApiResponse, + GetProjectsByProjectIdApiArg + >({ + query: (queryArg) => ({ url: `/projects/${queryArg.projectId}` }), + }), + patchProjectsByProjectId: build.mutation< + PatchProjectsByProjectIdApiResponse, + PatchProjectsByProjectIdApiArg + >({ + query: (queryArg) => ({ + url: `/projects/${queryArg.projectId}`, + method: "PATCH", + body: queryArg.projectPatch, + }), + }), + deleteProjectsByProjectId: build.mutation< + DeleteProjectsByProjectIdApiResponse, + DeleteProjectsByProjectIdApiArg + >({ + query: (queryArg) => ({ + url: `/projects/${queryArg.projectId}`, + method: "DELETE", + }), + }), + getProjectsByProjectIdMembers: build.query< + GetProjectsByProjectIdMembersApiResponse, + GetProjectsByProjectIdMembersApiArg + >({ + query: (queryArg) => ({ url: `/projects/${queryArg.projectId}/members` }), + }), + patchProjectsByProjectIdMembers: build.mutation< + PatchProjectsByProjectIdMembersApiResponse, + PatchProjectsByProjectIdMembersApiArg + >({ + query: (queryArg) => ({ + url: `/projects/${queryArg.projectId}/members`, + method: "PATCH", + body: queryArg.membersWithRoles, + }), + }), + deleteProjectsByProjectIdMembersAndMemberId: build.mutation< + DeleteProjectsByProjectIdMembersAndMemberIdApiResponse, + DeleteProjectsByProjectIdMembersAndMemberIdApiArg + >({ + query: (queryArg) => ({ + url: `/projects/${queryArg.projectId}/members/${queryArg.memberId}`, + method: "DELETE", + }), + }), + }), + overrideExisting: false, +}); +export { injectedRtkApi as projectV2Api }; +export type GetProjectsApiResponse = + /** status 200 List of projects */ ProjectsList; +export type GetProjectsApiArg = { + /** Result's page number starting from 1 */ + page?: number; + /** The number of results per page */ + perPage?: number; +}; +export type PostProjectsApiResponse = + /** status 201 The project was created */ Project; +export type PostProjectsApiArg = { + projectPost: ProjectPost; +}; +export type GetProjectsByProjectIdApiResponse = + /** status 200 The project */ Project; +export type GetProjectsByProjectIdApiArg = { + projectId: string; +}; +export type PatchProjectsByProjectIdApiResponse = + /** status 200 The patched project */ Project; +export type PatchProjectsByProjectIdApiArg = { + projectId: string; + projectPatch: ProjectPatch; +}; +export type DeleteProjectsByProjectIdApiResponse = + /** status 204 The project was removed or did not exist in the first place */ void; +export type DeleteProjectsByProjectIdApiArg = { + projectId: string; +}; +export type GetProjectsByProjectIdMembersApiResponse = + /** status 200 The project's members */ FullUsersWithRoles; +export type GetProjectsByProjectIdMembersApiArg = { + projectId: string; +}; +export type PatchProjectsByProjectIdMembersApiResponse = + /** status 200 The project's members were updated */ void; +export type PatchProjectsByProjectIdMembersApiArg = { + projectId: string; + membersWithRoles: MembersWithRoles; +}; +export type DeleteProjectsByProjectIdMembersAndMemberIdApiResponse = + /** status 204 The member was removed or wasn't part of project's members. */ void; +export type DeleteProjectsByProjectIdMembersAndMemberIdApiArg = { + projectId: string; + /** This is user's KeyCloak ID */ + memberId: string; +}; +export type Ulid = string; +export type Name = string; +export type Slug = string; +export type CreationDate = string; +export type KeyCloakId = string; +export type Member = { + id: KeyCloakId; +}; +export type Repository = string; +export type RepositoriesList = Repository[]; +export type Visibility = "private" | "public"; +export type Description = string; +export type Project = { + id: Ulid; + name: Name; + slug: Slug; + creation_date: CreationDate; + created_by: Member; + repositories?: RepositoriesList; + visibility: Visibility; + description?: Description; +}; +export type ProjectsList = Project[]; +export type ErrorResponse = { + error: { + code: number; + detail?: string; + message: string; + }; +}; +export type ProjectPost = { + name: Name; + slug?: Slug; + repositories?: RepositoriesList; + visibility?: Visibility; + description?: Description; +}; +export type ProjectPatch = { + name?: Name; + repositories?: RepositoriesList; + visibility?: Visibility; + description?: Description; +}; +export type UserId = string; +export type UserEmail = string; +export type UserFirstLastName = string; +export type UserWithId = { + id: UserId; + email?: UserEmail; + first_name?: UserFirstLastName; + last_name?: UserFirstLastName; +}; +export type Role = "member" | "owner"; +export type FullUserWithRole = { + member: UserWithId; + role: Role; +}; +export type FullUsersWithRoles = FullUserWithRole[]; +export type MemberWithRole = { + member: Member; + role: Role; +}; +export type MembersWithRoles = MemberWithRole[]; +export const { + useGetProjectsQuery, + usePostProjectsMutation, + useGetProjectsByProjectIdQuery, + usePatchProjectsByProjectIdMutation, + useDeleteProjectsByProjectIdMutation, + useGetProjectsByProjectIdMembersQuery, + usePatchProjectsByProjectIdMembersMutation, + useDeleteProjectsByProjectIdMembersAndMemberIdMutation, +} = injectedRtkApi; diff --git a/client/src/features/projectsV2/api/projectV2.enhanced-api.ts b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts new file mode 100644 index 0000000000..e58ea5ccd2 --- /dev/null +++ b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts @@ -0,0 +1,94 @@ +import { projectV2Api as api } from "./projectV2.api"; +import type { + GetProjectsApiArg, + GetProjectsApiResponse as GetProjectsApiResponseOrig, + ProjectsList, +} from "./projectV2.api"; + +interface GetProjectsApiResponse { + projects: GetProjectsApiResponseOrig; + page: number; + perPage: number; + total: number; + totalPages: number; +} + +const injectedApi = api.injectEndpoints({ + endpoints: (build) => ({ + getProjectsPaged: build.query({ + query: (queryArg) => ({ + url: `/projects`, + params: { page: queryArg.page, per_page: queryArg.perPage }, + }), + transformResponse: (response, meta, queryArg) => { + const projects = response as ProjectsList; + const headerPage = meta?.response?.headers.get("page"); + const headerPerPage = meta?.response?.headers.get("per-page"); + const headerTotal = meta?.response?.headers.get("total"); + const headerTotalPages = meta?.response?.headers.get("total-pages"); + const page = headerPage ? parseInt(headerPage) : queryArg.page ?? 1; + const perPage = headerPerPage + ? parseInt(headerPerPage) + : queryArg.perPage ?? 20; + const total = headerTotal + ? parseInt(headerTotal) + : projects + ? projects.length + : 0; + const totalPages = headerTotalPages + ? parseInt(headerTotalPages) + : total / perPage; + return { + projects, + page, + perPage, + total, + totalPages, + }; + }, + }), + }), +}); + +const enhancedApi = injectedApi.enhanceEndpoints({ + addTagTypes: ["Project", "Members"], + endpoints: { + deleteProjectsByProjectId: { + invalidatesTags: ["Project"], + }, + deleteProjectsByProjectIdMembersAndMemberId: { + invalidatesTags: ["Members"], + }, + // alternatively, define a function which is called with the endpoint definition as an argument + getProjects: { + providesTags: ["Project"], + }, + getProjectsPaged: { + providesTags: ["Project"], + }, + getProjectsByProjectId: { + providesTags: ["Project"], + }, + getProjectsByProjectIdMembers: { + providesTags: ["Members"], + }, + patchProjectsByProjectId: { + invalidatesTags: ["Project"], + }, + patchProjectsByProjectIdMembers: { + invalidatesTags: ["Members"], + }, + }, +}); + +export { enhancedApi as projectV2Api }; +export const { + useGetProjectsPagedQuery: useGetProjectsQuery, + usePostProjectsMutation, + useGetProjectsByProjectIdQuery, + usePatchProjectsByProjectIdMutation, + useDeleteProjectsByProjectIdMutation, + useGetProjectsByProjectIdMembersQuery, + usePatchProjectsByProjectIdMembersMutation, + useDeleteProjectsByProjectIdMembersAndMemberIdMutation, +} = enhancedApi; diff --git a/client/src/features/projectsV2/api/projectV2.openapi.json b/client/src/features/projectsV2/api/projectV2.openapi.json new file mode 100644 index 0000000000..f8ad5ceae8 --- /dev/null +++ b/client/src/features/projectsV2/api/projectV2.openapi.json @@ -0,0 +1,693 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Renku Project Management Service", + "description": "Service that allows creating, updating, deleting, and managing Renku native projects.\nAll errors have the same format as the schema called ErrorResponse.\n", + "version": "0.5.0" + }, + "servers": [ + { + "url": "/api/data" + }, + { + "url": "/ui-server/api/data" + } + ], + "paths": { + "/projects": { + "get": { + "summary": "Get all projects", + "parameters": [ + { + "in": "query", + "description": "Result's page number starting from 1", + "name": "page", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "default": 1 + } + }, + { + "in": "query", + "description": "The number of results per page", + "name": "per_page", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "List of projects", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectsList" + } + } + }, + "headers": { + "page": { + "description": "The index of the current page (starting at 1).", + "required": true, + "schema": { + "type": "integer" + } + }, + "per-page": { + "description": "The number of items per page.", + "required": true, + "schema": { + "type": "integer" + } + }, + "total": { + "description": "The total number of items.", + "required": true, + "schema": { + "type": "integer" + } + }, + "total-pages": { + "description": "The total number of pages.", + "required": true, + "schema": { + "type": "integer" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + }, + "post": { + "summary": "Create a new project", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectPost" + } + } + } + }, + "responses": { + "201": { + "description": "The project was created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + } + }, + "/projects/{project_id}": { + "get": { + "summary": "Get a project", + "parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The project", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "404": { + "description": "The project does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + }, + "patch": { + "summary": "Update specific fields of an existing project", + "parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectPatch" + } + } + } + }, + "responses": { + "200": { + "description": "The patched project", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "404": { + "description": "The project does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + }, + "delete": { + "summary": "Remove a project", + "parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "The project was removed or did not exist in the first place" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + } + }, + "/projects/{project_id}/members": { + "get": { + "summary": "Get all members of a project", + "parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The project's members", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullUsersWithRoles" + } + } + } + }, + "404": { + "description": "The project does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + }, + "patch": { + "summary": "New members in the list are added to the project's members. If a member\nalready exists, then only the role is updated. No member will be deleted\nin this endpoint.\n", + "parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MembersWithRoles" + } + } + } + }, + "responses": { + "200": { + "description": "The project's members were updated" + }, + "404": { + "description": "The project does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + } + }, + "/projects/{project_id}/members/{member_id}": { + "delete": { + "summary": "Remove a member from a project", + "parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "member_id", + "description": "This is user's KeyCloak ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "The member was removed or wasn't part of project's members." + }, + "404": { + "description": "The project does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + } + } + }, + "components": { + "schemas": { + "ProjectsList": { + "description": "A list of Renku projects", + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + }, + "minItems": 0 + }, + "Project": { + "description": "A Renku native project definition and metadata", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/Ulid" + }, + "name": { + "$ref": "#/components/schemas/Name" + }, + "slug": { + "$ref": "#/components/schemas/Slug" + }, + "creation_date": { + "$ref": "#/components/schemas/CreationDate" + }, + "created_by": { + "$ref": "#/components/schemas/Member" + }, + "repositories": { + "$ref": "#/components/schemas/RepositoriesList" + }, + "visibility": { + "$ref": "#/components/schemas/Visibility" + }, + "description": { + "$ref": "#/components/schemas/Description" + } + }, + "required": [ + "id", + "name", + "slug", + "created_by", + "creation_date", + "visibility" + ], + "example": { + "id": "01AN4Z79ZS5XN0F25N3DB94T4R", + "name": "Renku R Project", + "slug": "r-project", + "created_by": { + "id": "owner-KC-id" + }, + "visibility": "public", + "repositories": [ + { + "url": "https://github.com/SwissDataScienceCenter/project-1.git" + }, + { + "url": "git@github.com:SwissDataScienceCenter/project-2.git" + } + ] + } + }, + "ProjectPost": { + "description": "Project metadata to be created in Renku", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "$ref": "#/components/schemas/Name" + }, + "slug": { + "$ref": "#/components/schemas/Slug" + }, + "repositories": { + "$ref": "#/components/schemas/RepositoriesList" + }, + "visibility": { + "$ref": "#/components/schemas/Visibility", + "default": "private" + }, + "description": { + "$ref": "#/components/schemas/Description" + } + }, + "required": ["name"] + }, + "ProjectPatch": { + "type": "object", + "description": "Patch of a project", + "additionalProperties": false, + "properties": { + "name": { + "$ref": "#/components/schemas/Name" + }, + "repositories": { + "$ref": "#/components/schemas/RepositoriesList" + }, + "visibility": { + "$ref": "#/components/schemas/Visibility" + }, + "description": { + "$ref": "#/components/schemas/Description" + } + } + }, + "Ulid": { + "description": "ULID identifier", + "type": "string", + "minLength": 26, + "maxLength": 26, + "pattern": "^[A-Z0-9]{26}$", + "format": "ulid" + }, + "Name": { + "description": "Renku project name", + "type": "string", + "minLength": 1, + "maxLength": 99, + "example": "My Renku Project :)" + }, + "Slug": { + "description": "A command-line friendly name for a project", + "type": "string", + "minLength": 1, + "maxLength": 99, + "pattern": "^[a-z0-9]+[a-z0-9._-]*$", + "example": "my-renku-project" + }, + "CreationDate": { + "description": "The date and time the project was created (time is always in UTC)", + "type": "string", + "format": "date-time", + "example": "2023-11-01T17:32:28Z" + }, + "Description": { + "description": "A description for project", + "type": "string", + "maxLength": 500 + }, + "Member": { + "description": "A KeyCloak user", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/components/schemas/KeyCloakId" + } + }, + "required": ["id"], + "example": { + "id": "some-keycloak-user-id" + } + }, + "KeyCloakId": { + "description": "Member's KeyCloak ID", + "type": "string", + "pattern": "^[A-Za-z0-9-]+$", + "minLength": 1, + "example": "123-keycloak-user-id-456" + }, + "RepositoriesList": { + "description": "A list of repositories", + "type": "array", + "items": { + "$ref": "#/components/schemas/Repository" + }, + "minItems": 0, + "example": [ + "https://github.com/SwissDataScienceCenter/project-1.git", + "git@github.com:SwissDataScienceCenter/project-2.git" + ] + }, + "Repository": { + "description": "A project's repository", + "type": "string", + "example": "git@github.com:SwissDataScienceCenter/project-1.git" + }, + "Visibility": { + "description": "Project's visibility levels", + "type": "string", + "enum": ["private", "public"] + }, + "MembersWithRoles": { + "description": "List of members and their access level to the project", + "type": "array", + "items": { + "$ref": "#/components/schemas/MemberWithRole" + }, + "minItems": 0, + "example": [ + { + "id": "some-keycloak-user-id", + "role": "owner" + }, + { + "id": "another-keycloak-user-id", + "role": "member" + } + ] + }, + "MemberWithRole": { + "description": "A member and the access level to the project", + "type": "object", + "additionalProperties": false, + "properties": { + "member": { + "$ref": "#/components/schemas/Member" + }, + "role": { + "$ref": "#/components/schemas/Role" + } + }, + "required": ["member", "role"], + "example": { + "member": { + "id": "some-keycloak-user-id" + }, + "role": "owner" + } + }, + "FullUsersWithRoles": { + "description": "List of members with full info and their access level to the project", + "type": "array", + "items": { + "$ref": "#/components/schemas/FullUserWithRole" + }, + "minItems": 0 + }, + "FullUserWithRole": { + "description": "A member with full info (email, name, ...) and the access level to the project", + "type": "object", + "additionalProperties": false, + "properties": { + "member": { + "$ref": "#/components/schemas/UserWithId" + }, + "role": { + "$ref": "#/components/schemas/Role" + } + }, + "required": ["member", "role"] + }, + "Role": { + "description": "Possible roles of members in a project", + "type": "string", + "enum": ["member", "owner"] + }, + "UserWithId": { + "type": "object", + "description": "This is copied from ../users/api.spec.yaml", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/components/schemas/UserId" + }, + "email": { + "$ref": "#/components/schemas/UserEmail" + }, + "first_name": { + "$ref": "#/components/schemas/UserFirstLastName" + }, + "last_name": { + "$ref": "#/components/schemas/UserFirstLastName" + } + }, + "required": ["id"], + "example": { + "id": "some-random-keycloak-id", + "email": "user@gmail.com" + } + }, + "UserId": { + "type": "string", + "description": "Keycloak user ID", + "example": "f74a228b-1790-4276-af5f-25c2424e9b0c" + }, + "UserFirstLastName": { + "type": "string", + "description": "First or last name of the user", + "example": "John", + "minLength": 1, + "maxLength": 256 + }, + "UserEmail": { + "type": "string", + "format": "email", + "description": "User email", + "example": "some-user@gmail.com" + }, + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "example": 1404 + }, + "detail": { + "type": "string", + "example": "A more detailed optional message showing what the problem was" + }, + "message": { + "type": "string", + "example": "Something went wrong - please try again later" + } + }, + "required": ["code", "message"] + } + }, + "required": ["error"] + } + }, + "responses": { + "Error": { + "description": "The schema for all 4xx and 5xx responses", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } +} diff --git a/client/src/features/projectsV2/edit/ProjectRepositoryFormField.tsx b/client/src/features/projectsV2/edit/ProjectRepositoryFormField.tsx new file mode 100644 index 0000000000..34129f123a --- /dev/null +++ b/client/src/features/projectsV2/edit/ProjectRepositoryFormField.tsx @@ -0,0 +1,103 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; + +import { Controller } from "react-hook-form"; +import type { FieldValues } from "react-hook-form"; + +import { Button, FormText, Input, Label } from "reactstrap"; + +import type { Repository } from "../projectV2.types"; +import type { GenericProjectFormFieldProps } from "./formField.types"; + +interface ProjectRepositoryFormFieldProps + extends GenericProjectFormFieldProps { + id: string; + index: number; + onDelete: () => void; +} + +interface ProjectV2Repositories extends FieldValues { + repositories: Repository[]; +} + +export default function ProjectRepositoryFormField({ + control, + defaultValue, + errors, + id, + index, + name, + onDelete, +}: ProjectRepositoryFormFieldProps) { + return ( +
+ +
0 && + (errors.repositories == null || + errors.repositories[index] == null) && + "mb-2" + )} + > + ( + + )} + rules={{ + required: true, + pattern: /^(http|https):\/\/[^ "]+$/, + }} + /> + +
+ {errors.repositories && errors.repositories[index] ? null : index > + 0 ? null : ( + + A URL that refers to a git repository. + + )} +
+ Please provide a valid URL or remove the repository. +
+
+ ); +} diff --git a/client/src/features/projectsV2/edit/formField.types.ts b/client/src/features/projectsV2/edit/formField.types.ts new file mode 100644 index 0000000000..10560a3596 --- /dev/null +++ b/client/src/features/projectsV2/edit/formField.types.ts @@ -0,0 +1,10 @@ +import type { + FieldErrors, + FieldValues, + UseControllerProps, +} from "react-hook-form"; + +export interface GenericProjectFormFieldProps + extends UseControllerProps { + errors: FieldErrors; +} diff --git a/client/src/features/projectsV2/edit/index.tsx b/client/src/features/projectsV2/edit/index.tsx new file mode 100644 index 0000000000..a6a7266663 --- /dev/null +++ b/client/src/features/projectsV2/edit/index.tsx @@ -0,0 +1,29 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + ProjectDescriptionFormField, + ProjectNameFormField, + ProjectSlugFormField, + ProjectVisibilityFormField, +} from "./simpleFormFields"; + +export { AddProjectMemberModal } from "./projectMemberFields"; +import ProjectRepositoryFormField from "./ProjectRepositoryFormField"; + +export { ProjectRepositoryFormField }; diff --git a/client/src/features/projectsV2/edit/projectMemberFields.tsx b/client/src/features/projectsV2/edit/projectMemberFields.tsx new file mode 100644 index 0000000000..bca64bbdd1 --- /dev/null +++ b/client/src/features/projectsV2/edit/projectMemberFields.tsx @@ -0,0 +1,283 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; + +import { useCallback, useEffect, useState } from "react"; +import { PlusLg, XLg } from "react-bootstrap-icons"; +import { Controller, useForm } from "react-hook-form"; + +import { + Button, + Form, + Input, + Label, + Modal, + ModalBody, + ModalFooter, + ModalHeader, +} from "reactstrap"; + +import { useGetUsersQuery } from "../../user/dataServicesUser.api"; +import type { UserWithId } from "../../user/dataServicesUser.api"; + +import type { FullUsersWithRoles, MemberWithRole } from "../api"; +import { usePatchProjectsByProjectIdMembersMutation } from "../api"; +import type { ProjectMember } from "../projectV2.types"; + +const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; + +interface AddProjectMemberModalProps { + isOpen: boolean; + members: FullUsersWithRoles; + projectId: string; + toggle: () => void; +} + +type NewProjectMember = Pick; + +interface AddProjectMemberEmailLookupFormProps + extends Pick { + setNewMember: (user: UserWithId) => void; +} +function AddProjectMemberEmailLookupForm({ + setNewMember, + toggle, +}: AddProjectMemberEmailLookupFormProps) { + const [lookupEmail, setLookupEmail] = useState(undefined); + const [isUserNotFound, setIsUserNotFound] = useState(false); + const { data, isLoading } = useGetUsersQuery( + { exactEmail: lookupEmail }, + { skip: lookupEmail == null } + ); + + useEffect(() => { + if (data == null) return; + if (data.length < 1) { + setIsUserNotFound(true); + return; + } + setNewMember(data[0]); + }, [data, setNewMember, setIsUserNotFound]); + + const { + control, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: { + email: "", + }, + }); + + const onSubmit = useCallback( + (data: NewProjectMember) => { + setIsUserNotFound(false); + setLookupEmail(data.email); + }, + [setLookupEmail] + ); + + return ( + <> + +
+
+ + ( + + )} + rules={{ required: true, pattern: emailRegex }} + /> +
+ Please provide the email address for the member to add. +
+ {isUserNotFound &&
No user found for {lookupEmail}.
} +
+
+ +
+
+
+ + + + + + ); +} + +interface ProjectMemberForAdd extends MemberWithRole { + email: string; +} + +interface AddProjectMemberAccessFormProps + extends Pick { + user: UserWithId; +} +function AddProjectMemberAccessForm({ + members, + projectId, + toggle, + user, +}: AddProjectMemberAccessFormProps) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [patchProjectMembers, _result] = + usePatchProjectsByProjectIdMembersMutation(); + const { control, handleSubmit } = useForm({ + defaultValues: { + member: { id: user.id }, + email: user.email, + role: "member", + }, + }); + + const onSubmit = useCallback( + (data: ProjectMemberForAdd) => { + const projectMembers = members.map((m) => ({ + member: { id: m.member.id }, + role: m.role, + })); + projectMembers.push({ member: { id: data.member.id }, role: data.role }); + + patchProjectMembers({ projectId, membersWithRoles: projectMembers }).then( + () => { + toggle(); + } + ); + }, + [patchProjectMembers, projectId, members, toggle] + ); + + return ( + <> + +
+
+ + ( + + + + + )} + rules={{ required: true }} + /> +
+
+
+ + + + + + ); +} + +export function AddProjectMemberModal({ + isOpen, + members, + projectId, + toggle, +}: AddProjectMemberModalProps) { + const [newMember, setNewMember] = useState(undefined); + const toggleVisible = useCallback(() => { + setNewMember(undefined); + toggle(); + }, [setNewMember, toggle]); + + return ( + + Add a project member + {newMember == null && ( + + )} + {newMember != null && ( + + )} + + ); +} diff --git a/client/src/features/projectsV2/edit/simpleFormFields.tsx b/client/src/features/projectsV2/edit/simpleFormFields.tsx new file mode 100644 index 0000000000..38c914c299 --- /dev/null +++ b/client/src/features/projectsV2/edit/simpleFormFields.tsx @@ -0,0 +1,169 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; + +import { Controller } from "react-hook-form"; +import type { FieldValues } from "react-hook-form"; + +import { FormText, Input, Label } from "reactstrap"; +import type { GenericProjectFormFieldProps } from "./formField.types"; + +export function ProjectDescriptionFormField({ + control, + errors, + name, +}: GenericProjectFormFieldProps) { + return ( +
+ + ( + + )} + rules={{ maxLength: 500, required: false }} + /> + {errors[name] ? null : ( + + A brief (at most 500 character) description of the project. + + )} +
Please provide a description
+
+ ); +} + +export function ProjectNameFormField({ + control, + errors, + name, +}: GenericProjectFormFieldProps) { + return ( +
+ + ( + + )} + rules={{ required: true, maxLength: 99 }} + /> + {errors[name] ? null : ( + + The name you will use to refer to the project + + )} +
Please provide a name
+
+ ); +} + +export function ProjectSlugFormField({ + control, + errors, + name, +}: GenericProjectFormFieldProps) { + return ( +
+ + ( + + )} + rules={{ required: true, maxLength: 99, pattern: /^[a-z0-9-]+$/ }} + /> + {errors[name] ? null : ( + + A short, machine-readable identifier for the project, restricted to + lowercase letters, numbers, and hyphens.{" "} + Cannot be changed after project creation. + + )} +
+ Please provide a slug consisting of lowercase letters, numbers, and + hyphens. +
+
+ ); +} + +export function ProjectVisibilityFormField({ + control, + errors, + name, +}: GenericProjectFormFieldProps) { + return ( +
+ + ( + + + + + )} + rules={{ required: true }} + /> + {errors[name] ? null : ( + + Should the project be visible to everyone or only to members? + + )} +
Please select a visibility
+
+ ); +} diff --git a/client/src/features/projectsV2/index.ts b/client/src/features/projectsV2/index.ts new file mode 100644 index 0000000000..de8564487d --- /dev/null +++ b/client/src/features/projectsV2/index.ts @@ -0,0 +1,7 @@ +export { projectV2Api } from "./api/"; +export { projectV2NewSlice } from "./new/projectV2New.slice"; + +import ProjectV2List from "./list/ProjectV2List"; +import ProjectV2New from "./new/ProjectV2New"; +import ProjectV2Show from "./show/ProjectV2Show"; +export { ProjectV2List, ProjectV2New, ProjectV2Show }; diff --git a/client/src/features/projectsV2/list/ProjectV2List.tsx b/client/src/features/projectsV2/list/ProjectV2List.tsx new file mode 100644 index 0000000000..b524cfdeb6 --- /dev/null +++ b/client/src/features/projectsV2/list/ProjectV2List.tsx @@ -0,0 +1,108 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +import { useState } from "react"; +import { Link } from "react-router-dom"; +import cx from "classnames"; + +import FormSchema from "../../../components/formschema/FormSchema"; +import { Loader } from "../../../components/Loader"; +import { Pagination } from "../../../components/Pagination"; +import { TimeCaption } from "../../../components/TimeCaption"; +import { Url } from "../../../utils/helpers/url"; + +import { useGetProjectsQuery } from "../api"; +import type { Project } from "../api"; + +import styles from "./projectV2List.module.scss"; + +interface ProjectV2ListProjectProps { + project: Project; +} +function ProjectV2ListProject({ project }: ProjectV2ListProjectProps) { + const projectUrl = Url.get(Url.pages.projectsV2.show, { id: project.id }); + return ( +
+
+

+ {project.name} +

+
{project.description}
+
+ {project.visibility} + +
+
+
+ ); +} + +function ProjectList() { + const perPage = 10; + const [page, setPage] = useState(1); + const { data, error, isLoading } = useGetProjectsQuery({ + page, + perPage, + }); + + if (isLoading) + return ( +
+
+ +
Retrieving projects...
+
+
+ ); + if (error) return
Cannot show projects.
; + + if (data == null) return
No V2 projects.
; + + return ( + <> +
+ {data.projects?.map((project) => ( + + ))} +
+ { + setPage(page); + }} + className="d-flex justify-content-center rk-search-pagination" + /> + + ); +} + +export default function ProjectV2List() { + return ( + All visible projects} + > + + + ); +} diff --git a/client/src/features/projectsV2/list/projectV2List.module.scss b/client/src/features/projectsV2/list/projectV2List.module.scss new file mode 100644 index 0000000000..1bb37db9d5 --- /dev/null +++ b/client/src/features/projectsV2/list/projectV2List.module.scss @@ -0,0 +1,3 @@ +.listProjectWidth { + min-width: 250px; +} diff --git a/client/src/features/projectsV2/new/ProjectV2FormSubmitGroup.tsx b/client/src/features/projectsV2/new/ProjectV2FormSubmitGroup.tsx new file mode 100644 index 0000000000..a30f5e0cfb --- /dev/null +++ b/client/src/features/projectsV2/new/ProjectV2FormSubmitGroup.tsx @@ -0,0 +1,66 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; +import { useCallback } from "react"; +import { useDispatch } from "react-redux"; + +import { Button } from "reactstrap"; + +import type { NewProjectV2State } from "./projectV2New.slice"; +import { setCurrentStep } from "./projectV2New.slice"; + +interface ProjectV2NewFormProps { + currentStep: NewProjectV2State["currentStep"]; +} +export default function ProjectFormSubmitGroup({ + currentStep, +}: ProjectV2NewFormProps) { + const dispatch = useDispatch(); + + const previousStep = useCallback(() => { + if (currentStep < 1) return; + const previousStep = (currentStep - 1) as typeof currentStep; + dispatch(setCurrentStep(previousStep)); + }, [currentStep, dispatch]); + + return ( +
+ +
+ {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )} + {currentStep === 2 && } + {currentStep === 3 && } +
+
+ ); +} diff --git a/client/src/features/projectsV2/new/ProjectV2New.tsx b/client/src/features/projectsV2/new/ProjectV2New.tsx new file mode 100644 index 0000000000..25e30d7b57 --- /dev/null +++ b/client/src/features/projectsV2/new/ProjectV2New.tsx @@ -0,0 +1,205 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FormEvent, useCallback } from "react"; +import { useDispatch } from "react-redux"; + +import { Button, Form, Label } from "reactstrap"; + +import { Loader } from "../../../components/Loader"; +import FormSchema from "../../../components/formschema/FormSchema"; +import useAppSelector from "../../../utils/customHooks/useAppSelector.hook"; +import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; + +import { usePostProjectsMutation } from "../api"; +import type { ProjectPost } from "../api"; +import { ProjectV2DescriptionAndRepositories } from "../show/ProjectV2Show"; + +import type { NewProjectV2State } from "./projectV2New.slice"; +import { setCurrentStep } from "./projectV2New.slice"; +import ProjectFormSubmitGroup from "./ProjectV2FormSubmitGroup"; +import ProjectV2NewForm from "./ProjectV2NewForm"; + +function projectToProjectPost( + project: NewProjectV2State["project"] +): ProjectPost { + return { + name: project.metadata.name, + slug: project.metadata.slug, + description: project.metadata.description, + visibility: project.access.visibility, + repositories: project.content.repositories + .map((r) => r.url.trim()) + .filter((r) => r.length > 0), + }; +} + +function ProjectV2NewAccessStepHeader() { + return ( + <> + Set up visibility and access +

Decide who can see your project and who is allowed to work in it.

+ + ); +} + +function ProjectV2NewHeader({ + currentStep, +}: Pick) { + return ( + <> +

+ V2 Projects let you group together related resources and control who can + access them. +

+ {currentStep === 0 && } + {currentStep === 1 && } + {currentStep === 2 && } + {currentStep === 3 && } + + ); +} + +function ProjectV2NewMetadataStepHeader() { + return ( + <> + Describe your project +

Provide some information to explain what your project is about.

+ + ); +} + +function ProjectV2NewProjectCreatingStepHeader() { + return ( + <> + Review and create +

Review what has been entered and, if ready, create the project.

+ + ); +} + +function ProjectV2NewRepositoryStepHeader() { + return ( + <> + Associate some repositories (optional) +

+ You can associate one or more repositories with the project now if you + want. This can also be done later at any time. +

+ + ); +} + +function ProjectV2BeingCreatedLoader() { + return ( +
+
+ +
Creating project...
+
+
+ ); +} + +function ProjectV2BeingCreated({ + result, +}: { + result: ReturnType[1]; +}) { + const dispatch = useDispatch(); + + const previousStep = useCallback(() => { + dispatch(setCurrentStep(2)); + }, [dispatch]); + + if (result.isLoading) { + return ; + } + + if (result.isError || result.data == null) { + return ( +
+

Something went wrong.

+
+ +
+
+ ); + } + return
Project created
; +} + +function ProjectV2NewReviewCreateStep({ + currentStep, +}: Pick) { + const { project } = useAppSelector((state) => state.newProjectV2); + const [createProject, result] = usePostProjectsMutation(); + const newProject = projectToProjectPost(project); + + const onSubmit = useCallback( + (e: FormEvent) => { + e.preventDefault(); + createProject({ projectPost: newProject }); + }, + [createProject, newProject] + ); + + if (result != null && !result.isUninitialized) { + return ; + } + + return ( +
+

Review

+
+ +
{newProject.name}
+
+
+ +
{newProject.slug}
+
+
+ +
{newProject.visibility}
+
+ + + + ); +} + +export default function ProjectV2New() { + const user = useLegacySelector((state) => state.stateModel.user); + const { currentStep } = useAppSelector((state) => state.newProjectV2); + if (!user.logged) { + return

Please log in to create a project.

; + } + return ( + } + > + {currentStep < 3 && } + {currentStep == 3 && ( + + )} + + ); +} diff --git a/client/src/features/projectsV2/new/ProjectV2NewForm.tsx b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx new file mode 100644 index 0000000000..402690e657 --- /dev/null +++ b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx @@ -0,0 +1,230 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useCallback, useEffect } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { useDispatch } from "react-redux"; +import { Button, Form, Label } from "reactstrap"; + +import useAppSelector from "../../../utils/customHooks/useAppSelector.hook"; +import { slugFromTitle } from "../../../utils/helpers/HelperFunctions"; + +import { + ProjectDescriptionFormField, + ProjectNameFormField, + ProjectRepositoryFormField, + ProjectSlugFormField, + ProjectVisibilityFormField, +} from "../edit"; + +import ProjectFormSubmitGroup from "./ProjectV2FormSubmitGroup"; +import type { NewProjectV2State } from "./projectV2New.slice"; +import { + setAccess, + setContent, + setCurrentStep, + setMetadata, +} from "./projectV2New.slice"; + +interface ProjectV2NewFormProps { + currentStep: NewProjectV2State["currentStep"]; +} +export default function ProjectV2NewForm({ + currentStep, +}: ProjectV2NewFormProps) { + return ( +
+ {currentStep === 0 && ( + + )} + {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + + )} +
+ ); +} + +function ProjectV2NewAccessStepForm({ currentStep }: ProjectV2NewFormProps) { + const dispatch = useDispatch(); + const { project } = useAppSelector((state) => state.newProjectV2); + const { + control, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: project.access, + }); + + const onSubmit = useCallback( + (data: NewProjectV2State["project"]["access"]) => { + dispatch(setAccess(data)); + const nextStep = (currentStep + 1) as typeof currentStep; + dispatch(setCurrentStep(nextStep)); + }, + [currentStep, dispatch] + ); + return ( + <> +

Define access

+
+ +
+
+ +
+
+ + + + ); +} + +function ProjectV2NewMetadataStepForm({ currentStep }: ProjectV2NewFormProps) { + const dispatch = useDispatch(); + const { project } = useAppSelector((state) => state.newProjectV2); + const { + control, + formState: { errors }, + handleSubmit, + setValue, + watch, + } = useForm({ + defaultValues: project.metadata, + }); + + const name = watch("name"); + useEffect(() => { + setValue("slug", slugFromTitle(name, true, true)); + }, [setValue, name]); + + const onSubmit = useCallback( + (data: NewProjectV2State["project"]["metadata"]) => { + dispatch(setMetadata(data)); + const nextStep = (currentStep + 1) as typeof currentStep; + dispatch(setCurrentStep(nextStep)); + }, + [currentStep, dispatch] + ); + + return ( + <> +

Describe the project

+
+ + + + + + + ); +} + +function ProjectV2NewRepositoryStepForm({ + currentStep, +}: ProjectV2NewFormProps) { + const dispatch = useDispatch(); + const { project } = useAppSelector((state) => state.newProjectV2); + const { + control, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: project.content, + }); + const { fields, append, remove } = useFieldArray< + NewProjectV2State["project"]["content"] + >({ + control, + name: "repositories", + }); + + const onAppend = useCallback(() => { + append({ url: "" }); + }, [append]); + const onDelete = useCallback( + (index: number) => { + remove(index); + }, + [remove] + ); + + const onSubmit = useCallback( + (data: NewProjectV2State["project"]["content"]) => { + dispatch(setContent(data)); + const nextStep = (currentStep + 1) as typeof currentStep; + dispatch(setCurrentStep(nextStep)); + }, + [currentStep, dispatch] + ); + return ( + <> +
+

Add repositories

+
+ +
+
+
+
+ {fields.map((f, i) => { + return ( +
+ onDelete(i)} + /> +
+ ); + })} +
+ + + + ); +} diff --git a/client/src/features/projectsV2/new/projectV2New.slice.ts b/client/src/features/projectsV2/new/projectV2New.slice.ts new file mode 100644 index 0000000000..601979a6bf --- /dev/null +++ b/client/src/features/projectsV2/new/projectV2New.slice.ts @@ -0,0 +1,69 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +import type { Project } from "../projectV2.types"; + +type NewProjectV2Step = 0 | 1 | 2 | 3; + +export interface NewProjectV2State { + project: Project; + currentStep: NewProjectV2Step; +} + +const initialState: NewProjectV2State = { + project: { + access: { + members: [], + visibility: "private", + }, + content: { + repositories: [], + }, + metadata: { + name: "", + slug: "", + description: "", + }, + }, + currentStep: 0, +}; + +export const projectV2NewSlice = createSlice({ + name: "newProjectV2", + initialState, + reducers: { + setAccess: (state, action: PayloadAction) => { + state.project.access = action.payload; + }, + setContent: (state, action: PayloadAction) => { + state.project.content = action.payload; + }, + setCurrentStep: (state, action: PayloadAction) => { + state.currentStep = action.payload; + }, + setMetadata: (state, action: PayloadAction) => { + state.project.metadata = action.payload; + }, + reset: () => initialState, + }, +}); + +export const { setAccess, setContent, setCurrentStep, setMetadata, reset } = + projectV2NewSlice.actions; diff --git a/client/src/features/projectsV2/projectV2.types.ts b/client/src/features/projectsV2/projectV2.types.ts new file mode 100644 index 0000000000..039330d0e8 --- /dev/null +++ b/client/src/features/projectsV2/projectV2.types.ts @@ -0,0 +1,29 @@ +import type { Role, Visibility } from "./api"; + +export type ProjectVisibility = Visibility; +export type ProjectRole = Role; + +export interface Project { + access: { + members: ProjectMember[]; + visibility: ProjectVisibility; + }; + content: { + repositories: Repository[]; + }; + metadata: { + name: string; + slug: string; + description: string; + }; +} + +export interface ProjectMember { + providerId?: string; + email: string; + role: ProjectRole; +} + +export interface Repository { + url: string; +} diff --git a/client/src/features/projectsV2/show/ProjectV2EditForm.tsx b/client/src/features/projectsV2/show/ProjectV2EditForm.tsx new file mode 100644 index 0000000000..8b8dd8b208 --- /dev/null +++ b/client/src/features/projectsV2/show/ProjectV2EditForm.tsx @@ -0,0 +1,398 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; +import { useCallback, useEffect, useState } from "react"; +import { CheckLg, XLg } from "react-bootstrap-icons"; +import { useFieldArray, useForm } from "react-hook-form"; + +import { + Button, + Form, + Input, + Modal, + ModalBody, + ModalFooter, + Table, +} from "reactstrap"; + +import { Loader } from "../../../components/Loader"; + +import { + useDeleteProjectsByProjectIdMutation, + useDeleteProjectsByProjectIdMembersAndMemberIdMutation, + useGetProjectsByProjectIdMembersQuery, + usePatchProjectsByProjectIdMutation, +} from "../api"; +import type { Member, Project, ProjectPatch } from "../api"; +import type { Repository } from "../projectV2.types"; + +import { + AddProjectMemberModal, + ProjectDescriptionFormField, + ProjectNameFormField, + ProjectRepositoryFormField, + ProjectVisibilityFormField, +} from "../edit"; + +import { SettingEditOption } from "./projectV2Show.types"; + +type ProjectV2Metadata = Omit; + +interface ProjectDeleteConfirmationProps { + isOpen: boolean; + toggle: () => void; + project: Project; +} + +function ProjectDeleteConfirmation({ + isOpen, + toggle, + project, +}: ProjectDeleteConfirmationProps) { + const [deleteProject, result] = useDeleteProjectsByProjectIdMutation(); + const onDelete = useCallback(() => { + deleteProject({ projectId: project.id }); + }, [deleteProject, project.id]); + const [typedName, setTypedName] = useState(""); + const onChange = useCallback( + (e: React.ChangeEvent) => { + setTypedName(e.target.value.trim()); + }, + [setTypedName] + ); + + useEffect(() => { + if (result.isSuccess || result.isError) { + toggle(); + } + }, [result.isError, result.isSuccess, toggle]); + + return ( + + +

+ Are you absolutely sure? +

+

+ Deleted projects cannot be restored. Please type{" "} + {project.slug}, the slug of the project, to confirm. +

+ +
+ + + + +
+ ); +} + +interface ProjectEditSubmitGroupProps { + isUpdating: boolean; + onCancel: () => void; +} +function ProjectEditSubmitGroup({ + isUpdating, + onCancel, +}: ProjectEditSubmitGroupProps) { + return ( +
+ +
+ +
+
+ ); +} + +interface ProjectV2MetadataFormProps { + project: Project; + setSettingEdit: (option: SettingEditOption) => void; +} +export function ProjectV2MetadataForm({ + project, + setSettingEdit, +}: ProjectV2MetadataFormProps) { + const { + control, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: { + description: project.description, + name: project.name, + visibility: project.visibility, + }, + }); + + const [updateProject, { isLoading, isError }] = + usePatchProjectsByProjectIdMutation(); + + const isUpdating = isLoading; + const onCancel = useCallback(() => { + setSettingEdit(null); + }, [setSettingEdit]); + + const onSubmit = useCallback( + (data: ProjectV2Metadata) => { + updateProject({ projectId: project.id, projectPatch: data }) + .unwrap() + .then(() => setSettingEdit(null)); + }, + [project, updateProject, setSettingEdit] + ); + + const [isOpen, setIsOpen] = useState(false); + const toggle = useCallback(() => { + setIsOpen((open) => !open); + }, []); + + return ( +
+
+ +
+ +
+ + + + + {isError &&
There was an error
} + +
+ ); +} + +export function ProjectV2MembersForm({ + project, + setSettingEdit, +}: ProjectV2MetadataFormProps) { + const { data, isLoading } = useGetProjectsByProjectIdMembersQuery({ + projectId: project.id, + }); + const [isAddMemberModalOpen, setIsAddMemberModalOpen] = useState(false); + const toggleAddMemberModalOpen = useCallback(() => { + setIsAddMemberModalOpen((open) => !open); + }, []); + + const [deleteMember] = + useDeleteProjectsByProjectIdMembersAndMemberIdMutation(); + const onCancel = useCallback(() => { + setSettingEdit(null); + }, [setSettingEdit]); + + const onDelete = useCallback( + (member: Member) => { + deleteMember({ projectId: project.id, memberId: member.id }); + }, + [deleteMember, project.id] + ); + + if (isLoading) return ; + if (data == null) + return ( + <> +

Project Members

+
Could not load members
+
+ +
+ + ); + return ( + <> +
+

Project Members

+
+ +
+
+ + + {data.map((d, i) => { + return ( + + + + + + ); + })} + +
{d.member.email ?? d.member.id}{d.role} + +
+ +
+ +
+ + ); +} + +type ProjectV2Repositories = { repositories: Repository[] }; + +export function ProjectV2RepositoryForm({ + project, + setSettingEdit, +}: ProjectV2MetadataFormProps) { + const { + control, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: { + repositories: project.repositories + ? project.repositories.map((s) => ({ + url: s, + })) + : [], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control, + name: "repositories", + }); + + const onAppend = useCallback(() => { + append({ url: "" }); + }, [append]); + const onDelete = useCallback( + (index: number) => { + remove(index); + }, + [remove] + ); + + const [updateProject, { isLoading, isError }] = + usePatchProjectsByProjectIdMutation(); + + const isUpdating = isLoading; + const onCancel = useCallback(() => { + setSettingEdit(null); + }, [setSettingEdit]); + + const onSubmit = useCallback( + (data: ProjectV2Repositories) => { + const repositories = data.repositories.map((r) => r.url); + updateProject({ projectId: project.id, projectPatch: { repositories } }) + .unwrap() + .then(() => setSettingEdit(null)); + }, + [project, updateProject, setSettingEdit] + ); + + return ( + <> +
+

Update repositories

+
+ +
+
+
+
+ {fields.map((f, i) => { + return ( +
+ onDelete(i)} + /> +
+ ); + })} +
+ + {isError &&
There was an error
} + + + ); +} diff --git a/client/src/features/projectsV2/show/ProjectV2Show.tsx b/client/src/features/projectsV2/show/ProjectV2Show.tsx new file mode 100644 index 0000000000..934d4a12e5 --- /dev/null +++ b/client/src/features/projectsV2/show/ProjectV2Show.tsx @@ -0,0 +1,210 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useCallback, useState } from "react"; +import { Link, useLocation } from "react-router-dom"; +import { + Dropdown, + DropdownItem, + DropdownMenu, + DropdownToggle, + Label, +} from "reactstrap"; + +import FormSchema from "../../../components/formschema/FormSchema"; +import { Loader } from "../../../components/Loader"; +import { TimeCaption } from "../../../components/TimeCaption"; +import { Url } from "../../../utils/helpers/url"; + +import { isErrorResponse, useGetProjectsByProjectIdQuery } from "../api"; +import type { Project } from "../api"; + +import { + ProjectV2MembersForm, + ProjectV2MetadataForm, + ProjectV2RepositoryForm, +} from "./ProjectV2EditForm"; +import { SettingEditOption } from "./projectV2Show.types"; + +interface ProjectV2HeaderProps { + project: Project; + setSettingEdit: (option: SettingEditOption) => void; + settingEdit: SettingEditOption; +} +function ProjectV2Header({ + project, + setSettingEdit, + settingEdit, +}: ProjectV2HeaderProps) { + return ( + <> +
{project.slug}
+
{project.visibility}
+ +
+ + + ); +} + +function ProjectV2HeaderEditButtonGroup({ + project, + setSettingEdit, + settingEdit, +}: ProjectV2HeaderProps) { + const canEdit = project.slug != null; + const [dropdownOpen, setDropdownOpen] = useState(false); + const toggle = useCallback( + () => setDropdownOpen((prev) => !prev), + [setDropdownOpen] + ); + const onSetMetadata = useCallback( + () => setSettingEdit("metadata"), + [setSettingEdit] + ); + const onSetMembers = useCallback( + () => setSettingEdit("members"), + [setSettingEdit] + ); + const onSetRepositories = useCallback( + () => setSettingEdit("repositories"), + [setSettingEdit] + ); + + if (!canEdit) return null; + + return ( + + + Edit Settings + + + Metadata + Members + Repositories + + + ); +} + +function ProjectV2Description({ description }: Pick) { + const desc = + description == null + ? "(no description)" + : description.length < 1 + ? "(no description)" + : description; + return
{desc}
; +} + +function ProjectV2Repositories({ + repositories, +}: Pick) { + if (repositories == null || repositories.length < 1) + return
(no repositories)
; + return ( +
+ {repositories?.map((repo, i) => ( +
{repo}
+ ))} +
+ ); +} + +interface ProjectV2DisplayProps { + project: Pick; +} +export function ProjectV2DescriptionAndRepositories({ + project, +}: ProjectV2DisplayProps) { + return ( + <> +
+ + +
+
+ + +
+ + ); +} + +export default function ProjectV2Show() { + const location = useLocation(); + const pathname = location.pathname; + const components = pathname.split("/"); + const projectId = components[components.length - 1]; + const { data, isLoading, error } = useGetProjectsByProjectIdQuery({ + projectId, + }); + + const [settingEdit, setSettingEdit] = useState(null); + + if (isLoading) return ; + if (error) { + if (isErrorResponse(error)) { + return ( +
+ Project does not exist, or you are not authorized to access it.{" "} + Return to list +
+ ); + } + return
Could not retrieve project
; + } + if (data == null) return
Could not retrieve project
; + + return ( + + } + > + {settingEdit == null && ( + + )} + {settingEdit == "members" && ( + + )} + {settingEdit == "metadata" && ( + + )} + {settingEdit == "repositories" && ( + + )} + + ); +} diff --git a/client/src/features/projectsV2/show/projectV2Show.types.ts b/client/src/features/projectsV2/show/projectV2Show.types.ts new file mode 100644 index 0000000000..3aebc863ac --- /dev/null +++ b/client/src/features/projectsV2/show/projectV2Show.types.ts @@ -0,0 +1,19 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type SettingEditOption = "metadata" | "members" | "repositories" | null; diff --git a/client/src/features/user/dataServicesUser.api/dataServicesUser-empty.api.ts b/client/src/features/user/dataServicesUser.api/dataServicesUser-empty.api.ts new file mode 100644 index 0000000000..c6a173f914 --- /dev/null +++ b/client/src/features/user/dataServicesUser.api/dataServicesUser-empty.api.ts @@ -0,0 +1,26 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; + +// initialize an empty api service that we'll inject endpoints into later as needed +export const dataServicesUserEmptyApi = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: "/ui-server/api/data" }), + endpoints: () => ({}), + reducerPath: "dataServicesUserApi", +}); diff --git a/client/src/features/user/dataServicesUser.api/dataServicesUser.api-config.ts b/client/src/features/user/dataServicesUser.api/dataServicesUser.api-config.ts new file mode 100644 index 0000000000..2c58024d05 --- /dev/null +++ b/client/src/features/user/dataServicesUser.api/dataServicesUser.api-config.ts @@ -0,0 +1,32 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Run `npx @rtk-query/codegen-openapi dataServicesUser.api-config.ts` to generate the API +import type { ConfigFile } from "@rtk-query/codegen-openapi"; +import path from "path"; + +const config: ConfigFile = { + apiFile: "./dataServicesUser-empty.api.ts", + apiImport: "dataServicesUserEmptyApi", + outputFile: "./dataServicesUser.api.ts", + exportName: "dataServicesUserApi", + hooks: true, + schemaFile: path.join(__dirname, "dataServicesUser.openapi.json"), +}; + +export default config; diff --git a/client/src/features/user/dataServicesUser.api/dataServicesUser.api.ts b/client/src/features/user/dataServicesUser.api/dataServicesUser.api.ts new file mode 100644 index 0000000000..97c9fbfc35 --- /dev/null +++ b/client/src/features/user/dataServicesUser.api/dataServicesUser.api.ts @@ -0,0 +1,73 @@ +import { dataServicesUserEmptyApi as api } from "./dataServicesUser-empty.api"; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + getUser: build.query({ + query: () => ({ url: `/user` }), + }), + getUsers: build.query({ + query: (queryArg) => ({ + url: `/users`, + params: { exact_email: queryArg.exactEmail }, + }), + }), + getUsersByUserId: build.query< + GetUsersByUserIdApiResponse, + GetUsersByUserIdApiArg + >({ + query: (queryArg) => ({ url: `/users/${queryArg.userId}` }), + }), + getError: build.query({ + query: () => ({ url: `/error` }), + }), + getVersion: build.query({ + query: () => ({ url: `/version` }), + }), + }), + overrideExisting: false, +}); +export { injectedRtkApi as dataServicesUserApi }; +export type GetUserApiResponse = + /** status 200 The currently logged in user */ UserWithId; +export type GetUserApiArg = void; +export type GetUsersApiResponse = + /** status 200 The list of users in the service (this is a subset of what is in Keycloak) */ UsersWithId; +export type GetUsersApiArg = { + /** Return the user(s) with an exact match on the email provided */ + exactEmail?: string; +}; +export type GetUsersByUserIdApiResponse = + /** status 200 The requested user */ UserWithId; +export type GetUsersByUserIdApiArg = { + userId: string; +}; +export type GetErrorApiResponse = unknown; +export type GetErrorApiArg = void; +export type GetVersionApiResponse = /** status 200 The error */ Version; +export type GetVersionApiArg = void; +export type UserId = string; +export type UserEmail = string; +export type UserFirstLastName = string; +export type UserWithId = { + id: UserId; + email?: UserEmail; + first_name?: UserFirstLastName; + last_name?: UserFirstLastName; +}; +export type UsersWithId = UserWithId[]; +export type ErrorResponse = { + error: { + code: number; + detail?: string; + message: string; + }; +}; +export type Version = { + version: string; +}; +export const { + useGetUserQuery, + useGetUsersQuery, + useGetUsersByUserIdQuery, + useGetErrorQuery, + useGetVersionQuery, +} = injectedRtkApi; diff --git a/client/src/features/user/dataServicesUser.api/dataServicesUser.openapi.json b/client/src/features/user/dataServicesUser.api/dataServicesUser.openapi.json new file mode 100644 index 0000000000..007e6b2811 --- /dev/null +++ b/client/src/features/user/dataServicesUser.api/dataServicesUser.openapi.json @@ -0,0 +1,237 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Renku data services.", + "description": "Endpoints that provide different information from different backends.\n", + "version": "v1" + }, + "servers": [ + { + "url": "/api/data" + }, + { + "url": "/ui-server/api/data" + } + ], + "paths": { + "/user": { + "get": { + "summary": "Get information about the currently logged in user", + "responses": { + "200": { + "description": "The currently logged in user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithId" + } + } + } + } + }, + "tags": ["users"] + } + }, + "/users": { + "get": { + "summary": "List all users", + "parameters": [ + { + "in": "query", + "name": "exact_email", + "schema": { + "type": "string" + }, + "required": false, + "description": "Return the user(s) with an exact match on the email provided" + } + ], + "responses": { + "200": { + "description": "The list of users in the service (this is a subset of what is in Keycloak)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UsersWithId" + } + } + } + } + }, + "tags": ["users"] + } + }, + "/users/{user_id}": { + "get": { + "summary": "Get a specific user by their Keycloak ID", + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The requested user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWithId" + } + } + } + } + }, + "tags": ["users"] + } + }, + "/error": { + "get": { + "summary": "Get a sample error response with status code 422", + "responses": { + "422": { + "description": "The error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/version": { + "get": { + "summary": "Get the version of the service", + "responses": { + "200": { + "description": "The error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Version" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "UserWithId": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/components/schemas/UserId" + }, + "email": { + "$ref": "#/components/schemas/UserEmail" + }, + "first_name": { + "$ref": "#/components/schemas/UserFirstLastName" + }, + "last_name": { + "$ref": "#/components/schemas/UserFirstLastName" + } + }, + "required": ["id"], + "example": { + "id": "some-random-keycloak-id", + "email": "user@gmail.com" + } + }, + "UsersWithId": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserWithId" + }, + "uniqueItems": true + }, + "Version": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"] + }, + "UserId": { + "type": "string", + "description": "Keycloak user ID", + "example": "f74a228b-1790-4276-af5f-25c2424e9b0c" + }, + "UserFirstLastName": { + "type": "string", + "description": "First or last name of the user", + "example": "John", + "minLength": 1, + "maxLength": 256 + }, + "UserEmail": { + "type": "string", + "format": "email", + "description": "User email", + "example": "some-user@gmail.com" + }, + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "example": 1404 + }, + "detail": { + "type": "string", + "example": "A more detailed optional message showing what the problem was" + }, + "message": { + "type": "string", + "example": "Something went wrong - please try again later" + } + }, + "required": ["code", "message"] + } + }, + "required": ["error"] + } + }, + "responses": { + "Error": { + "description": "The schema for all 4xx and 5xx responses", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "securitySchemes": { + "oidc": { + "type": "openIdConnect", + "openIdConnectUrl": "/auth/realms/Renku/.well-known/openid-configuration" + } + } + }, + "security": [ + { + "oidc": ["openid"] + } + ] +} diff --git a/client/src/features/user/dataServicesUser.api/index.ts b/client/src/features/user/dataServicesUser.api/index.ts new file mode 100644 index 0000000000..4690795f1b --- /dev/null +++ b/client/src/features/user/dataServicesUser.api/index.ts @@ -0,0 +1,10 @@ +export { dataServicesUserApi } from "./dataServicesUser.api"; + +export type { UserWithId } from "./dataServicesUser.api"; + +export { + useGetUserQuery, + useGetUsersQuery, + useGetUsersByUserIdQuery, + useGetErrorQuery, +} from "./dataServicesUser.api"; diff --git a/client/src/utils/helpers/EnhancedState.ts b/client/src/utils/helpers/EnhancedState.ts index 243724e68a..5aa181de0e 100644 --- a/client/src/utils/helpers/EnhancedState.ts +++ b/client/src/utils/helpers/EnhancedState.ts @@ -25,7 +25,7 @@ import { AnyAction, ReducersMapObject, StoreEnhancer, - configureStore, + configureStore } from "@reduxjs/toolkit"; import adminComputeResourcesApi from "../../features/admin/adminComputeResources.api"; @@ -43,12 +43,14 @@ import { projectCoreApi } from "../../features/project/projectCoreApi"; import projectGitLabApi from "../../features/project/projectGitLab.api"; import { projectKgApi } from "../../features/project/projectKg.api"; import { projectsApi } from "../../features/projects/projects.api"; +import { projectV2Api, projectV2NewSlice } from "../../features/projectsV2/"; import { recentUserActivityApi } from "../../features/recentUserActivity/RecentUserActivityApi"; import sessionsApi from "../../features/session/sessions.api"; import { sessionSidecarApi } from "../../features/session/sidecarApi"; import startSessionSlice from "../../features/session/startSession.slice"; import { startSessionOptionsSlice } from "../../features/session/startSessionOptionsSlice"; import termsApi from "../../features/terms/terms.api"; +import { dataServicesUserApi } from "../../features/user/dataServicesUser.api"; import keycloakUserApi from "../../features/user/keycloakUser.api"; import userPreferencesApi from "../../features/user/userPreferences.api"; import { versionsApi } from "../../features/versions/versions.api"; @@ -69,11 +71,13 @@ export const createStore = ( [kgInactiveProjectsSlice.name]: kgInactiveProjectsSlice.reducer, [startSessionSlice.name]: startSessionSlice.reducer, [startSessionOptionsSlice.name]: startSessionOptionsSlice.reducer, + [projectV2NewSlice.name]: projectV2NewSlice.reducer, [workflowsSlice.name]: workflowsSlice.reducer, // APIs [adminComputeResourcesApi.reducerPath]: adminComputeResourcesApi.reducer, [adminKeycloakApi.reducerPath]: adminKeycloakApi.reducer, [dataServicesApi.reducerPath]: dataServicesApi.reducer, + [dataServicesUserApi.reducerPath]: dataServicesUserApi.reducer, [datasetsCoreApi.reducerPath]: datasetsCoreApi.reducer, [inactiveKgProjectsApi.reducerPath]: inactiveKgProjectsApi.reducer, [keycloakUserApi.reducerPath]: keycloakUserApi.reducer, @@ -83,13 +87,14 @@ export const createStore = ( [projectGitLabApi.reducerPath]: projectGitLabApi.reducer, [projectKgApi.reducerPath]: projectKgApi.reducer, [projectsApi.reducerPath]: projectsApi.reducer, + [projectV2Api.reducerPath]: projectV2Api.reducer, [recentUserActivityApi.reducerPath]: recentUserActivityApi.reducer, [sessionsApi.reducerPath]: sessionsApi.reducer, [sessionSidecarApi.reducerPath]: sessionSidecarApi.reducer, [termsApi.reducerPath]: termsApi.reducer, [userPreferencesApi.reducerPath]: userPreferencesApi.reducer, [versionsApi.reducerPath]: versionsApi.reducer, - [workflowsApi.reducerPath]: workflowsApi.reducer, + [workflowsApi.reducerPath]: workflowsApi.reducer }; // For the moment, disable the custom middleware, since it causes problems for our app. @@ -98,11 +103,13 @@ export const createStore = ( middleware: (getDefaultMiddleware) => getDefaultMiddleware({ immutableCheck: false, - serializableCheck: false, + serializableCheck: false }) .concat(adminComputeResourcesApi.middleware) .concat(adminKeycloakApi.middleware) .concat(dataServicesApi.middleware) + // this is causing some problems, and I do not know why + .concat(dataServicesUserApi.middleware) .concat(datasetsCoreApi.middleware) .concat(inactiveKgProjectsApi.middleware) .concat(keycloakUserApi.middleware) @@ -112,6 +119,7 @@ export const createStore = ( .concat(projectGitLabApi.middleware) .concat(projectKgApi.middleware) .concat(projectsApi.middleware) + .concat(projectV2Api.middleware) .concat(recentUserActivityApi.middleware) .concat(sessionSidecarApi.middleware) .concat(sessionsApi.middleware) @@ -120,7 +128,7 @@ export const createStore = ( .concat(userPreferencesApi.middleware) .concat(versionsApi.middleware) .concat(workflowsApi.middleware), - enhancers, + enhancers }); return store; }; diff --git a/client/src/utils/helpers/url/Url.js b/client/src/utils/helpers/url/Url.js index e9e6f26fa8..55f7492d9f 100644 --- a/client/src/utils/helpers/url/Url.js +++ b/client/src/utils/helpers/url/Url.js @@ -484,6 +484,14 @@ const Url = { ), }, }, + projectsV2: { + base: "/projectsV2", + new: "/projectsV2/new", + list: "/projectsV2", + show: new UrlRule((data) => `/projectsV2/${data.id}`, ["id"], null, [ + "/projectsV2/id", + ]), + }, sessions: { base: "/sessions", }, diff --git a/tests/cypress/e2e/projectV2.spec.ts b/tests/cypress/e2e/projectV2.spec.ts new file mode 100644 index 0000000000..6931b04e56 --- /dev/null +++ b/tests/cypress/e2e/projectV2.spec.ts @@ -0,0 +1,264 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fixtures from "../support/renkulab-fixtures"; + +describe("Add new v2 project", () => { + const newProjectTitle = "new project"; + const slug = "new-project"; + + beforeEach(() => { + fixtures.config().versions().userTest().namespaces(); + fixtures.projects().landingUserProjects(); + fixtures.createProjectV2().readProjectV2(); + cy.visit("projectsV2/new"); + }); + + it("create a new project", () => { + cy.contains("New Project (V2)").should("be.visible"); + cy.getDataCy("project-name-input").clear().type(newProjectTitle); + cy.getDataCy("project-slug-input").should("have.value", slug); + cy.contains("Set Visibility").click(); + cy.contains("Add repositories").click(); + cy.getDataCy("project-add-repository").click(); + cy.getDataCy("project-repository-input-0") + .clear() + .type("https://domain.name/repo1.git"); + cy.contains("Review").click(); + cy.contains("Create").click(); + + cy.contains("Creating project...").should("be.visible"); + cy.wait("@createProjectV2"); + cy.contains("Project created").should("be.visible"); + }); + + it("prevents invalid input", () => { + cy.contains("Set Visibility").click(); + cy.contains("Please provide a name").should("be.visible"); + cy.getDataCy("project-name-input").clear().type(newProjectTitle); + cy.getDataCy("project-slug-input").clear().type(newProjectTitle); + cy.contains("Set Visibility").click(); + cy.contains( + "Please provide a slug consisting of lowercase letters, numbers, and hyphens." + ).should("be.visible"); + cy.getDataCy("project-slug-input").clear().type(slug); + cy.contains("Set Visibility").click(); + + cy.contains("Define access").should("be.visible"); + cy.getDataCy("project-visibility").select("Public"); + cy.contains("Add repositories").click(); + + cy.contains("Review").click(); + cy.contains("Back").click(); + cy.getDataCy("project-add-repository").click(); + cy.contains("Review").click(); + cy.contains("Please provide a valid URL or remove the repository").should( + "be.visible" + ); + cy.getDataCy("project-repository-input-0") + .clear() + .type("https://domain.name/repo1.git"); + + cy.contains("Review").click(); + cy.contains(newProjectTitle).should("be.visible"); + cy.contains(slug).should("be.visible"); + cy.contains("public").should("be.visible"); + cy.contains("https://domain.name/repo1.git").should("be.visible"); + + cy.contains("Create").click(); + + cy.contains("Creating project...").should("be.visible"); + cy.wait("@createProjectV2"); + cy.contains("Project created").should("be.visible"); + }); +}); + +describe("Add new v2 project -- not logged in", () => { + beforeEach(() => { + fixtures.config().versions().userNone(); + cy.visit("projectsV2/new"); + }); + + it("create a new project", () => { + cy.contains("Please log in to create a project").should("be.visible"); + }); +}); + +describe("List v2 project", () => { + beforeEach(() => { + fixtures.config().versions().userTest().namespaces(); + fixtures.projects().landingUserProjects().listProjectV2(); + cy.visit("projectsV2"); + }); + + it("list projects", () => { + cy.contains("List Projects (V2)").should("be.visible"); + }); + + it("list projects with pagination", () => { + fixtures.listManyProjectV2(); + cy.wait("@listProjectV2"); + cy.contains("List Projects (V2)").should("be.visible"); + cy.get("ul.rk-search-pagination").should("be.visible"); + }); + + it("shows projects", () => { + fixtures.readProjectV2(); + cy.contains("List Projects (V2)").should("be.visible"); + cy.contains("test 2 v2-project").should("be.visible").click(); + cy.wait("@readProjectV2"); + cy.contains("test 2 v2-project").should("be.visible"); + }); +}); + +describe("Edit v2 project", () => { + beforeEach(() => { + fixtures.config().versions().userTest().namespaces(); + fixtures.projects().landingUserProjects().listProjectV2(); + cy.visit("projectsV2"); + }); + + it("changes project metadata", () => { + fixtures.readProjectV2().updateProjectV2(); + cy.contains("List Projects (V2)").should("be.visible"); + cy.contains("test 2 v2-project").should("be.visible").click(); + cy.wait("@readProjectV2"); + cy.contains("test 2 v2-project").should("be.visible"); + cy.contains("Edit Settings").should("be.visible").click(); + cy.get("button").contains("Metadata").should("be.visible").click(); + cy.getDataCy("project-name-input").clear().type("new name"); + cy.getDataCy("project-description-input").clear().type("new description"); + fixtures.readProjectV2({ + fixture: "projectV2/update-projectV2-metadata.json", + name: "readPostUpdate", + }); + cy.get("button").contains("Update").should("be.visible").click(); + cy.wait("@updateProjectV2"); + cy.wait("@readPostUpdate"); + cy.contains("new name").should("be.visible"); + }); + + it("changes project repositories", () => { + fixtures.readProjectV2().updateProjectV2({ + fixture: "projectV2/update-projectV2-repositories.json", + }); + cy.contains("List Projects (V2)").should("be.visible"); + cy.contains("test 2 v2-project").should("be.visible").click(); + cy.wait("@readProjectV2"); + cy.contains("test 2 v2-project").should("be.visible"); + cy.contains("Edit Settings").should("be.visible").click(); + cy.get("button").contains("Repositories").should("be.visible").click(); + cy.getDataCy("project-add-repository").click(); + cy.getDataCy("project-repository-input-2") + .clear() + .type("https://domain.name/repo3.git"); + fixtures.readProjectV2({ + fixture: "projectV2/update-projectV2-repositories.json", + name: "readPostUpdate", + }); + cy.get("button").contains("Update").should("be.visible").click(); + cy.wait("@updateProjectV2"); + cy.wait("@readPostUpdate"); + cy.contains("https://domain.name/repo3.git").should("be.visible"); + }); + + it("changes project members", () => { + const projectMemberToRemove = "user3-uuid"; + fixtures + .deleteProjectV2Member({ memberId: projectMemberToRemove }) + .exactUser({ + name: "getExactUserSuccess", + exactEmailQueryString: "foo%40bar.com", + response: [ + { + id: "user-id", + email: "foo@bar.com", + first_name: "Foo", + last_name: "Bar", + }, + ], + }) + .exactUser({ + name: "getExactUserFail", + exactEmailQueryString: "noone%40bar.com", + response: [], + }) + .listProjectV2Members() + .readProjectV2(); + cy.contains("List Projects (V2)").should("be.visible"); + cy.contains("test 2 v2-project").should("be.visible").click(); + cy.wait("@readProjectV2"); + cy.contains("test 2 v2-project").should("be.visible"); + cy.contains("Edit Settings").should("be.visible").click(); + cy.get("button").contains("Members").should("be.visible").click(); + cy.contains("user1@email.com").should("be.visible"); + cy.contains("user3-uuid").should("be.visible"); + fixtures + .deleteProjectV2Member({ memberId: projectMemberToRemove }) + .listProjectV2Members({ removeMemberId: projectMemberToRemove }); + cy.getDataCy("delete-member-2").should("be.visible").click(); + cy.contains("user3-uuid").should("not.exist"); + cy.contains("Add").should("be.visible").click(); + cy.getDataCy("add-project-member-email").clear().type("foo@bar.com"); + cy.contains("Lookup").should("be.visible").click(); + cy.wait("@getExactUserSuccess"); + fixtures.patchProjectV2Member().listProjectV2Members({ + addMember: { + member: { id: "foo-id", email: "foo@bar.com" }, + role: "member", + }, + removeMemberId: projectMemberToRemove, + }); + cy.get("button").contains("Add Member").should("be.visible").click(); + cy.contains("foo@bar.com").should("be.visible"); + + cy.contains("Add").should("be.visible").click(); + cy.getDataCy("add-project-member-email").clear().type("noone@bar.com"); + cy.contains("Lookup").should("be.visible").click(); + cy.wait("@getExactUserFail"); + cy.contains("No user found for noone@bar.com").should("be.visible"); + cy.getDataCy("user-lookup-close-button").should("be.visible").click(); + }); + + it("deletes project", () => { + fixtures.readProjectV2().deleteProjectV2(); + cy.contains("List Projects (V2)").should("be.visible"); + cy.contains("test 2 v2-project").should("be.visible").click(); + cy.wait("@readProjectV2"); + cy.contains("test 2 v2-project").should("be.visible"); + cy.contains("Edit Settings").should("be.visible").click(); + cy.get("button").contains("Metadata").should("be.visible").click(); + cy.get("button").contains("Delete").should("be.visible").click(); + cy.get("button") + .contains("Yes, delete") + .should("be.visible") + .should("be.disabled"); + cy.contains("Please type test-2-v2-project").should("be.visible"); + cy.getDataCy("delete-confirmation-input").clear().type("test-2-v2-project"); + fixtures.postDeleteReadProjectV2(); + cy.get("button").contains("Yes, delete").should("be.enabled").click(); + cy.wait("@deleteProjectV2"); + cy.wait("@postDeleteReadProjectV2"); + + fixtures.listProjectV2({ + fixture: "projectV2/list-projectV2-post-delete.json", + name: "listProjectV2PostDelete", + }); + cy.contains("Return to list").click(); + }); +}); diff --git a/tests/cypress/fixtures/projectV2/create-projectV2.json b/tests/cypress/fixtures/projectV2/create-projectV2.json new file mode 100644 index 0000000000..6941d4c03f --- /dev/null +++ b/tests/cypress/fixtures/projectV2/create-projectV2.json @@ -0,0 +1,9 @@ +{ + "id": "THEPROJECTULID26CHARACTERS", + "name": "Renku R Project", + "slug": "r-project", + "created_by": { + "id": "owner-KC-id" + }, + "visibility": "public" +} diff --git a/tests/cypress/fixtures/projectV2/list-projectV2-members.json b/tests/cypress/fixtures/projectV2/list-projectV2-members.json new file mode 100644 index 0000000000..2bc15a883a --- /dev/null +++ b/tests/cypress/fixtures/projectV2/list-projectV2-members.json @@ -0,0 +1,14 @@ +[ + { + "member": { "id": "user1-uuid", "email": "user1@email.com" }, + "role": "owner" + }, + { + "member": { "id": "user2-uuid", "email": "user2@email.com" }, + "role": "member" + }, + { + "member": { "id": "user3-uuid" }, + "role": "member" + } +] diff --git a/tests/cypress/fixtures/projectV2/list-projectV2-post-delete.json b/tests/cypress/fixtures/projectV2/list-projectV2-post-delete.json new file mode 100644 index 0000000000..f2bb992ccf --- /dev/null +++ b/tests/cypress/fixtures/projectV2/list-projectV2-post-delete.json @@ -0,0 +1,12 @@ +[ + { + "id": "01HF96BXZ3JF9DX88B7XB405S5", + "name": "test 1 v2-project", + "slug": "test-1-v2-project", + "creation_date": "2023-11-15T09:52:59Z", + "created_by": { "id": "user1-uuid" }, + "repositories": [], + "visibility": "private", + "description": "Project 1 description" + } +] diff --git a/tests/cypress/fixtures/projectV2/list-projectV2.json b/tests/cypress/fixtures/projectV2/list-projectV2.json new file mode 100644 index 0000000000..a15eb04f86 --- /dev/null +++ b/tests/cypress/fixtures/projectV2/list-projectV2.json @@ -0,0 +1,25 @@ +[ + { + "id": "THEPROJECTULID26CHARACTERS", + "name": "test 2 v2-project", + "slug": "test-2-v2-project", + "creation_date": "2023-11-15T09:55:59Z", + "created_by": { "id": "user1-uuid" }, + "repositories": [ + "https://domain.name/repo1.git", + "https://domain.name/repo2.git" + ], + "visibility": "public", + "description": "Project 2 description" + }, + { + "id": "01HF96BXZ3JF9DX88B7XB405S5", + "name": "test 1 v2-project", + "slug": "test-1-v2-project", + "creation_date": "2023-11-15T09:52:59Z", + "created_by": { "id": "user1-uuid" }, + "repositories": [], + "visibility": "private", + "description": "Project 1 description" + } +] diff --git a/tests/cypress/fixtures/projectV2/read-projectV2.json b/tests/cypress/fixtures/projectV2/read-projectV2.json new file mode 100644 index 0000000000..127af63b65 --- /dev/null +++ b/tests/cypress/fixtures/projectV2/read-projectV2.json @@ -0,0 +1,13 @@ +{ + "id": "THEPROJECTULID26CHARACTERS", + "name": "test 2 v2-project", + "slug": "test-2-v2-project", + "creation_date": "2023-11-15T09:55:59Z", + "created_by": { "id": "user1-uuid" }, + "repositories": [ + "https://domain.name/repo1.git", + "https://domain.name/repo2.git" + ], + "visibility": "public", + "description": "Project 2 description" +} diff --git a/tests/cypress/fixtures/projectV2/update-projectV2-metadata.json b/tests/cypress/fixtures/projectV2/update-projectV2-metadata.json new file mode 100644 index 0000000000..9614c58a0f --- /dev/null +++ b/tests/cypress/fixtures/projectV2/update-projectV2-metadata.json @@ -0,0 +1,13 @@ +{ + "id": "THEPROJECTULID26CHARACTERS", + "name": "new name", + "slug": "new-slug", + "creation_date": "2023-11-15T09:55:59Z", + "created_by": { "id": "user1-uuid" }, + "repositories": [ + "https://domain.name/repo1.git", + "https://domain.name/repo2.git" + ], + "visibility": "public", + "description": "new description" +} diff --git a/tests/cypress/fixtures/projectV2/update-projectV2-repositories.json b/tests/cypress/fixtures/projectV2/update-projectV2-repositories.json new file mode 100644 index 0000000000..a50fd5045e --- /dev/null +++ b/tests/cypress/fixtures/projectV2/update-projectV2-repositories.json @@ -0,0 +1,14 @@ +{ + "id": "THEPROJECTULID26CHARACTERS", + "name": "test 2 v2-project", + "slug": "test-2-v2-project", + "creation_date": "2023-11-15T09:55:59Z", + "created_by": { "id": "user1-uuid" }, + "repositories": [ + "https://domain.name/repo1.git", + "https://domain.name/repo2.git", + "https://domain.name/repo3.git" + ], + "visibility": "public", + "description": "Project 2 description" +} diff --git a/tests/cypress/support/renkulab-fixtures/dataServices.ts b/tests/cypress/support/renkulab-fixtures/dataServices.ts index 27a119230e..63a16c5e0b 100644 --- a/tests/cypress/support/renkulab-fixtures/dataServices.ts +++ b/tests/cypress/support/renkulab-fixtures/dataServices.ts @@ -17,7 +17,18 @@ */ import { FixturesConstructor } from "./fixtures"; -import { SimpleFixture } from "./fixtures.types"; +import { NameOnlyFixture, SimpleFixture } from "./fixtures.types"; + +interface ExactUser { + id: string; + email: string; + first_name: string; + last_name: string; +} +interface ExactUserFixture extends NameOnlyFixture { + exactEmailQueryString: string; + response: ExactUser[]; +} /** * Fixtures for Data Services @@ -49,5 +60,17 @@ export function DataServices(Parent: T) { }); return this; } + + exactUser(args: ExactUserFixture) { + const { name = "getExactUser", exactEmailQueryString, response } = args; + cy.intercept( + "GET", + `/ui-server/api/data/users?exact_email=${exactEmailQueryString}`, + { + body: response, + } + ).as(name); + return this; + } }; } diff --git a/tests/cypress/support/renkulab-fixtures/index.ts b/tests/cypress/support/renkulab-fixtures/index.ts index d4b4d25c6e..58491f857c 100644 --- a/tests/cypress/support/renkulab-fixtures/index.ts +++ b/tests/cypress/support/renkulab-fixtures/index.ts @@ -30,6 +30,7 @@ import { KgSearch } from "./kgSearch"; import { NewProject } from "./newProject"; import { NewSession } from "./newSession"; import { Projects } from "./projects"; +import { ProjectV2 } from "./projectV2"; import { Sessions } from "./sessions"; import { Terms } from "./terms"; import { User } from "./user"; @@ -46,10 +47,12 @@ const Fixtures = NewProject( CloudStorage( Datasets( Projects( - Terms( - User( - UserPreferences( - Versions(Workflows(KgSearch(Global(BaseFixtures)))) + ProjectV2( + Terms( + User( + UserPreferences( + Versions(Workflows(KgSearch(Global(BaseFixtures)))) + ) ) ) ) diff --git a/tests/cypress/support/renkulab-fixtures/projectV2.ts b/tests/cypress/support/renkulab-fixtures/projectV2.ts new file mode 100644 index 0000000000..28d1f508b8 --- /dev/null +++ b/tests/cypress/support/renkulab-fixtures/projectV2.ts @@ -0,0 +1,235 @@ +/*! + * Copyright 2023 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FixturesConstructor } from "./fixtures"; +import { NameOnlyFixture, SimpleFixture } from "./fixtures.types"; + +/** + * Fixtures for New Project + */ + +interface ListManyProjectArgs extends NameOnlyFixture { + numberOfProjects?: number; +} + +interface ListProjectV2MembersFixture extends ProjectV2Args { + removeMemberId?: string; + addMember?: { member: { id: string; email: string }; role: string }; +} + +interface ProjectV2Args extends SimpleFixture { + projectId?: string; +} + +interface ProjectV2DeleteFixture extends NameOnlyFixture { + projectId?: string; +} + +interface ProjectV2DeleteMemberFixture extends ProjectV2Args { + memberId?: string; +} + +function generateProjects(numberOfProjects: number, start: number) { + const projects = []; + for (let i = 0; i < numberOfProjects; ++i) { + const id = start + i; + const project = { + id: `${id}`, + name: `test ${id} v2-project`, + slug: `test-${id}-v2-project`, + creation_date: "2023-11-15T09:55:59Z", + created_by: { id: "user1-uuid" }, + repositories: [ + "https://domain.name/repo1.git", + "https://domain.name/repo2.git", + ], + visibility: "public", + description: `Project ${id} description`, + }; + projects.push(project); + } + return projects; +} + +export function ProjectV2(Parent: T) { + return class ProjectV2Fixtures extends Parent { + createProjectV2(args?: SimpleFixture) { + const { + fixture = "projectV2/create-projectV2.json", + name = "createProjectV2", + } = args ?? {}; + const response = { fixture, delay: 2000, statusCode: 201 }; + cy.intercept("POST", "/ui-server/api/data/projects", response).as(name); + return this; + } + + deleteProjectV2(args?: ProjectV2DeleteFixture) { + const { + name = "deleteProjectV2", + projectId = "THEPROJECTULID26CHARACTERS", + } = args ?? {}; + const response = { delay: 2000, statusCode: 204 }; + cy.intercept( + "DELETE", + `/ui-server/api/data/projects/${projectId}`, + response + ).as(name); + return this; + } + + deleteProjectV2Member(args?: ProjectV2DeleteMemberFixture) { + const { + fixture = "projectV2/list-projectV2-members.json", + name = "deleteProjectV2Members", + projectId = "THEPROJECTULID26CHARACTERS", + memberId = "user3-uuid", + } = args ?? {}; + const response = { fixture }; + cy.intercept( + "DELETE", + `/ui-server/api/data/projects/${projectId}/members/${memberId}`, + response + ).as(name); + return this; + } + + listManyProjectV2(args?: ListManyProjectArgs) { + const { numberOfProjects = 50, name = "listProjectV2" } = args ?? {}; + cy.intercept("GET", `/ui-server/api/data/projects?*`, (req) => { + const page = (req.query["perPage"] as number) ?? 1; + const perPage = (req.query["perPage"] as number) ?? 20; + const start = (page - 1) * perPage; + const numToGen = Math.min( + Math.max(numberOfProjects - start - perPage, 0), + perPage + ); + req.reply({ + body: generateProjects(numToGen, start), + headers: { + page: page.toString(), + "per-page": perPage.toString(), + total: numberOfProjects.toString(), + "total-pages": Math.ceil(numberOfProjects / perPage).toString(), + }, + }); + }).as(name); + return this; + } + + listProjectV2(args?: SimpleFixture) { + const { + fixture = "projectV2/list-projectV2.json", + name = "listProjectV2", + } = args ?? {}; + const response = { fixture, delay: 2000 }; + cy.intercept("GET", `/ui-server/api/data/projects?*`, response).as(name); + return this; + } + + listProjectV2Members(args?: ListProjectV2MembersFixture) { + const { + fixture = "projectV2/list-projectV2-members.json", + name = "listProjectV2Members", + projectId = "THEPROJECTULID26CHARACTERS", + removeMemberId = null, + addMember = null, + } = args ?? {}; + cy.fixture(fixture).then((content) => { + const result = content.filter( + (memberWithRole) => memberWithRole.member.id !== removeMemberId + ); + if (addMember != null) result.push(addMember); + const response = { body: result }; + cy.intercept( + "GET", + `/ui-server/api/data/projects/${projectId}/members`, + response + ).as(name); + }); + return this; + } + + patchProjectV2Member(args?: ProjectV2DeleteMemberFixture) { + const { + fixture = "projectV2/list-projectV2-members.json", + name = "patchProjectV2Members", + projectId = "THEPROJECTULID26CHARACTERS", + } = args ?? {}; + const response = { fixture }; + cy.intercept( + "PATCH", + `/ui-server/api/data/projects/${projectId}/members`, + response + ).as(name); + return this; + } + + postDeleteReadProjectV2(args?: ProjectV2DeleteFixture) { + const { + name = "postDeleteReadProjectV2", + projectId = "THEPROJECTULID26CHARACTERS", + } = args ?? {}; + const response = { + body: { + error: { + code: 1404, + message: `Project with id ${projectId} does not exist.`, + }, + }, + delay: 2000, + statusCode: 404, + }; + cy.intercept( + "GET", + `/ui-server/api/data/projects/${projectId}`, + response + ).as(name); + return this; + } + + readProjectV2(args?: ProjectV2Args) { + const { + fixture = "projectV2/read-projectV2.json", + name = "readProjectV2", + projectId = "THEPROJECTULID26CHARACTERS", + } = args ?? {}; + const response = { fixture }; + cy.intercept( + "GET", + `/ui-server/api/data/projects/${projectId}`, + response + ).as(name); + return this; + } + + updateProjectV2(args?: ProjectV2Args) { + const { + fixture = "projectV2/update-projectV2-metadata.json", + name = "updateProjectV2", + projectId = "THEPROJECTULID26CHARACTERS", + } = args ?? {}; + const response = { fixture, delay: 2000 }; + cy.intercept( + "PATCH", + `/ui-server/api/data/projects/${projectId}`, + response + ).as(name); + return this; + } + }; +} From cb39e2de221f7b3ba3bd5f6ad91947546f02adac Mon Sep 17 00:00:00 2001 From: Chandrasekhar Ramakrishnan Date: Thu, 18 Jan 2024 16:02:56 +0100 Subject: [PATCH 02/15] added Alpha badge --- .../projectsV2/list/ProjectV2List.tsx | 7 +++- .../features/projectsV2/new/ProjectV2New.tsx | 7 ++-- .../features/projectsV2/shared/WipBadge.tsx | 38 +++++++++++++++++++ .../projectsV2/show/ProjectV2Show.tsx | 4 +- 4 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 client/src/features/projectsV2/shared/WipBadge.tsx diff --git a/client/src/features/projectsV2/list/ProjectV2List.tsx b/client/src/features/projectsV2/list/ProjectV2List.tsx index b524cfdeb6..832a97a7e2 100644 --- a/client/src/features/projectsV2/list/ProjectV2List.tsx +++ b/client/src/features/projectsV2/list/ProjectV2List.tsx @@ -27,6 +27,7 @@ import { Url } from "../../../utils/helpers/url"; import { useGetProjectsQuery } from "../api"; import type { Project } from "../api"; +import WipBadge from "../shared/WipBadge"; import styles from "./projectV2List.module.scss"; @@ -100,7 +101,11 @@ export default function ProjectV2List() { All visible projects} + description={ +
+ All visible projects {" "} +
+ } >
diff --git a/client/src/features/projectsV2/new/ProjectV2New.tsx b/client/src/features/projectsV2/new/ProjectV2New.tsx index 25e30d7b57..5f9f7cf549 100644 --- a/client/src/features/projectsV2/new/ProjectV2New.tsx +++ b/client/src/features/projectsV2/new/ProjectV2New.tsx @@ -34,6 +34,7 @@ import type { NewProjectV2State } from "./projectV2New.slice"; import { setCurrentStep } from "./projectV2New.slice"; import ProjectFormSubmitGroup from "./ProjectV2FormSubmitGroup"; import ProjectV2NewForm from "./ProjectV2NewForm"; +import WipBadge from "../shared/WipBadge"; function projectToProjectPost( project: NewProjectV2State["project"] @@ -63,10 +64,10 @@ function ProjectV2NewHeader({ }: Pick) { return ( <> -

+

V2 Projects let you group together related resources and control who can - access them. -

+ access them. {" "} +
{currentStep === 0 && } {currentStep === 1 && } {currentStep === 2 && } diff --git a/client/src/features/projectsV2/shared/WipBadge.tsx b/client/src/features/projectsV2/shared/WipBadge.tsx new file mode 100644 index 0000000000..cd36509a20 --- /dev/null +++ b/client/src/features/projectsV2/shared/WipBadge.tsx @@ -0,0 +1,38 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +import uniqueIdFn from "lodash/uniqueId"; +import { useState } from "react"; +import { Badge } from "reactstrap"; + +import { ThrottledTooltip } from "../../../components/Tooltip"; + +export default function WipBadge() { + const [uniqueId] = useState(`wip-tooltip-toggle-${uniqueIdFn()}`); + return ( + <> + + + Alpha + + + ); +} diff --git a/client/src/features/projectsV2/show/ProjectV2Show.tsx b/client/src/features/projectsV2/show/ProjectV2Show.tsx index 934d4a12e5..eea15122a5 100644 --- a/client/src/features/projectsV2/show/ProjectV2Show.tsx +++ b/client/src/features/projectsV2/show/ProjectV2Show.tsx @@ -32,6 +32,7 @@ import { Url } from "../../../utils/helpers/url"; import { isErrorResponse, useGetProjectsByProjectIdQuery } from "../api"; import type { Project } from "../api"; +import WipBadge from "../shared/WipBadge"; import { ProjectV2MembersForm, @@ -54,7 +55,8 @@ function ProjectV2Header({ <>
{project.slug}
{project.visibility}
- + {" "} +
Date: Thu, 18 Jan 2024 17:08:53 +0100 Subject: [PATCH 03/15] include links to make creating a project and navigating to the project list after creation easier --- .../src/features/projectsV2/list/ProjectV2List.tsx | 14 +++++++++++--- .../src/features/projectsV2/new/ProjectV2New.tsx | 12 ++++++++++-- .../src/features/projectsV2/show/ProjectV2Show.tsx | 7 +++++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/client/src/features/projectsV2/list/ProjectV2List.tsx b/client/src/features/projectsV2/list/ProjectV2List.tsx index 832a97a7e2..3dda5b825d 100644 --- a/client/src/features/projectsV2/list/ProjectV2List.tsx +++ b/client/src/features/projectsV2/list/ProjectV2List.tsx @@ -97,14 +97,22 @@ function ProjectList() { } export default function ProjectV2List() { + const newProjectUrl = Url.get(Url.pages.projectsV2.new); return ( - All visible projects {" "} - + <> +
+ All visible projects {" "} +
+
+ + Create New Project + +
+ } > diff --git a/client/src/features/projectsV2/new/ProjectV2New.tsx b/client/src/features/projectsV2/new/ProjectV2New.tsx index 5f9f7cf549..f1d2440607 100644 --- a/client/src/features/projectsV2/new/ProjectV2New.tsx +++ b/client/src/features/projectsV2/new/ProjectV2New.tsx @@ -18,13 +18,14 @@ import { FormEvent, useCallback } from "react"; import { useDispatch } from "react-redux"; - +import { Link } from "react-router-dom"; import { Button, Form, Label } from "reactstrap"; import { Loader } from "../../../components/Loader"; import FormSchema from "../../../components/formschema/FormSchema"; import useAppSelector from "../../../utils/customHooks/useAppSelector.hook"; import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; +import { Url } from "../../../utils/helpers/url"; import { usePostProjectsMutation } from "../api"; import type { ProjectPost } from "../api"; @@ -142,7 +143,14 @@ function ProjectV2BeingCreated({ ); } - return
Project created
; + const projectList = Url.get(Url.pages.projectsV2.list); + return ( + <> +
Project created.
+ {" "} + Go to project list + + ); } function ProjectV2NewReviewCreateStep({ diff --git a/client/src/features/projectsV2/show/ProjectV2Show.tsx b/client/src/features/projectsV2/show/ProjectV2Show.tsx index eea15122a5..88b03197d3 100644 --- a/client/src/features/projectsV2/show/ProjectV2Show.tsx +++ b/client/src/features/projectsV2/show/ProjectV2Show.tsx @@ -16,6 +16,7 @@ * limitations under the License. */ import { useCallback, useState } from "react"; +import { ArrowLeft } from "react-bootstrap-icons"; import { Link, useLocation } from "react-router-dom"; import { Dropdown, @@ -51,12 +52,18 @@ function ProjectV2Header({ setSettingEdit, settingEdit, }: ProjectV2HeaderProps) { + const projectListUrl = Url.get(Url.pages.projectsV2.list); return ( <>
{project.slug}
{project.visibility}
{" "} +
+ + Back to list + +

Date: Fri, 2 Feb 2024 09:03:33 +0100 Subject: [PATCH 04/15] refactor: rename edit folder to fields folder --- .../projectsV2/{edit => fields}/ProjectRepositoryFormField.tsx | 0 .../src/features/projectsV2/{edit => fields}/formField.types.ts | 0 client/src/features/projectsV2/{edit => fields}/index.tsx | 0 .../projectsV2/{edit => fields}/projectMemberFields.tsx | 0 .../features/projectsV2/{edit => fields}/simpleFormFields.tsx | 0 client/src/features/projectsV2/new/ProjectV2NewForm.tsx | 2 +- client/src/features/projectsV2/show/ProjectV2EditForm.tsx | 2 +- 7 files changed, 2 insertions(+), 2 deletions(-) rename client/src/features/projectsV2/{edit => fields}/ProjectRepositoryFormField.tsx (100%) rename client/src/features/projectsV2/{edit => fields}/formField.types.ts (100%) rename client/src/features/projectsV2/{edit => fields}/index.tsx (100%) rename client/src/features/projectsV2/{edit => fields}/projectMemberFields.tsx (100%) rename client/src/features/projectsV2/{edit => fields}/simpleFormFields.tsx (100%) diff --git a/client/src/features/projectsV2/edit/ProjectRepositoryFormField.tsx b/client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx similarity index 100% rename from client/src/features/projectsV2/edit/ProjectRepositoryFormField.tsx rename to client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx diff --git a/client/src/features/projectsV2/edit/formField.types.ts b/client/src/features/projectsV2/fields/formField.types.ts similarity index 100% rename from client/src/features/projectsV2/edit/formField.types.ts rename to client/src/features/projectsV2/fields/formField.types.ts diff --git a/client/src/features/projectsV2/edit/index.tsx b/client/src/features/projectsV2/fields/index.tsx similarity index 100% rename from client/src/features/projectsV2/edit/index.tsx rename to client/src/features/projectsV2/fields/index.tsx diff --git a/client/src/features/projectsV2/edit/projectMemberFields.tsx b/client/src/features/projectsV2/fields/projectMemberFields.tsx similarity index 100% rename from client/src/features/projectsV2/edit/projectMemberFields.tsx rename to client/src/features/projectsV2/fields/projectMemberFields.tsx diff --git a/client/src/features/projectsV2/edit/simpleFormFields.tsx b/client/src/features/projectsV2/fields/simpleFormFields.tsx similarity index 100% rename from client/src/features/projectsV2/edit/simpleFormFields.tsx rename to client/src/features/projectsV2/fields/simpleFormFields.tsx diff --git a/client/src/features/projectsV2/new/ProjectV2NewForm.tsx b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx index 402690e657..94dfcb5a33 100644 --- a/client/src/features/projectsV2/new/ProjectV2NewForm.tsx +++ b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx @@ -30,7 +30,7 @@ import { ProjectRepositoryFormField, ProjectSlugFormField, ProjectVisibilityFormField, -} from "../edit"; +} from "../fields"; import ProjectFormSubmitGroup from "./ProjectV2FormSubmitGroup"; import type { NewProjectV2State } from "./projectV2New.slice"; diff --git a/client/src/features/projectsV2/show/ProjectV2EditForm.tsx b/client/src/features/projectsV2/show/ProjectV2EditForm.tsx index 8b8dd8b208..db23f73250 100644 --- a/client/src/features/projectsV2/show/ProjectV2EditForm.tsx +++ b/client/src/features/projectsV2/show/ProjectV2EditForm.tsx @@ -48,7 +48,7 @@ import { ProjectNameFormField, ProjectRepositoryFormField, ProjectVisibilityFormField, -} from "../edit"; +} from "../fields"; import { SettingEditOption } from "./projectV2Show.types"; From b9d525b0ccd046b832968ef0491a398f32b57037 Mon Sep 17 00:00:00 2001 From: Chandrasekhar Ramakrishnan Date: Fri, 2 Feb 2024 09:29:19 +0100 Subject: [PATCH 05/15] do not remove help text after validation --- .../fields/ProjectRepositoryFormField.tsx | 11 ++++---- .../projectsV2/fields/simpleFormFields.tsx | 28 ++++++++----------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx b/client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx index 34129f123a..69f09c3e2c 100644 --- a/client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx +++ b/client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx @@ -89,15 +89,14 @@ export default function ProjectRepositoryFormField({ X - {errors.repositories && errors.repositories[index] ? null : index > - 0 ? null : ( - +
+ Please provide a valid URL or remove the repository. +
+ {index == 0 && ( + A URL that refers to a git repository. )} -
- Please provide a valid URL or remove the repository. -
); } diff --git a/client/src/features/projectsV2/fields/simpleFormFields.tsx b/client/src/features/projectsV2/fields/simpleFormFields.tsx index 38c914c299..6f4f469a37 100644 --- a/client/src/features/projectsV2/fields/simpleFormFields.tsx +++ b/client/src/features/projectsV2/fields/simpleFormFields.tsx @@ -82,12 +82,10 @@ export function ProjectNameFormField({ )} rules={{ required: true, maxLength: 99 }} /> - {errors[name] ? null : ( - - The name you will use to refer to the project - - )}
Please provide a name
+ + The name you will use to refer to the project + ); } @@ -116,17 +114,15 @@ export function ProjectSlugFormField({ )} rules={{ required: true, maxLength: 99, pattern: /^[a-z0-9-]+$/ }} /> - {errors[name] ? null : ( - - A short, machine-readable identifier for the project, restricted to - lowercase letters, numbers, and hyphens.{" "} - Cannot be changed after project creation. - - )}
Please provide a slug consisting of lowercase letters, numbers, and hyphens.
+ + A short, machine-readable identifier for the project, restricted to + lowercase letters, numbers, and hyphens.{" "} + Cannot be changed after project creation. + ); } @@ -158,12 +154,10 @@ export function ProjectVisibilityFormField({ )} rules={{ required: true }} /> - {errors[name] ? null : ( - - Should the project be visible to everyone or only to members? - - )}
Please select a visibility
+ + Should the project be visible to everyone or only to members? + ); } From 66b03a608e5179cf113dd438096f094aece9c503 Mon Sep 17 00:00:00 2001 From: Chandrasekhar Ramakrishnan Date: Fri, 2 Feb 2024 11:37:50 +0100 Subject: [PATCH 06/15] remove barrel files --- client/src/App.jsx | 47 ++--- .../index.tsx => LazyProjectV2List.tsx} | 24 +-- .../features/projectsV2/LazyProjectV2New.tsx | 29 ++++ .../features/projectsV2/LazyProjectV2Show.tsx | 29 ++++ client/src/features/projectsV2/api/index.ts | 58 ------- .../projectsV2/api/projectV2.enhanced-api.ts | 5 + ...erFields.tsx => AddProjectMemberModal.tsx} | 6 +- .../fields/ProjectDescriptionFormField.tsx | 59 +++++++ .../fields/ProjectNameFormField.tsx | 57 ++++++ .../fields/ProjectSlugFormField.tsx | 62 +++++++ .../fields/ProjectVisibilityFormField.tsx | 60 +++++++ .../projectsV2/fields/simpleFormFields.tsx | 163 ------------------ client/src/features/projectsV2/index.ts | 7 - .../projectsV2/list/ProjectV2List.tsx | 4 +- .../features/projectsV2/new/ProjectV2New.tsx | 4 +- .../projectsV2/new/ProjectV2NewForm.tsx | 12 +- .../features/projectsV2/projectV2.types.ts | 2 +- .../projectsV2/show/ProjectV2EditForm.tsx | 16 +- .../projectsV2/show/ProjectV2Show.tsx | 7 +- client/src/utils/helpers/EnhancedState.ts | 11 +- 20 files changed, 362 insertions(+), 300 deletions(-) rename client/src/features/projectsV2/{fields/index.tsx => LazyProjectV2List.tsx} (62%) create mode 100644 client/src/features/projectsV2/LazyProjectV2New.tsx create mode 100644 client/src/features/projectsV2/LazyProjectV2Show.tsx delete mode 100644 client/src/features/projectsV2/api/index.ts rename client/src/features/projectsV2/fields/{projectMemberFields.tsx => AddProjectMemberModal.tsx} (98%) create mode 100644 client/src/features/projectsV2/fields/ProjectDescriptionFormField.tsx create mode 100644 client/src/features/projectsV2/fields/ProjectNameFormField.tsx create mode 100644 client/src/features/projectsV2/fields/ProjectSlugFormField.tsx create mode 100644 client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx delete mode 100644 client/src/features/projectsV2/fields/simpleFormFields.tsx delete mode 100644 client/src/features/projectsV2/index.ts diff --git a/client/src/App.jsx b/client/src/App.jsx index c616f88573..8b0f73c60e 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -51,11 +51,9 @@ import Cookie from "./privacy/Cookie"; import LazyProjectView from "./project/LazyProjectView"; import LazyProjectList from "./project/list/LazyProjectList"; import LazyNewProject from "./project/new/LazyNewProject"; -import { - ProjectV2List, - ProjectV2New, - ProjectV2Show, -} from "./features/projectsV2/"; +import LazyProjectV2List from "./features/projectsV2/LazyProjectV2List"; +import LazyProjectV2New from "./features/projectsV2/LazyProjectV2New"; +import LazyProjectV2Show from "./features/projectsV2/LazyProjectV2Show"; import LazyStyleGuide from "./styleguide/LazyStyleGuide"; import AppContext from "./utils/context/appContext"; import useLegacySelector from "./utils/customHooks/useLegacySelector.hook"; @@ -276,30 +274,21 @@ function CentralContentContainer(props) { )} /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> + + + + + + + + + + + + + + + ( diff --git a/client/src/features/projectsV2/fields/index.tsx b/client/src/features/projectsV2/LazyProjectV2List.tsx similarity index 62% rename from client/src/features/projectsV2/fields/index.tsx rename to client/src/features/projectsV2/LazyProjectV2List.tsx index a6a7266663..7324588093 100644 --- a/client/src/features/projectsV2/fields/index.tsx +++ b/client/src/features/projectsV2/LazyProjectV2List.tsx @@ -1,5 +1,5 @@ /*! - * Copyright 2023 - Swiss Data Science Center (SDSC) + * Copyright 2024 - Swiss Data Science Center (SDSC) * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and * Eidgenössische Technische Hochschule Zürich (ETHZ). * @@ -13,17 +13,17 @@ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License. + * limitations under the License */ +import { Suspense, lazy } from "react"; +import PageLoader from "../../components/PageLoader"; -export { - ProjectDescriptionFormField, - ProjectNameFormField, - ProjectSlugFormField, - ProjectVisibilityFormField, -} from "./simpleFormFields"; +const ProjectV2List = lazy(() => import("./list/ProjectV2List")); -export { AddProjectMemberModal } from "./projectMemberFields"; -import ProjectRepositoryFormField from "./ProjectRepositoryFormField"; - -export { ProjectRepositoryFormField }; +export default function LazyProjectV2List() { + return ( + }> + + + ); +} diff --git a/client/src/features/projectsV2/LazyProjectV2New.tsx b/client/src/features/projectsV2/LazyProjectV2New.tsx new file mode 100644 index 0000000000..2e41c04bf6 --- /dev/null +++ b/client/src/features/projectsV2/LazyProjectV2New.tsx @@ -0,0 +1,29 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +import { Suspense, lazy } from "react"; +import PageLoader from "../../components/PageLoader"; + +const ProjectV2New = lazy(() => import("./new/ProjectV2New")); + +export default function LazyProjectV2New() { + return ( + }> + + + ); +} diff --git a/client/src/features/projectsV2/LazyProjectV2Show.tsx b/client/src/features/projectsV2/LazyProjectV2Show.tsx new file mode 100644 index 0000000000..a153631852 --- /dev/null +++ b/client/src/features/projectsV2/LazyProjectV2Show.tsx @@ -0,0 +1,29 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +import { Suspense, lazy } from "react"; +import PageLoader from "../../components/PageLoader"; + +const ProjectV2Show = lazy(() => import("./show/ProjectV2Show")); + +export default function LazyProjectV2List() { + return ( + }> + + + ); +} diff --git a/client/src/features/projectsV2/api/index.ts b/client/src/features/projectsV2/api/index.ts deleted file mode 100644 index fa9ec5e19d..0000000000 --- a/client/src/features/projectsV2/api/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -export { projectV2Api } from "./projectV2.enhanced-api"; - -export type { - CreationDate, - Description, - ErrorResponse, - Member, - MemberWithRole, - MembersWithRoles, - Name, - Project, - ProjectsList, - ProjectPatch, - ProjectPost, - RepositoriesList, - Role, - Slug, - Ulid, - UserId, - Visibility, -} from "./projectV2.api"; - -export type { - DeleteProjectsByProjectIdApiArg, - DeleteProjectsByProjectIdApiResponse, - DeleteProjectsByProjectIdMembersAndMemberIdApiArg, - DeleteProjectsByProjectIdMembersAndMemberIdApiResponse, - FullUsersWithRoles, - GetProjectsByProjectIdApiArg, - GetProjectsByProjectIdApiResponse, - GetProjectsByProjectIdMembersApiArg, - GetProjectsByProjectIdMembersApiResponse, - GetProjectsApiResponse, - GetProjectsApiArg, - PatchProjectsByProjectIdApiResponse, - PatchProjectsByProjectIdApiArg, - PatchProjectsByProjectIdMembersApiArg, - PatchProjectsByProjectIdMembersApiResponse, - PostProjectsApiResponse, - PostProjectsApiArg, -} from "./projectV2.api"; - -export { - useGetProjectsQuery, - usePostProjectsMutation, - useGetProjectsByProjectIdQuery, - usePatchProjectsByProjectIdMutation, - useDeleteProjectsByProjectIdMutation, - useGetProjectsByProjectIdMembersQuery, - usePatchProjectsByProjectIdMembersMutation, - useDeleteProjectsByProjectIdMembersAndMemberIdMutation, -} from "./projectV2.enhanced-api"; - -import type { ErrorResponse } from "./projectV2.api"; - -export function isErrorResponse(arg: unknown): arg is { data: ErrorResponse } { - return (arg as { data: ErrorResponse }).data?.error != null; -} diff --git a/client/src/features/projectsV2/api/projectV2.enhanced-api.ts b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts index e58ea5ccd2..53c194d195 100644 --- a/client/src/features/projectsV2/api/projectV2.enhanced-api.ts +++ b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts @@ -1,5 +1,6 @@ import { projectV2Api as api } from "./projectV2.api"; import type { + ErrorResponse, GetProjectsApiArg, GetProjectsApiResponse as GetProjectsApiResponseOrig, ProjectsList, @@ -92,3 +93,7 @@ export const { usePatchProjectsByProjectIdMembersMutation, useDeleteProjectsByProjectIdMembersAndMemberIdMutation, } = enhancedApi; + +export function isErrorResponse(arg: unknown): arg is { data: ErrorResponse } { + return (arg as { data: ErrorResponse }).data?.error != null; +} diff --git a/client/src/features/projectsV2/fields/projectMemberFields.tsx b/client/src/features/projectsV2/fields/AddProjectMemberModal.tsx similarity index 98% rename from client/src/features/projectsV2/fields/projectMemberFields.tsx rename to client/src/features/projectsV2/fields/AddProjectMemberModal.tsx index bca64bbdd1..d26ad77d0d 100644 --- a/client/src/features/projectsV2/fields/projectMemberFields.tsx +++ b/client/src/features/projectsV2/fields/AddProjectMemberModal.tsx @@ -36,8 +36,8 @@ import { import { useGetUsersQuery } from "../../user/dataServicesUser.api"; import type { UserWithId } from "../../user/dataServicesUser.api"; -import type { FullUsersWithRoles, MemberWithRole } from "../api"; -import { usePatchProjectsByProjectIdMembersMutation } from "../api"; +import type { FullUsersWithRoles, MemberWithRole } from "../api/projectV2.api"; +import { usePatchProjectsByProjectIdMembersMutation } from "../api/projectV2.enhanced-api"; import type { ProjectMember } from "../projectV2.types"; const emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/; @@ -242,7 +242,7 @@ function AddProjectMemberAccessForm({ ); } -export function AddProjectMemberModal({ +export default function AddProjectMemberModal({ isOpen, members, projectId, diff --git a/client/src/features/projectsV2/fields/ProjectDescriptionFormField.tsx b/client/src/features/projectsV2/fields/ProjectDescriptionFormField.tsx new file mode 100644 index 0000000000..c32753a5ca --- /dev/null +++ b/client/src/features/projectsV2/fields/ProjectDescriptionFormField.tsx @@ -0,0 +1,59 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; + +import { Controller } from "react-hook-form"; +import type { FieldValues } from "react-hook-form"; + +import { FormText, Input, Label } from "reactstrap"; +import type { GenericProjectFormFieldProps } from "./formField.types"; + +export default function ProjectDescriptionFormField({ + control, + errors, + name, +}: GenericProjectFormFieldProps) { + return ( +
+ + ( + + )} + rules={{ maxLength: 500, required: false }} + /> + {errors[name] ? null : ( + + A brief (at most 500 character) description of the project. + + )} +
Please provide a description
+
+ ); +} diff --git a/client/src/features/projectsV2/fields/ProjectNameFormField.tsx b/client/src/features/projectsV2/fields/ProjectNameFormField.tsx new file mode 100644 index 0000000000..fc49c7dfbc --- /dev/null +++ b/client/src/features/projectsV2/fields/ProjectNameFormField.tsx @@ -0,0 +1,57 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; + +import { Controller } from "react-hook-form"; +import type { FieldValues } from "react-hook-form"; + +import { FormText, Input, Label } from "reactstrap"; +import type { GenericProjectFormFieldProps } from "./formField.types"; + +export default function ProjectNameFormField({ + control, + errors, + name, +}: GenericProjectFormFieldProps) { + return ( +
+ + ( + + )} + rules={{ required: true, maxLength: 99 }} + /> +
Please provide a name
+ + The name you will use to refer to the project + +
+ ); +} diff --git a/client/src/features/projectsV2/fields/ProjectSlugFormField.tsx b/client/src/features/projectsV2/fields/ProjectSlugFormField.tsx new file mode 100644 index 0000000000..76cdfd1d8c --- /dev/null +++ b/client/src/features/projectsV2/fields/ProjectSlugFormField.tsx @@ -0,0 +1,62 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; + +import { Controller } from "react-hook-form"; +import type { FieldValues } from "react-hook-form"; + +import { FormText, Input, Label } from "reactstrap"; +import type { GenericProjectFormFieldProps } from "./formField.types"; + +export default function ProjectSlugFormField({ + control, + errors, + name, +}: GenericProjectFormFieldProps) { + return ( +
+ + ( + + )} + rules={{ required: true, maxLength: 99, pattern: /^[a-z0-9-]+$/ }} + /> +
+ Please provide a slug consisting of lowercase letters, numbers, and + hyphens. +
+ + A short, machine-readable identifier for the project, restricted to + lowercase letters, numbers, and hyphens.{" "} + Cannot be changed after project creation. + +
+ ); +} diff --git a/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx b/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx new file mode 100644 index 0000000000..71ef9a04af --- /dev/null +++ b/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx @@ -0,0 +1,60 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; + +import { Controller } from "react-hook-form"; +import type { FieldValues } from "react-hook-form"; + +import { FormText, Input, Label } from "reactstrap"; +import type { GenericProjectFormFieldProps } from "./formField.types"; + +export default function ProjectVisibilityFormField({ + control, + errors, + name, +}: GenericProjectFormFieldProps) { + return ( +
+ + ( + + + + + )} + rules={{ required: true }} + /> +
Please select a visibility
+ + Should the project be visible to everyone or only to members? + +
+ ); +} diff --git a/client/src/features/projectsV2/fields/simpleFormFields.tsx b/client/src/features/projectsV2/fields/simpleFormFields.tsx deleted file mode 100644 index 6f4f469a37..0000000000 --- a/client/src/features/projectsV2/fields/simpleFormFields.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/*! - * Copyright 2023 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import cx from "classnames"; - -import { Controller } from "react-hook-form"; -import type { FieldValues } from "react-hook-form"; - -import { FormText, Input, Label } from "reactstrap"; -import type { GenericProjectFormFieldProps } from "./formField.types"; - -export function ProjectDescriptionFormField({ - control, - errors, - name, -}: GenericProjectFormFieldProps) { - return ( -
- - ( - - )} - rules={{ maxLength: 500, required: false }} - /> - {errors[name] ? null : ( - - A brief (at most 500 character) description of the project. - - )} -
Please provide a description
-
- ); -} - -export function ProjectNameFormField({ - control, - errors, - name, -}: GenericProjectFormFieldProps) { - return ( -
- - ( - - )} - rules={{ required: true, maxLength: 99 }} - /> -
Please provide a name
- - The name you will use to refer to the project - -
- ); -} - -export function ProjectSlugFormField({ - control, - errors, - name, -}: GenericProjectFormFieldProps) { - return ( -
- - ( - - )} - rules={{ required: true, maxLength: 99, pattern: /^[a-z0-9-]+$/ }} - /> -
- Please provide a slug consisting of lowercase letters, numbers, and - hyphens. -
- - A short, machine-readable identifier for the project, restricted to - lowercase letters, numbers, and hyphens.{" "} - Cannot be changed after project creation. - -
- ); -} - -export function ProjectVisibilityFormField({ - control, - errors, - name, -}: GenericProjectFormFieldProps) { - return ( -
- - ( - - - - - )} - rules={{ required: true }} - /> -
Please select a visibility
- - Should the project be visible to everyone or only to members? - -
- ); -} diff --git a/client/src/features/projectsV2/index.ts b/client/src/features/projectsV2/index.ts deleted file mode 100644 index de8564487d..0000000000 --- a/client/src/features/projectsV2/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { projectV2Api } from "./api/"; -export { projectV2NewSlice } from "./new/projectV2New.slice"; - -import ProjectV2List from "./list/ProjectV2List"; -import ProjectV2New from "./new/ProjectV2New"; -import ProjectV2Show from "./show/ProjectV2Show"; -export { ProjectV2List, ProjectV2New, ProjectV2Show }; diff --git a/client/src/features/projectsV2/list/ProjectV2List.tsx b/client/src/features/projectsV2/list/ProjectV2List.tsx index 3dda5b825d..9f5de1e5a9 100644 --- a/client/src/features/projectsV2/list/ProjectV2List.tsx +++ b/client/src/features/projectsV2/list/ProjectV2List.tsx @@ -25,8 +25,8 @@ import { Pagination } from "../../../components/Pagination"; import { TimeCaption } from "../../../components/TimeCaption"; import { Url } from "../../../utils/helpers/url"; -import { useGetProjectsQuery } from "../api"; -import type { Project } from "../api"; +import { useGetProjectsQuery } from "../api/projectV2.enhanced-api"; +import type { Project } from "../api/projectV2.api"; import WipBadge from "../shared/WipBadge"; import styles from "./projectV2List.module.scss"; diff --git a/client/src/features/projectsV2/new/ProjectV2New.tsx b/client/src/features/projectsV2/new/ProjectV2New.tsx index f1d2440607..e37c30b1d1 100644 --- a/client/src/features/projectsV2/new/ProjectV2New.tsx +++ b/client/src/features/projectsV2/new/ProjectV2New.tsx @@ -27,8 +27,8 @@ import useAppSelector from "../../../utils/customHooks/useAppSelector.hook"; import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; import { Url } from "../../../utils/helpers/url"; -import { usePostProjectsMutation } from "../api"; -import type { ProjectPost } from "../api"; +import { usePostProjectsMutation } from "../api/projectV2.enhanced-api"; +import type { ProjectPost } from "../api/projectV2.api"; import { ProjectV2DescriptionAndRepositories } from "../show/ProjectV2Show"; import type { NewProjectV2State } from "./projectV2New.slice"; diff --git a/client/src/features/projectsV2/new/ProjectV2NewForm.tsx b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx index 94dfcb5a33..ce302e706a 100644 --- a/client/src/features/projectsV2/new/ProjectV2NewForm.tsx +++ b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx @@ -24,13 +24,11 @@ import { Button, Form, Label } from "reactstrap"; import useAppSelector from "../../../utils/customHooks/useAppSelector.hook"; import { slugFromTitle } from "../../../utils/helpers/HelperFunctions"; -import { - ProjectDescriptionFormField, - ProjectNameFormField, - ProjectRepositoryFormField, - ProjectSlugFormField, - ProjectVisibilityFormField, -} from "../fields"; +import ProjectDescriptionFormField from "../fields/ProjectDescriptionFormField"; +import ProjectNameFormField from "../fields/ProjectNameFormField"; +import ProjectRepositoryFormField from "../fields/ProjectRepositoryFormField"; +import ProjectSlugFormField from "../fields/ProjectSlugFormField"; +import ProjectVisibilityFormField from "../fields/ProjectVisibilityFormField"; import ProjectFormSubmitGroup from "./ProjectV2FormSubmitGroup"; import type { NewProjectV2State } from "./projectV2New.slice"; diff --git a/client/src/features/projectsV2/projectV2.types.ts b/client/src/features/projectsV2/projectV2.types.ts index 039330d0e8..0efa39b48e 100644 --- a/client/src/features/projectsV2/projectV2.types.ts +++ b/client/src/features/projectsV2/projectV2.types.ts @@ -1,4 +1,4 @@ -import type { Role, Visibility } from "./api"; +import type { Role, Visibility } from "./api/projectV2.api"; export type ProjectVisibility = Visibility; export type ProjectRole = Role; diff --git a/client/src/features/projectsV2/show/ProjectV2EditForm.tsx b/client/src/features/projectsV2/show/ProjectV2EditForm.tsx index db23f73250..f2de24478d 100644 --- a/client/src/features/projectsV2/show/ProjectV2EditForm.tsx +++ b/client/src/features/projectsV2/show/ProjectV2EditForm.tsx @@ -38,17 +38,15 @@ import { useDeleteProjectsByProjectIdMembersAndMemberIdMutation, useGetProjectsByProjectIdMembersQuery, usePatchProjectsByProjectIdMutation, -} from "../api"; -import type { Member, Project, ProjectPatch } from "../api"; +} from "../api/projectV2.enhanced-api"; +import type { Member, Project, ProjectPatch } from "../api/projectV2.api"; import type { Repository } from "../projectV2.types"; -import { - AddProjectMemberModal, - ProjectDescriptionFormField, - ProjectNameFormField, - ProjectRepositoryFormField, - ProjectVisibilityFormField, -} from "../fields"; +import AddProjectMemberModal from "../fields/AddProjectMemberModal"; +import ProjectDescriptionFormField from "../fields/ProjectDescriptionFormField"; +import ProjectNameFormField from "../fields/ProjectNameFormField"; +import ProjectRepositoryFormField from "../fields/ProjectRepositoryFormField"; +import ProjectVisibilityFormField from "../fields/ProjectVisibilityFormField"; import { SettingEditOption } from "./projectV2Show.types"; diff --git a/client/src/features/projectsV2/show/ProjectV2Show.tsx b/client/src/features/projectsV2/show/ProjectV2Show.tsx index 88b03197d3..180e95ea65 100644 --- a/client/src/features/projectsV2/show/ProjectV2Show.tsx +++ b/client/src/features/projectsV2/show/ProjectV2Show.tsx @@ -31,8 +31,11 @@ import { Loader } from "../../../components/Loader"; import { TimeCaption } from "../../../components/TimeCaption"; import { Url } from "../../../utils/helpers/url"; -import { isErrorResponse, useGetProjectsByProjectIdQuery } from "../api"; -import type { Project } from "../api"; +import { + isErrorResponse, + useGetProjectsByProjectIdQuery, +} from "../api/projectV2.enhanced-api"; +import type { Project } from "../api/projectV2.api"; import WipBadge from "../shared/WipBadge"; import { diff --git a/client/src/utils/helpers/EnhancedState.ts b/client/src/utils/helpers/EnhancedState.ts index 5aa181de0e..c6a8812f93 100644 --- a/client/src/utils/helpers/EnhancedState.ts +++ b/client/src/utils/helpers/EnhancedState.ts @@ -25,7 +25,7 @@ import { AnyAction, ReducersMapObject, StoreEnhancer, - configureStore + configureStore, } from "@reduxjs/toolkit"; import adminComputeResourcesApi from "../../features/admin/adminComputeResources.api"; @@ -43,7 +43,8 @@ import { projectCoreApi } from "../../features/project/projectCoreApi"; import projectGitLabApi from "../../features/project/projectGitLab.api"; import { projectKgApi } from "../../features/project/projectKg.api"; import { projectsApi } from "../../features/projects/projects.api"; -import { projectV2Api, projectV2NewSlice } from "../../features/projectsV2/"; +import { projectV2Api } from "../../features/projectsV2/api/projectV2.enhanced-api"; +import { projectV2NewSlice } from "../../features/projectsV2/new/projectV2New.slice"; import { recentUserActivityApi } from "../../features/recentUserActivity/RecentUserActivityApi"; import sessionsApi from "../../features/session/sessions.api"; import { sessionSidecarApi } from "../../features/session/sidecarApi"; @@ -94,7 +95,7 @@ export const createStore = ( [termsApi.reducerPath]: termsApi.reducer, [userPreferencesApi.reducerPath]: userPreferencesApi.reducer, [versionsApi.reducerPath]: versionsApi.reducer, - [workflowsApi.reducerPath]: workflowsApi.reducer + [workflowsApi.reducerPath]: workflowsApi.reducer, }; // For the moment, disable the custom middleware, since it causes problems for our app. @@ -103,7 +104,7 @@ export const createStore = ( middleware: (getDefaultMiddleware) => getDefaultMiddleware({ immutableCheck: false, - serializableCheck: false + serializableCheck: false, }) .concat(adminComputeResourcesApi.middleware) .concat(adminKeycloakApi.middleware) @@ -128,7 +129,7 @@ export const createStore = ( .concat(userPreferencesApi.middleware) .concat(versionsApi.middleware) .concat(workflowsApi.middleware), - enhancers + enhancers, }); return store; }; From d2035b509828c8d128273e5a48c44022295c1bab Mon Sep 17 00:00:00 2001 From: Chandrasekhar Ramakrishnan Date: Fri, 2 Feb 2024 12:04:32 +0100 Subject: [PATCH 07/15] use classnames when there is more than one class --- README.md | 4 ++++ .../fields/AddProjectMemberModal.tsx | 6 ++++-- .../features/projectsV2/list/ProjectV2List.tsx | 18 +++++++++++------- .../new/ProjectV2FormSubmitGroup.tsx | 2 +- .../features/projectsV2/new/ProjectV2New.tsx | 7 ++++--- .../projectsV2/new/ProjectV2NewForm.tsx | 8 +++++--- .../projectsV2/show/ProjectV2EditForm.tsx | 8 ++++---- 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3b246775f7..206476e3af 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,10 @@ We use [CSS modules](https://github.com/css-modules/css-modules) to apply CSS st locally and avoid leaking styles to the whole web application. No additional configuration is needed since Create React App [supports CSS modules out of the box](https://create-react-app.dev/docs/adding-a-css-modules-stylesheet). +#### **Use classnames for complex CSS class names** + +When a node has a class name that is either computed dynamically or is comprised of two or more classes, use the [classnames](https://www.npmjs.com/package/classnames) package (idiomatically imported typically as `cx`) to construct the class name string. + ### Code splitting If a component requires a large package, it can be loaded on demand by using the `lazy()` function from React. diff --git a/client/src/features/projectsV2/fields/AddProjectMemberModal.tsx b/client/src/features/projectsV2/fields/AddProjectMemberModal.tsx index d26ad77d0d..1aaf6d2224 100644 --- a/client/src/features/projectsV2/fields/AddProjectMemberModal.tsx +++ b/client/src/features/projectsV2/fields/AddProjectMemberModal.tsx @@ -126,7 +126,7 @@ function AddProjectMemberEmailLookupForm({ {isUserNotFound &&
No user found for {lookupEmail}.
} -
+
diff --git a/client/src/features/projectsV2/new/ProjectV2NewForm.tsx b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx index ce302e706a..85c3f85b03 100644 --- a/client/src/features/projectsV2/new/ProjectV2NewForm.tsx +++ b/client/src/features/projectsV2/new/ProjectV2NewForm.tsx @@ -16,6 +16,8 @@ * limitations under the License. */ +import cx from "classnames"; + import { useCallback, useEffect } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { useDispatch } from "react-redux"; @@ -46,7 +48,7 @@ export default function ProjectV2NewForm({ currentStep, }: ProjectV2NewFormProps) { return ( -
+
{currentStep === 0 && ( )} @@ -93,7 +95,7 @@ function ProjectV2NewAccessStepForm({ currentStep }: ProjectV2NewFormProps) { errors={errors} />
-
+
@@ -192,7 +194,7 @@ function ProjectV2NewRepositoryStepForm({ ); return ( <> -
+

Add repositories

@@ -257,7 +257,7 @@ export function ProjectV2MembersForm({ ); return ( <> -
+

Project Members

@@ -359,7 +359,7 @@ export function ProjectV2RepositoryForm({ return ( <> -
+

Update repositories

); diff --git a/client/src/features/projectsV2/fields/ProjectNameFormField.tsx b/client/src/features/projectsV2/fields/ProjectNameFormField.tsx index fc49c7dfbc..111e603acf 100644 --- a/client/src/features/projectsV2/fields/ProjectNameFormField.tsx +++ b/client/src/features/projectsV2/fields/ProjectNameFormField.tsx @@ -39,6 +39,7 @@ export default function ProjectNameFormField({ name={name} render={({ field }) => ( ({ rules={{ required: true, maxLength: 99 }} />
Please provide a name
- + The name you will use to refer to the project
diff --git a/client/src/features/projectsV2/fields/ProjectSlugFormField.tsx b/client/src/features/projectsV2/fields/ProjectSlugFormField.tsx index 76cdfd1d8c..ab44018be2 100644 --- a/client/src/features/projectsV2/fields/ProjectSlugFormField.tsx +++ b/client/src/features/projectsV2/fields/ProjectSlugFormField.tsx @@ -39,6 +39,7 @@ export default function ProjectSlugFormField({ name={name} render={({ field }) => ( ({ Please provide a slug consisting of lowercase letters, numbers, and hyphens.
- + A short, machine-readable identifier for the project, restricted to lowercase letters, numbers, and hyphens.{" "} Cannot be changed after project creation. diff --git a/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx b/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx index 71ef9a04af..a1dff0e0e0 100644 --- a/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx +++ b/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx @@ -35,6 +35,7 @@ export default function ProjectVisibilityFormField({ Visibility ( @@ -52,7 +53,7 @@ export default function ProjectVisibilityFormField({ rules={{ required: true }} />
Please select a visibility
- + Should the project be visible to everyone or only to members?
From af82cda0cfb8688eb6000b8934c8115ac37016fa Mon Sep 17 00:00:00 2001 From: Chandrasekhar Ramakrishnan Date: Fri, 2 Feb 2024 14:20:08 +0100 Subject: [PATCH 10/15] make ProjectRepositoryFormFieldProps non-generic --- .../projectsV2/fields/ProjectRepositoryFormField.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx b/client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx index 69f09c3e2c..e5bc53d601 100644 --- a/client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx +++ b/client/src/features/projectsV2/fields/ProjectRepositoryFormField.tsx @@ -26,8 +26,8 @@ import { Button, FormText, Input, Label } from "reactstrap"; import type { Repository } from "../projectV2.types"; import type { GenericProjectFormFieldProps } from "./formField.types"; -interface ProjectRepositoryFormFieldProps - extends GenericProjectFormFieldProps { +interface ProjectRepositoryFormFieldProps + extends GenericProjectFormFieldProps { id: string; index: number; onDelete: () => void; @@ -45,7 +45,7 @@ export default function ProjectRepositoryFormField({ index, name, onDelete, -}: ProjectRepositoryFormFieldProps) { +}: ProjectRepositoryFormFieldProps) { return (