From 6ba48be170f0277a144358254a29a50f54aaaf37 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Wed, 28 Dec 2022 18:52:19 +0200 Subject: [PATCH 1/6] feat: added Git Servies tab to the User Preferences Signed-off-by: Oleksii Orel --- .deps/dev.md | 6 +- .deps/prod.md | 8 +- packages/common/src/dto/api.ts | 2 + .../services/helpers/retryableExec.ts | 4 + .../src/localRun/hooks/authorizationHooks.ts | 2 +- packages/dashboard-frontend/package.json | 2 +- .../GetStarted/__tests__/devfileMetadata.json | 134 +------ .../__snapshots__/index.spec.tsx.snap | 217 ---------- .../UserAccount/__tests__/index.spec.tsx | 71 ---- .../src/pages/UserAccount/index.tsx | 78 ---- .../Modals/DeleteRegistriesModal.tsx | 6 +- .../Modals/EditRegistryModal.tsx | 2 +- .../__tests__/index.spec.tsx | 4 - .../ContainerRegistriesTab/index.tsx | 15 +- .../__snapshots__/index.spec.tsx.snap | 36 ++ .../EmptyState/__tests__/index.spec.tsx | 23 ++ .../GitServicesTab/EmptyState/index.tsx | 32 ++ .../Modals/RevokeGitServicesModal.tsx | 121 ++++++ .../__tests__/RevokeRegistriesModal.spec.tsx | 115 ++++++ .../RevokeRegistriesModal.spec.tsx.snap | 3 + .../__tests__/__mocks__/gitOauthRowBuilder.ts | 32 ++ .../__snapshots__/index.spec.tsx.snap | 375 ++++++++++++++++++ .../GitServicesTab/__tests__/index.spec.tsx | 117 ++++++ .../UserPreferences/GitServicesTab/index.tsx | 255 ++++++++++++ .../src/pages/UserPreferences/index.tsx | 23 +- .../{clusterConfig.ts => clusterConfigApi.ts} | 0 .../{clusterInfo.ts => clusterInfoApi.ts} | 0 .../src/services/helpers/types.ts | 2 +- .../src/store/ClusterConfig/index.ts | 2 +- .../src/store/ClusterInfo/index.ts | 2 +- .../src/store/GitOauthConfig/index.ts | 169 ++++++++ .../src/store/GitOauthConfig/selectors.ts | 29 ++ .../src/store/GitOauthConfig/types.ts | 18 + .../src/store/__mocks__/storeBuilder.ts | 17 + .../dashboard-frontend/src/store/index.ts | 3 + yarn.lock | 8 +- 36 files changed, 1407 insertions(+), 526 deletions(-) delete mode 100644 packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap delete mode 100644 packages/dashboard-frontend/src/pages/UserAccount/__tests__/index.spec.tsx delete mode 100644 packages/dashboard-frontend/src/pages/UserAccount/index.tsx create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/__snapshots__/index.spec.tsx.snap create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/index.spec.tsx create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/index.tsx create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/RevokeGitServicesModal.tsx create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/RevokeRegistriesModal.spec.tsx create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/__snapshots__/RevokeRegistriesModal.spec.tsx.snap create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/__mocks__/gitOauthRowBuilder.ts create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/__snapshots__/index.spec.tsx.snap create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/index.spec.tsx create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx rename packages/dashboard-frontend/src/services/dashboard-backend-client/{clusterConfig.ts => clusterConfigApi.ts} (100%) rename packages/dashboard-frontend/src/services/dashboard-backend-client/{clusterInfo.ts => clusterInfoApi.ts} (100%) create mode 100644 packages/dashboard-frontend/src/store/GitOauthConfig/index.ts create mode 100644 packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts create mode 100644 packages/dashboard-frontend/src/store/GitOauthConfig/types.ts diff --git a/.deps/dev.md b/.deps/dev.md index a10aa6d5f..eb6ef50e1 100644 --- a/.deps/dev.md +++ b/.deps/dev.md @@ -163,7 +163,7 @@ | [`@testing-library/jest-dom@5.16.2`](https://github.com/testing-library/jest-dom) | MIT | clearlydefined | | [`@testing-library/react@10.4.9`](https://github.com/testing-library/react-testing-library) | MIT | clearlydefined | | [`@testing-library/user-event@12.8.3`](https://github.com/testing-library/user-event) | MIT | clearlydefined | -| [`@tootallnate/once@1.1.2`](git://github.com/TooTallNate/once.git) | MIT | iot.diafanis | +| [`@tootallnate/once@1.1.2`](git://github.com/TooTallNate/once.git) | MIT | clearlydefined | | [`@types/args@5.0.0`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | | [`@types/aria-query@4.2.2`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | | [`@types/axios@0.14.0`](https://github.com/mzabriskie/axios) | MIT | clearlydefined | @@ -180,7 +180,7 @@ | [`@types/eslint-scope@3.7.3`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | | [`@types/eslint@8.4.2`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | #2429 | | [`@types/estree@0.0.51`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | -| [`@types/express-serve-static-core@4.17.28`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | +| [`@types/express-serve-static-core@4.17.28`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | #6020 | | [`@types/express@4.17.13`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | #5760 | | [`@types/fs-extra@9.0.13`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | | [`@types/graceful-fs@4.1.5`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | @@ -231,7 +231,7 @@ | [`@types/webpack-sources@3.2.0`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | | [`@types/webpack@4.41.32`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | | [`@types/yargs-parser@21.0.0`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | -| [`@types/yargs@15.0.14`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | clearlydefined | +| [`@types/yargs@15.0.14`](https://github.com/DefinitelyTyped/DefinitelyTyped.git) | MIT | #6241 | | [`@typescript-eslint/eslint-plugin@4.33.0`](https://github.com/typescript-eslint/typescript-eslint.git) | MIT | clearlydefined | | [`@typescript-eslint/experimental-utils@4.33.0`](https://github.com/typescript-eslint/typescript-eslint.git) | MIT | clearlydefined | | [`@typescript-eslint/parser@4.33.0`](https://github.com/typescript-eslint/typescript-eslint.git) | BSD-2-Clause | clearlydefined | diff --git a/.deps/prod.md b/.deps/prod.md index af2182565..4fc8acb1b 100644 --- a/.deps/prod.md +++ b/.deps/prod.md @@ -7,11 +7,11 @@ | `@eclipse-che/api@7.44.0` | EPL-2.0 | ecd.che | | [`@eclipse-che/che-code-devworkspace-handler@1.74.0-dev-e701cae`](git+https://github.com/che-incubator/che-code.git) | EPL-2.0 | ecd.che | | [`@eclipse-che/che-theia-devworkspace-handler@0.0.1-1667484092`](git+https://github.com/eclipse-che/che-theia.git) | EPL-2.0 | ecd.che | -| [`@eclipse-che/common@7.59.0-next`](https://github.com/eclipse-che/che-dashboard) | EPL-2.0 | ecd.che | -| [`@eclipse-che/dashboard-backend@7.59.0-next`](https://github.com/eclipse-che/che-dashboard) | EPL-2.0 | ecd.che | -| [`@eclipse-che/dashboard-frontend@7.59.0-next`](git://github.com/eclipse/che-dashboard.git) | EPL-2.0 | ecd.che | +| [`@eclipse-che/common@7.60.0-next`](https://github.com/eclipse-che/che-dashboard) | EPL-2.0 | ecd.che | +| [`@eclipse-che/dashboard-backend@7.60.0-next`](https://github.com/eclipse-che/che-dashboard) | EPL-2.0 | ecd.che | +| [`@eclipse-che/dashboard-frontend@7.60.0-next`](git://github.com/eclipse/che-dashboard.git) | EPL-2.0 | ecd.che | | [`@eclipse-che/devfile-converter@0.0.1-d624e3e`](git+https://github.com/che-incubator/devfile-converter.git) | EPL-2.0 | ecd.che | -| [`@eclipse-che/workspace-client@0.0.1-1671793076`](https://github.com/eclipse/che-workspace-client) | EPL-2.0 | ecd.che | +| [`@eclipse-che/workspace-client@0.0.1-1672830275`](https://github.com/eclipse/che-workspace-client) | EPL-2.0 | ecd.che | | [`@fastify/ajv-compiler@1.1.0`](git+https://github.com/fastify/ajv-compiler.git) | MIT | clearlydefined | | [`@fastify/cors@7.0.0`](git+https://github.com/fastify/fastify-cors.git) | MIT | clearlydefined | | [`@fastify/error@3.0.0`](git+https://github.com/fastify/fastify-error.git) | MIT | clearlydefined | diff --git a/packages/common/src/dto/api.ts b/packages/common/src/dto/api.ts index 55259d561..23a0591a4 100644 --- a/packages/common/src/dto/api.ts +++ b/packages/common/src/dto/api.ts @@ -12,6 +12,8 @@ import { V220DevfileComponents } from '@devfile/api'; +export type GitOauthProvider = 'github' | 'gitlab' | 'bitbucket'; + export interface IPatch { op: string; path: string; diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/helpers/retryableExec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/helpers/retryableExec.ts index 00d44294c..d31d3bb83 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/helpers/retryableExec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/helpers/retryableExec.ts @@ -11,6 +11,7 @@ */ import { delay } from '../../../services/helpers'; +import { helpers } from '@eclipse-che/common'; export async function retryableExec(callback: () => Promise, maxAttempt = 5): Promise { let error: unknown; @@ -19,6 +20,9 @@ export async function retryableExec(callback: () => Promise, maxAttempt = return await callback(); } catch (e) { error = e; + if (helpers.errors.isKubeClientError(error) && error.statusCode === 404) { + return Promise.reject(error); + } console.error(e); } await delay(1000); diff --git a/packages/dashboard-backend/src/localRun/hooks/authorizationHooks.ts b/packages/dashboard-backend/src/localRun/hooks/authorizationHooks.ts index 27ff730c5..afb34b0b7 100644 --- a/packages/dashboard-backend/src/localRun/hooks/authorizationHooks.ts +++ b/packages/dashboard-backend/src/localRun/hooks/authorizationHooks.ts @@ -15,7 +15,7 @@ import { FastifyInstance } from 'fastify'; export function addAuthorizationHooks(server: FastifyInstance) { server.addHook('onResponse', (request, reply, done) => { if ( - (request.url.startsWith('/api/') || request.url.startsWith('/dashboard/api/')) && + request.url.startsWith('/dashboard/api/') && request.method === 'GET' && reply.statusCode === 401 ) { diff --git a/packages/dashboard-frontend/package.json b/packages/dashboard-frontend/package.json index 0473b3d01..504a04281 100644 --- a/packages/dashboard-frontend/package.json +++ b/packages/dashboard-frontend/package.json @@ -36,7 +36,7 @@ "@eclipse-che/che-code-devworkspace-handler": "1.74.0-dev-e701cae", "@eclipse-che/che-theia-devworkspace-handler": "0.0.1-1667484092", "@eclipse-che/devfile-converter": "0.0.1-d624e3e", - "@eclipse-che/workspace-client": "0.0.1-1671793076", + "@eclipse-che/workspace-client": "0.0.1-1672830275", "@patternfly/react-core": "4.120.0", "@patternfly/react-icons": "^4.3.5", "@patternfly/react-table": "^4.5.7", diff --git a/packages/dashboard-frontend/src/pages/GetStarted/__tests__/devfileMetadata.json b/packages/dashboard-frontend/src/pages/GetStarted/__tests__/devfileMetadata.json index ab266018c..a770f86e6 100644 --- a/packages/dashboard-frontend/src/pages/GetStarted/__tests__/devfileMetadata.json +++ b/packages/dashboard-frontend/src/pages/GetStarted/__tests__/devfileMetadata.json @@ -20,14 +20,7 @@ { "displayName": "Java with Spring Boot and MongoDB", "description": "Java stack with OpenJDK 8, MongoDB and Spring Boot Guestbook demo application", - "tags": [ - "Community", - "Java", - "OpenJDK", - "Maven", - "Spring Boot", - "MongoDB" - ], + "tags": ["Community", "Java", "OpenJDK", "Maven", "Spring Boot", "MongoDB"], "icon": "/images/java.svg", "links": { "v2": "https://github.com/che-samples/java-guestbook/tree/devfilev2" @@ -36,14 +29,7 @@ { "displayName": "Java Lombok", "description": "Java Stack with Lombok 1.18.18, OpenJDK 11 and Maven 3.6.0", - "tags": [ - "Community", - "Java", - "OpenJDK", - "Maven", - "Debian", - "Lombok" - ], + "tags": ["Community", "Java", "OpenJDK", "Maven", "Debian", "Lombok"], "icon": "/images/lombok.svg", "links": { "v2": "https://github.com/che-samples/lombok-project-sample/tree/devfilev2" @@ -52,13 +38,7 @@ { "displayName": "Scala", "description": "Scala Stack with OpenJDK 11 and sbt 1.x", - "tags": [ - "Community", - "Scala", - "OpenJDK", - "sbt", - "Debian" - ], + "tags": ["Community", "Scala", "OpenJDK", "sbt", "Debian"], "icon": "/images/scala.svg", "links": { "v2": "https://github.com/che-samples/scala-sbt/tree/devfilev2" @@ -67,13 +47,7 @@ { "displayName": "ASP.NET Core Web Application", "description": "Stack for developing ASP.NET Core Web Application", - "tags": [ - "Community", - "Debian", - "Dotnet", - "C#", - "ASP.NET" - ], + "tags": ["Community", "Debian", "Dotnet", "C#", "ASP.NET"], "icon": "/images/dotnetcore.svg", "links": { "v2": "https://github.com/che-samples/aspnetcore-realworld-example-app/tree/devfilev2" @@ -82,13 +56,7 @@ { "displayName": "Node.js React Web Application", "description": "Stack for developing Node.js React Web Application", - "tags": [ - "Community", - "Node.js", - "React", - "Redux", - "RealWorld" - ], + "tags": ["Community", "Node.js", "React", "Redux", "RealWorld"], "icon": "/images/nodejs.svg", "links": { "v2": "https://github.com/che-samples/nodejs-react-redux/tree/devfilev2" @@ -97,12 +65,7 @@ { "displayName": "Node.js Angular Web Application", "description": "Stack for developing Node.js Angular Web Application", - "tags": [ - "Community", - "Node.js", - "Angular", - "Alpine" - ], + "tags": ["Community", "Node.js", "Angular", "Alpine"], "icon": "/images/angular.svg", "links": { "v2": "https://github.com/che-samples/nodejs-angular/tree/devfilev2" @@ -111,15 +74,7 @@ { "displayName": "PHP Symfony", "description": "PHP Stack with Symfony Demo Application https://symfony.com/", - "tags": [ - "Community", - "PHP", - "Apache", - "MySQL", - "Symfony", - "Debian", - "Centos" - ], + "tags": ["Community", "PHP", "Apache", "MySQL", "Symfony", "Debian", "Centos"], "icon": "/images/php.svg", "links": { "v2": "https://github.com/che-samples/php-symfony/tree/devfilev2" @@ -128,14 +83,7 @@ { "displayName": "Quarkus REST API", "description": "Quarkus stack with a default REST endpoint application sample", - "tags": [ - "Community", - "Java", - "Quarkus", - "OpenJDK", - "Maven", - "Debian" - ], + "tags": ["Community", "Java", "Quarkus", "OpenJDK", "Maven", "Debian"], "icon": "/images/quarkus.svg", "links": { "v2": "https://github.com/che-samples/quarkus-quickstarts/tree/devfilev2" @@ -144,12 +92,7 @@ { "displayName": "Apache Camel K", "description": "Stack with tooling ready to develop Integration projects with Apache Camel K", - "tags": [ - "Community", - "Apache Camel K", - "Red Hat Fuse", - "Integration" - ], + "tags": ["Community", "Apache Camel K", "Red Hat Fuse", "Integration"], "icon": "/images/camelk.svg", "links": { "v2": "https://github.com/che-samples/apache-camel-k/tree/devfilev2" @@ -158,12 +101,7 @@ { "displayName": "Node.js Express Web Application", "description": "Stack with Node.js 10", - "tags": [ - "Community", - "Node.js", - "Express", - "ubi8" - ], + "tags": ["Community", "Node.js", "Express", "ubi8"], "icon": "/images/nodejs.svg", "links": { "v2": "https://github.com/che-samples/web-nodejs-sample/tree/devfilev2" @@ -172,13 +110,7 @@ { "displayName": "Node.js Web Application based on Yarn", "description": "Stack for developing Node.js Web Application based on Yarn", - "tags": [ - "Community", - "Node.js", - "Alpine", - "Yarn", - "React" - ], + "tags": ["Community", "Node.js", "Alpine", "Yarn", "React"], "icon": "/images/nodejs.svg", "links": { "v2": "https://github.com/che-samples/react-web-app/tree/devfilev2" @@ -187,11 +119,7 @@ { "displayName": "Bash", "description": "Stack with environment ready to develop bash scripts.", - "tags": [ - "Community", - "Bash", - "Shell" - ], + "tags": ["Community", "Bash", "Shell"], "icon": "/images/che.svg", "links": { "v2": "https://github.com/che-samples/bash/tree/devfilev2" @@ -200,12 +128,7 @@ { "displayName": "Python Django", "description": "Python Stack with Python 3.8 and Django application", - "tags": [ - "Community", - "Centos", - "Python", - "pip" - ], + "tags": ["Community", "Centos", "Python", "pip"], "icon": "/images/python.svg", "links": { "v2": "https://github.com/che-samples/django-realworld-example-app/tree/devfile2" @@ -214,13 +137,7 @@ { "displayName": "C/C++", "description": "Stack with C/C++ and Clang 8", - "tags": [ - "Community", - "C/C++", - "Clang", - "g++", - "GDB" - ], + "tags": ["Community", "C/C++", "Clang", "g++", "GDB"], "icon": "/images/cpp.svg", "links": { "v2": "https://github.com/che-samples/cpp-hello-world/tree/devfilev2" @@ -229,15 +146,7 @@ { "displayName": "Node.js MongoDB Web Application", "description": "Stack with NodeJS 10 and MongoDB 3.4", - "tags": [ - "Community", - "Node.js", - "Express", - "MongoDB", - "RealWorld", - "ubi8", - "Centos" - ], + "tags": ["Community", "Node.js", "Express", "MongoDB", "RealWorld", "ubi8", "Centos"], "icon": "/images/nodejs.svg", "links": { "v2": "https://github.com/che-samples/nodejs-mongodb-sample/tree/devfilev2" @@ -246,10 +155,7 @@ { "displayName": "Rust", "description": "Rust Stack with Rust 1.57", - "tags": [ - "Community", - "Rust" - ], + "tags": ["Community", "Rust"], "icon": "/images/rust.svg", "links": { "v2": "https://github.com/che-samples/helloworld-rust/tree/devfilev2" @@ -258,13 +164,7 @@ { "displayName": "Java Spring Boot", "description": "Java stack with OpenJDK 11 and Spring Boot Petclinic demo application", - "tags": [ - "Community", - "Java", - "OpenJDK", - "Maven", - "Spring Boot" - ], + "tags": ["Community", "Java", "OpenJDK", "Maven", "Spring Boot"], "icon": "/images/springboot.svg", "links": { "v2": "https://github.com/che-samples/java-spring-petclinic/tree/devfilev2" diff --git a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap deleted file mode 100644 index 76346f384..000000000 --- a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap +++ /dev/null @@ -1,217 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UserAccount page should correctly render the component which contains profile data 1`] = ` -
-
-
-

- Account -

-

- This is where you can view and edit your account information for Product name. -

-
-
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
-
-
-`; - -exports[`UserAccount page should correctly render the component without profile data 1`] = ` -
-
-
-

- Account -

-

- This is where you can view and edit your account information for test. -

-
-
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
-
-
-`; diff --git a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/UserAccount/__tests__/index.spec.tsx deleted file mode 100644 index 55ab69de1..000000000 --- a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/index.spec.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2018-2021 Red Hat, Inc. - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - */ - -import { createHashHistory } from 'history'; -import React from 'react'; -import { Provider } from 'react-redux'; -import renderer from 'react-test-renderer'; -import { Store } from 'redux'; -import { UserAccount } from '..'; -import { FakeStoreBuilder } from '../../../store/__mocks__/storeBuilder'; -import { BrandingData } from '../../../services/bootstrap/branding.constant'; -import { selectBranding } from '../../../store/Branding/selectors'; -import { selectUserProfile } from '../../../store/UserProfile/selectors'; - -describe('UserAccount page', () => { - const history = createHashHistory(); - - const getComponent = (store: Store): React.ReactElement => { - const state = store.getState(); - const branding = selectBranding(state); - const userProfile = selectUserProfile(state); - - return ( - - - - ); - }; - - it('should correctly render the component without profile data', () => { - const store = new FakeStoreBuilder() - .withBranding({ - name: 'test', - } as BrandingData) - .build(); - const component = getComponent(store); - const json = renderer.create(component).toJSON(); - - expect(json).toMatchSnapshot(); - }); - - it('should correctly render the component which contains profile data', () => { - const store = new FakeStoreBuilder() - .withBranding({ - name: 'Product name', - } as BrandingData) - .withUserProfile({ - email: 'johndoe@test.com', - username: 'john-doe', - }) - .build(); - const component = getComponent(store); - const json = renderer.create(component).toJSON(); - - expect(json).toMatchSnapshot(); - }); -}); diff --git a/packages/dashboard-frontend/src/pages/UserAccount/index.tsx b/packages/dashboard-frontend/src/pages/UserAccount/index.tsx deleted file mode 100644 index f1926f0cf..000000000 --- a/packages/dashboard-frontend/src/pages/UserAccount/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2018-2021 Red Hat, Inc. - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - */ - -import { - Form, - FormGroup, - PageSection, - PageSectionVariants, - Stack, - StackItem, - Text, - TextInput, - TextVariants, - Title, -} from '@patternfly/react-core'; -import { History } from 'history'; -import React from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import Head from '../../components/Head'; -import { AppState } from '../../store'; -import { selectBranding } from '../../store/Branding/selectors'; -import { selectUserProfile } from '../../store/UserProfile/selectors'; - -type Props = { - history: History; -} & MappedProps; - -export class UserAccount extends React.PureComponent { - render(): React.ReactNode { - const productName = this.props.branding.name; - const { userProfile } = this.props; - - return ( - - - - - - Account - - {`This is where you can view and edit your account information for ${productName}.`} - - - -
- - - - - - -
-
-
-
-
- ); - } -} - -const mapStateToProps = (state: AppState) => ({ - userProfile: selectUserProfile(state), - branding: selectBranding(state), -}); - -const connector = connect(mapStateToProps); - -type MappedProps = ConnectedProps; -export default connector(UserAccount); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/Modals/DeleteRegistriesModal.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/Modals/DeleteRegistriesModal.tsx index 77422a81d..a2b7fd339 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/Modals/DeleteRegistriesModal.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/Modals/DeleteRegistriesModal.tsx @@ -103,7 +103,11 @@ export default class DeleteRegistriesModal extends React.PureComponent Delete - diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/Modals/EditRegistryModal.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/Modals/EditRegistryModal.tsx index f69c4b9ac..ac39c5522 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/Modals/EditRegistryModal.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/Modals/EditRegistryModal.tsx @@ -164,7 +164,7 @@ export default class EditRegistryModal extends React.PureComponent > {isEditMode ? 'Save' : 'Add'} - diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/__tests__/index.spec.tsx index eff990b86..fba9c66cb 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/__tests__/index.spec.tsx @@ -11,7 +11,6 @@ */ import userEvent from '@testing-library/user-event'; -import { createHashHistory } from 'history'; import { Provider } from 'react-redux'; import React from 'react'; import { render, screen } from '@testing-library/react'; @@ -26,8 +25,6 @@ describe('ContainerRegistries', () => { const mockRequestCredentials = jest.fn(); const mockUpdateCredentials = jest.fn(); - const history = createHashHistory(); - const getComponent = (store: Store): React.ReactElement => { const state = store.getState(); const registries = selectRegistries(state); @@ -35,7 +32,6 @@ describe('ContainerRegistries', () => { return ( { @@ -67,7 +60,6 @@ export class ContainerRegistriesTab extends React.PureComponent { registries, currentRegistry: { url: '', password: '', username: '' }, selectedItems: [], - activeTabKey: CONTAINER_REGISTRIES_TAB_KEY, currentRegistryIndex: -1, isEditModalOpen: false, isDeleteModalOpen: false, @@ -120,11 +112,6 @@ export class ContainerRegistriesTab extends React.PureComponent { this.appAlerts.showAlert(alert); } - private handleTabClick(activeTabKey: string): void { - this.props.history.push(`${ROUTE.USER_PREFERENCES}?tab=${activeTabKey}`); - this.setState({ activeTabKey }); - } - private buildRegistryRow(registry: RegistryEntry): React.ReactNode[] { const { url, username } = registry; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..9552e1aeb --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`No git services component for empty state should render title correctly 1`] = ` +
+
+ +

+ No Git Services +

+
+
+`; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/index.spec.tsx new file mode 100644 index 000000000..d6c5c9257 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/index.spec.tsx @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import EmptyState from '../index'; + +describe('No git services component for empty state', () => { + it('should render title correctly', () => { + const element = ; + + expect(renderer.create(element).toJSON()).toMatchSnapshot(); + }); +}); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/index.tsx new file mode 100644 index 000000000..4a5c4b240 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/index.tsx @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import React from 'react'; +import { Title, EmptyState, EmptyStateVariant, EmptyStateIcon } from '@patternfly/react-core'; +import { RegistryIcon } from '@patternfly/react-icons'; + +type Props = { + text: string; +}; + +export default class Empty extends React.PureComponent { + public render(): React.ReactElement { + return ( + + + + {this.props.text} + + + ); + } +} diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/RevokeGitServicesModal.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/RevokeGitServicesModal.tsx new file mode 100644 index 000000000..8c491bbb8 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/RevokeGitServicesModal.tsx @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import React from 'react'; +import { + Button, + ButtonVariant, + ModalVariant, + Modal, + TextContent, + Text, + Checkbox, +} from '@patternfly/react-core'; +import { api } from '@eclipse-che/common'; +import { providersMap } from '../index'; + +type Props = { + gitOauth?: api.GitOauthProvider; + selectedItems: api.GitOauthProvider[]; + isOpen: boolean; + onRevoke: () => void; + onCancel: () => void; +}; +type State = { + warningInfoCheck: boolean; +}; + +export default class RevokeGitServicesModal extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { warningInfoCheck: false }; + } + + public componentDidUpdate(prevProps: Props): void { + if (prevProps.isOpen === this.props.isOpen && this.props.isOpen) { + return; + } + + this.setState({ warningInfoCheck: false }); + } + + private getRevokeModalContent(): React.ReactNode { + const { gitOauth, selectedItems } = this.props; + + let text = 'Would you like to revoke '; + + if (gitOauth) { + text += `git service '${providersMap[gitOauth]}'`; + } else { + if (selectedItems.length === 1) { + text += `git service '${providersMap[selectedItems[0]]}'`; + } else { + text += `${selectedItems.length} git services`; + } + } + text += '?'; + + return ( + + {text} + { + this.setState({ warningInfoCheck: !this.state.warningInfoCheck }); + }} + id="revoke-warning-info-check" + label="I understand, this operation cannot be reverted." + /> + + ); + } + + public render(): React.ReactElement { + const { isOpen, onCancel, onRevoke, gitOauth } = this.props; + const { warningInfoCheck } = this.state; + + return ( + onCancel()} + aria-label="warning-info" + footer={ + + + + + } + > + {this.getRevokeModalContent()} + + ); + } +} diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/RevokeRegistriesModal.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/RevokeRegistriesModal.spec.tsx new file mode 100644 index 000000000..2f40ecdd5 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/RevokeRegistriesModal.spec.tsx @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import renderer from 'react-test-renderer'; +import RevokeRegistriesModal from '../RevokeGitServicesModal'; +import { GitOauthProvider } from '@eclipse-che/common/lib/dto/api'; +import { api } from '@eclipse-che/common'; + +describe('Revoke Registries Modal', () => { + const mockOnRevoke = jest.fn(); + const mockOnCancel = jest.fn(); + + function getComponent( + isRevokeModalOpen: boolean, + selectedItems: GitOauthProvider[], + gitOauth?: api.GitOauthProvider, + ): React.ReactElement { + return ( + + ); + } + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should correctly render the closed modal component', () => { + const component = getComponent(false, []); + + const json = renderer.create(component).toJSON(); + + expect(json).toMatchSnapshot(); + }); + + it('should correctly render the component', () => { + const component = getComponent(true, [], 'gitlab'); + render(component); + + const text = screen.queryByText("Would you like to revoke git service 'GitLab'?"); + expect(text).toBeTruthy(); + + const checkbox = screen.queryByTestId('warning-info-checkbox'); + expect(checkbox).toBeTruthy(); + + const deleteButton = screen.queryByTestId('revoke-button'); + expect(deleteButton).toBeTruthy(); + + const cancelButton = screen.queryByTestId('cancel-button'); + expect(cancelButton).toBeTruthy(); + }); + + it('should correctly render the component with one selected git service', () => { + const component = getComponent(true, ['github']); + render(component); + + const label = screen.queryByText("Would you like to revoke git service 'GitHub'?"); + expect(label).toBeTruthy(); + }); + + it('should correctly render the component with two selected git services', () => { + const component = getComponent(true, ['github', 'gitlab']); + render(component); + + const label = screen.queryByText('Would you like to revoke 2 git services?'); + expect(label).toBeTruthy(); + }); + + it('should fire onRevoke event', () => { + const component = getComponent(true, ['github']); + render(component); + + const revokeButton = screen.getByTestId('revoke-button'); + expect(revokeButton).toBeDisabled(); + + const checkbox = screen.getByTestId('warning-info-checkbox'); + userEvent.click(checkbox); + + expect(revokeButton).toBeEnabled(); + + userEvent.click(revokeButton); + + expect(mockOnRevoke).toHaveBeenCalledWith(); + }); + + it('should fire onCancel event', () => { + const component = getComponent(true, ['github', 'gitlab']); + render(component); + + const deleteButton = screen.getByTestId('revoke-button'); + expect(deleteButton).toBeDisabled(); + + const cancelButton = screen.getByTestId('cancel-button'); + userEvent.click(cancelButton); + + expect(mockOnCancel).toBeCalled(); + }); +}); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/__snapshots__/RevokeRegistriesModal.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/__snapshots__/RevokeRegistriesModal.spec.tsx.snap new file mode 100644 index 000000000..19a011373 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/__snapshots__/RevokeRegistriesModal.spec.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Revoke Registries Modal should correctly render the closed modal component 1`] = `null`; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/__mocks__/gitOauthRowBuilder.ts b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/__mocks__/gitOauthRowBuilder.ts new file mode 100644 index 000000000..86d81ce0a --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/__mocks__/gitOauthRowBuilder.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { IGitOauth } from '../../../../../store/GitOauthConfig/types'; +import { api } from '@eclipse-che/common'; + +export class FakeGitOauthBuilder { + private gitOauth: IGitOauth = { name: 'github', endpointUrl: '-' }; + + public withName(name: api.GitOauthProvider): FakeGitOauthBuilder { + this.gitOauth.name = name; + return this; + } + + public withEndpointUrl(endpointUrl: string): FakeGitOauthBuilder { + this.gitOauth.endpointUrl = endpointUrl; + return this; + } + + public build(): IGitOauth { + return this.gitOauth; + } +} diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/__snapshots__/index.spec.tsx.snap new file mode 100644 index 000000000..1f9d0bb6b --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,375 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GitServices should correctly render the component which contains two git services 1`] = ` +Array [ + + + , +
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
, +] +`; + +exports[`GitServices should correctly render the component without sit services 1`] = ` +Array [ + + + , +
+
+
+ +

+ No Git Services +

+
+
+
, +] +`; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/index.spec.tsx new file mode 100644 index 000000000..b94bb11c5 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/index.spec.tsx @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import userEvent from '@testing-library/user-event'; +import { Provider } from 'react-redux'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import renderer from 'react-test-renderer'; +import { Store } from 'redux'; +import { GitServicesTab } from '..'; +import { FakeGitOauthBuilder } from './__mocks__/gitOauthRowBuilder'; +import { FakeStoreBuilder } from '../../../../store/__mocks__/storeBuilder'; +import { selectIsLoading, selectGitOauth } from '../../../../store/GitOauthConfig/selectors'; + +describe('GitServices', () => { + const mockRevokeOauth = jest.fn(); + const requestGitOauthConfig = jest.fn(); + + const getComponent = (store: Store): React.ReactElement => { + const state = store.getState(); + const gitOauth = selectGitOauth(state); + const isLoading = selectIsLoading(state); + return ( + + + + ); + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should correctly render the component without sit services', () => { + const component = getComponent(new FakeStoreBuilder().build()); + render(component); + + const emptyStateText = screen.queryByText('No Git Services'); + expect(emptyStateText).toBeTruthy(); + + const json = renderer.create(component).toJSON(); + + expect(json).toMatchSnapshot(); + }); + + it('should correctly render the component which contains two git services', () => { + const component = getComponent( + new FakeStoreBuilder() + .withGitOauthConfig([ + new FakeGitOauthBuilder() + .withName('github') + .withEndpointUrl('https://github.com') + .build(), + new FakeGitOauthBuilder() + .withName('gitlab') + .withEndpointUrl('https://gitlab.com') + .build(), + ]) + .build(), + ); + render(component); + + const emptyStateText = screen.queryByText('No Git Services'); + expect(emptyStateText).not.toBeTruthy(); + + const json = renderer.create(component).toJSON(); + + expect(json).toMatchSnapshot(); + }); + + it('should revoke a git service', () => { + const component = getComponent( + new FakeStoreBuilder() + .withGitOauthConfig([ + new FakeGitOauthBuilder() + .withName('github') + .withEndpointUrl('https://github.com') + .build(), + ]) + .build(), + ); + render(component); + + const menuButton = screen.getByLabelText('Actions'); + userEvent.click(menuButton); + + const revokeItem = screen.getByRole('menuitem', { name: /Revoke/i }); + userEvent.click(revokeItem); + + const text = screen.findByText("Would you like to revoke git service 'GitHub'?"); + expect(text).toBeTruthy(); + + const revokeButton = screen.getByTestId('revoke-button'); + expect(revokeButton).toBeDisabled(); + + const checkbox = screen.getByTestId('warning-info-checkbox'); + userEvent.click(checkbox); + expect(revokeButton).toBeEnabled(); + + userEvent.click(revokeButton); + expect(mockRevokeOauth).toBeCalledWith('github'); + }); +}); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx new file mode 100644 index 000000000..6d2c79c55 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { + AlertVariant, + Button, + ButtonVariant, + PageSection, + Toolbar, + ToolbarContent, + ToolbarItem, +} from '@patternfly/react-core'; +import { Table, TableBody, TableHeader } from '@patternfly/react-table'; +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import ProgressIndicator from '../../../components/Progress'; +import { lazyInject } from '../../../inversify.config'; +import { AppAlerts } from '../../../services/alerts/appAlerts'; +import { AlertItem } from '../../../services/helpers/types'; +import { AppState } from '../../../store'; +import { selectIsLoading, selectGitOauth } from '../../../store/GitOauthConfig/selectors'; +import EmptyState from './EmptyState'; +import RevokeGitServicesModal from './Modals/RevokeGitServicesModal'; +import { api, helpers } from '@eclipse-che/common'; +import * as GitOauthConfig from '../../../store/GitOauthConfig'; +import { isEqual } from 'lodash'; + +export const providersMap = { + github: 'GitHub', + gitlab: 'GitLab', + bitbucket: 'Bitbucket', +}; + +type Props = MappedProps; + +type State = { + selectedItems: api.GitOauthProvider[]; + gitOauth: { name: api.GitOauthProvider; endpointUrl: string }[]; + currentGitOauth: api.GitOauthProvider | undefined; + currentGitOauthIndex: number; + isRevokeModalOpen: boolean; + isEditModalOpen: boolean; +}; + +export class GitServicesTab extends React.PureComponent { + @lazyInject(AppAlerts) + private readonly appAlerts: AppAlerts; + + constructor(props: Props) { + super(props); + + const gitOauth = this.props.gitOauth; + + this.state = { + gitOauth, + currentGitOauth: undefined, + selectedItems: [], + currentGitOauthIndex: -1, + isEditModalOpen: false, + isRevokeModalOpen: false, + }; + } + + private onChangeSelection(isSelected: boolean, rowIndex: number) { + const { gitOauth } = this.state; + if (rowIndex === -1) { + const selectedItems = isSelected ? gitOauth.map(val => val.name) : []; + this.setState({ selectedItems }); + } else { + const selectedItem = gitOauth[rowIndex]?.name; + this.setState((prevState: State) => { + return { + selectedItems: isSelected + ? [...prevState.selectedItems, selectedItem] + : prevState.selectedItems.filter(item => item !== selectedItem), + }; + }); + } + } + + public async componentDidMount(): Promise { + const { isLoading, requestGitOauthConfig } = this.props; + if (!isLoading) { + requestGitOauthConfig(); + } + } + + public componentDidUpdate(prevProps: Props, prevState: State): void { + const gitOauth = this.props.gitOauth; + if (!isEqual(prevProps.gitOauth, gitOauth)) { + const selectedItems: api.GitOauthProvider[] = []; + this.state.selectedItems.forEach(selectedItem => { + if (gitOauth.map(val => val.name).indexOf(selectedItem) !== -1) { + selectedItems.push(selectedItem); + } + }); + this.setState({ gitOauth, selectedItems }); + } + if (prevState.currentGitOauthIndex !== this.state.currentGitOauthIndex) { + const currentGitOauth = gitOauth[this.state.currentGitOauthIndex]?.name; + this.setState({ currentGitOauth }); + } + } + + private showAlert(alert: AlertItem): void { + this.appAlerts.showAlert(alert); + } + + private buildGitOauthRow(gitOauth: api.GitOauthProvider, server: string): React.ReactNode[] { + const oauthRow: React.ReactNode[] = [{providersMap[gitOauth]}]; + + if (/^http[s]?:\/\/.*/.test(server)) { + oauthRow.push( + + + {server} + + , + ); + } else { + oauthRow.push({server}); + } + + return oauthRow; + } + + private showOnRevokeGitOauthModal(rowIndex: number): void { + this.setState({ currentGitOauthIndex: rowIndex, isRevokeModalOpen: true }); + } + + private async revokeOauth(gitOauth: api.GitOauthProvider): Promise { + try { + await this.props.revokeOauth(gitOauth); + this.showAlert({ + key: 'revoke-github', + variant: AlertVariant.success, + title: `Git oauth '${gitOauth}' successfully deleted.`, + }); + } catch (e) { + this.showAlert({ + key: 'revoke-fail', + variant: AlertVariant.danger, + title: helpers.errors.getMessage(e), + }); + } + } + + private async handleRevoke(gitOauth?: api.GitOauthProvider): Promise { + this.setState({ isRevokeModalOpen: false, currentGitOauthIndex: -1 }); + if (gitOauth === undefined) { + for (const selectedItem of this.state.selectedItems) { + await this.revokeOauth(selectedItem); + } + this.setState({ selectedItems: [] }); + } else { + await this.revokeOauth(gitOauth); + } + } + + private setRevokeModalStatus(isRevokeModalOpen: boolean): void { + if (this.state.isRevokeModalOpen === isRevokeModalOpen) { + return; + } + this.setState({ isRevokeModalOpen }); + } + + private handleModalShow(): void { + this.setState({ currentGitOauthIndex: -1, isRevokeModalOpen: true }); + } + + render(): React.ReactNode { + const { isLoading } = this.props; + const { isRevokeModalOpen, currentGitOauth, selectedItems } = this.state; + const columns = ['Name', 'Server']; + const rows = + this.state.gitOauth.map(provider => ({ + cells: this.buildGitOauthRow(provider.name, provider.endpointUrl), + selected: selectedItems.includes(provider.name), + })) || []; + const actions = [ + { + title: 'Revoke', + onClick: (event, rowIndex) => this.showOnRevokeGitOauthModal(rowIndex), + }, + ]; + + return ( + + + + {rows.length === 0 ? ( + + ) : ( + + this.setRevokeModalStatus(false)} + onRevoke={() => this.handleRevoke(currentGitOauth)} + isOpen={isRevokeModalOpen} + gitOauth={currentGitOauth} + /> + + + + + + + + { + this.onChangeSelection(isSelected, rowIndex); + }} + canSelectAll={true} + aria-label="Git services" + variant="compact" + > + + +
+
+ )} +
+
+ ); + } +} + +const mapStateToProps = (state: AppState) => ({ + gitOauth: selectGitOauth(state), + isLoading: selectIsLoading(state), +}); + +const connector = connect(mapStateToProps, GitOauthConfig.actionCreators); + +type MappedProps = ConnectedProps; +export default connector(GitServicesTab); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx index 7d2ec3a96..4665166be 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx @@ -18,9 +18,11 @@ import Head from '../../components/Head'; import { UserPreferencesTab } from '../../services/helpers/types'; import { ROUTE } from '../../Routes/routes'; import { AppState } from '../../store'; -import { selectIsLoading, selectRegistries } from '../../store/DockerConfig/selectors'; +import { selectIsLoading } from '../../store/GitOauthConfig/selectors'; +import { actionCreators } from '../../store/GitOauthConfig'; -const ContanerRegistryList = React.lazy(() => import('./ContainerRegistriesTab')); +const ContainerRegistryList = React.lazy(() => import('./ContainerRegistriesTab')); +const GitServicesTab = React.lazy(() => import('./GitServicesTab')); type Props = { history: History; @@ -63,11 +65,16 @@ export class UserPreferences extends React.PureComponent { this.setState({ activeTabKey: activeTabKey as UserPreferencesTab, }); + + if (activeTabKey === 'git-services') { + if (!this.props.isLoading) { + this.props.requestGitOauthConfig(); + } + } } render(): React.ReactNode { const { activeTabKey } = this.state; - const containerRegistriesTab: UserPreferencesTab = 'container-registries'; return ( @@ -83,10 +90,13 @@ export class UserPreferences extends React.PureComponent { > - + + + + @@ -95,11 +105,10 @@ export class UserPreferences extends React.PureComponent { } const mapStateToProps = (state: AppState) => ({ - registries: selectRegistries(state), isLoading: selectIsLoading(state), }); -const connector = connect(mapStateToProps); +const connector = connect(mapStateToProps, actionCreators); type MappedProps = ConnectedProps; export default connector(UserPreferences); diff --git a/packages/dashboard-frontend/src/services/dashboard-backend-client/clusterConfig.ts b/packages/dashboard-frontend/src/services/dashboard-backend-client/clusterConfigApi.ts similarity index 100% rename from packages/dashboard-frontend/src/services/dashboard-backend-client/clusterConfig.ts rename to packages/dashboard-frontend/src/services/dashboard-backend-client/clusterConfigApi.ts diff --git a/packages/dashboard-frontend/src/services/dashboard-backend-client/clusterInfo.ts b/packages/dashboard-frontend/src/services/dashboard-backend-client/clusterInfoApi.ts similarity index 100% rename from packages/dashboard-frontend/src/services/dashboard-backend-client/clusterInfo.ts rename to packages/dashboard-frontend/src/services/dashboard-backend-client/clusterInfoApi.ts diff --git a/packages/dashboard-frontend/src/services/helpers/types.ts b/packages/dashboard-frontend/src/services/helpers/types.ts index 433260ea4..7c4084966 100644 --- a/packages/dashboard-frontend/src/services/helpers/types.ts +++ b/packages/dashboard-frontend/src/services/helpers/types.ts @@ -101,6 +101,6 @@ export enum WorkspaceAction { EDIT_WORKSPACE = 'Edit Workspace', } -export type UserPreferencesTab = 'container-registries'; +export type UserPreferencesTab = 'container-registries' | 'git-services'; export type WorkspacesLogs = Map; diff --git a/packages/dashboard-frontend/src/store/ClusterConfig/index.ts b/packages/dashboard-frontend/src/store/ClusterConfig/index.ts index ad9f03c97..ba6176942 100644 --- a/packages/dashboard-frontend/src/store/ClusterConfig/index.ts +++ b/packages/dashboard-frontend/src/store/ClusterConfig/index.ts @@ -15,7 +15,7 @@ import common, { ClusterConfig } from '@eclipse-che/common'; import { AppThunk } from '..'; import { createObject } from '../helpers'; import * as BannerAlertStore from '../BannerAlert'; -import { fetchClusterConfig } from '../../services/dashboard-backend-client/clusterConfig'; +import { fetchClusterConfig } from '../../services/dashboard-backend-client/clusterConfigApi'; import { AddBannerAction } from '../BannerAlert'; import { AUTHORIZED, SanityCheckAction } from '../sanityCheckMiddleware'; diff --git a/packages/dashboard-frontend/src/store/ClusterInfo/index.ts b/packages/dashboard-frontend/src/store/ClusterInfo/index.ts index bce180610..7ce830f9b 100644 --- a/packages/dashboard-frontend/src/store/ClusterInfo/index.ts +++ b/packages/dashboard-frontend/src/store/ClusterInfo/index.ts @@ -14,7 +14,7 @@ import { Action, Reducer } from 'redux'; import common, { ClusterInfo } from '@eclipse-che/common'; import { AppThunk } from '..'; import { createObject } from '../helpers'; -import { fetchClusterInfo } from '../../services/dashboard-backend-client/clusterInfo'; +import { fetchClusterInfo } from '../../services/dashboard-backend-client/clusterInfoApi'; import { AUTHORIZED, SanityCheckAction } from '../sanityCheckMiddleware'; export interface State { diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts new file mode 100644 index 000000000..daa0aad76 --- /dev/null +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { Action, Reducer } from 'redux'; +import common, { api } from '@eclipse-che/common'; +import { AppThunk } from '..'; +import { createObject } from '../helpers'; +import { AUTHORIZED } from '../sanityCheckMiddleware'; +import { container } from '../../inversify.config'; +import { CheWorkspaceClient } from '../../services/workspace-client/cheworkspace/cheWorkspaceClient'; +import { IGitOauth } from './types'; + +export interface State { + isLoading: boolean; + gitOauth: IGitOauth[]; + error: string | undefined; +} + +const cheWorkspaceClient = container.get(CheWorkspaceClient); + +export enum Type { + REQUEST_GIT_OAUTH_CONFIG = 'REQUEST_GIT_OAUTH_CONFIG', + DELETE_OAUTH = 'DELETE_OAUTH', + RECEIVE_GIT_OAUTH_CONFIG = 'RECEIVE_GIT_OAUTH_CONFIG', + RECEIVE_GIT_OAUTH_CONFIG_ERROR = 'RECEIVE_GIT_OAUTH_CONFIG_ERROR', +} + +export interface RequestGitOauthConfigAction extends Action { + type: Type.REQUEST_GIT_OAUTH_CONFIG; +} + +export interface DeleteOauthAction extends Action { + type: Type.DELETE_OAUTH; + provider: api.GitOauthProvider; +} + +export interface ReceiveGitOauthConfigAction extends Action { + type: Type.RECEIVE_GIT_OAUTH_CONFIG; + gitOauth: IGitOauth[]; +} + +export interface ReceivedGitOauthConfigErrorAction extends Action { + type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR; + error: string; +} + +export type KnownAction = + | RequestGitOauthConfigAction + | DeleteOauthAction + | ReceiveGitOauthConfigAction + | ReceivedGitOauthConfigErrorAction; + +export type ActionCreators = { + requestGitOauthConfig: () => AppThunk>; + revokeOauth: (oauthProvider: api.GitOauthProvider) => AppThunk>; +}; + +export const actionCreators: ActionCreators = { + requestGitOauthConfig: + (): AppThunk> => + async (dispatch): Promise => { + await dispatch({ + type: Type.REQUEST_GIT_OAUTH_CONFIG, + }); + const gitOauth: IGitOauth[] = []; + try { + const oAuthProviders = await cheWorkspaceClient.restApiClient.getOAuthProviders(); + for (const { name, endpointUrl } of oAuthProviders) { + try { + await cheWorkspaceClient.restApiClient.getOAuthToken(name); + gitOauth.push({ + name: name as api.GitOauthProvider, + endpointUrl, + }); + } catch (e) { + // no-op + } + } + + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_CONFIG, + gitOauth, + }); + } catch (e) { + const errorMessage = common.helpers.errors.getMessage(e); + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR, + error: errorMessage, + }); + throw e; + } + }, + + revokeOauth: + (oauthProvider: api.GitOauthProvider): AppThunk> => + async (dispatch): Promise => { + await dispatch({ + type: Type.REQUEST_GIT_OAUTH_CONFIG, + check: AUTHORIZED, + }); + try { + await cheWorkspaceClient.restApiClient.deleteOAuthToken(oauthProvider); + dispatch({ + type: Type.DELETE_OAUTH, + provider: oauthProvider, + }); + } catch (e) { + const errorMessage = common.helpers.errors.getMessage(e); + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR, + error: errorMessage, + }); + throw errorMessage; + } + }, +}; + +const unloadedState: State = { + isLoading: false, + gitOauth: [], + error: undefined, +}; + +export const reducer: Reducer = ( + state: State | undefined, + incomingAction: Action, +): State => { + if (state === undefined) { + return unloadedState; + } + + const gitOauth = Object.assign({}, state.gitOauth); + + const action = incomingAction as KnownAction; + switch (action.type) { + case Type.REQUEST_GIT_OAUTH_CONFIG: + return createObject(state, { + isLoading: true, + error: undefined, + }); + case Type.RECEIVE_GIT_OAUTH_CONFIG: + return createObject(state, { + isLoading: false, + gitOauth: action.gitOauth, + }); + case Type.DELETE_OAUTH: + delete gitOauth[action.provider]; + return createObject(state, { + isLoading: false, + gitOauth, + }); + case Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR: + return createObject(state, { + isLoading: false, + error: action.error, + }); + default: + return state; + } +}; diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts new file mode 100644 index 000000000..9cb5c6058 --- /dev/null +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { createSelector } from 'reselect'; +import { AppState } from '..'; +import { State } from './index'; + +const selectState = (state: AppState) => state.gitOauthConfig; + +export const selectIsLoading = createSelector(selectState, state => { + return state.isLoading; +}); + +export const selectGitOauth = createSelector(selectState, (state: State) => { + return state.gitOauth; +}); + +export const selectError = createSelector(selectState, state => { + return state.error; +}); diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/types.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/types.ts new file mode 100644 index 000000000..f3d40666d --- /dev/null +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { api } from '@eclipse-che/common'; + +export interface IGitOauth { + name: api.GitOauthProvider; + endpointUrl: string; +} diff --git a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts index c322305cf..8978e5c88 100644 --- a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts +++ b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts @@ -26,6 +26,7 @@ import { State as PluginsState } from '../Plugins/chePlugins'; import { State as UserProfileState } from '../UserProfile'; import { State as WorkspacesState } from '../Workspaces/index'; import mockThunk from './thunk'; +import { IGitOauth } from '../GitOauthConfig/types'; export class FakeStoreBuilder { private state: AppState = { @@ -97,6 +98,11 @@ export class FakeStoreBuilder { isLoading: false, data: {}, } as BrandingState, + gitOauthConfig: { + isLoading: false, + gitOauth: [], + error: undefined, + }, devfileRegistries: { isLoading: false, devfiles: {}, @@ -139,6 +145,17 @@ export class FakeStoreBuilder { return this; } + public withGitOauthConfig( + gitOauth: IGitOauth[], + isLoading = false, + error?: string, + ): FakeStoreBuilder { + this.state.gitOauthConfig.gitOauth = gitOauth; + this.state.dockerConfig.isLoading = isLoading; + this.state.dockerConfig.error = error; + return this; + } + public withDockerConfig( registries: RegistryEntry[], isLoading = false, diff --git a/packages/dashboard-frontend/src/store/index.ts b/packages/dashboard-frontend/src/store/index.ts index 49f7ca82f..8564d39c9 100644 --- a/packages/dashboard-frontend/src/store/index.ts +++ b/packages/dashboard-frontend/src/store/index.ts @@ -28,6 +28,7 @@ import * as UserProfileStore from './UserProfile'; import * as WorkspacesStore from './Workspaces'; import * as DevWorkspacesStore from './Workspaces/devWorkspaces'; import * as WorkspacesSettingsStore from './Workspaces/Settings'; +import * as GitOauthConfigStore from './GitOauthConfig'; // the top-level state object export interface AppState { @@ -37,6 +38,7 @@ export interface AppState { clusterInfo: ClusterInfo.State; devWorkspaces: DevWorkspacesStore.State; devfileRegistries: DevfileRegistriesStore.State; + gitOauthConfig: GitOauthConfigStore.State; dockerConfig: DockerConfigStore.State; dwPlugins: DwPluginsStore.State; dwServerConfig: DwServerConfigStore.State; @@ -56,6 +58,7 @@ export const reducers = { clusterInfo: ClusterInfo.reducer, devWorkspaces: DevWorkspacesStore.reducer, devfileRegistries: DevfileRegistriesStore.reducer, + gitOauthConfig: GitOauthConfigStore.reducer, dockerConfig: DockerConfigStore.reducer, dwPlugins: DwPluginsStore.reducer, dwServerConfig: DwServerConfigStore.reducer, diff --git a/yarn.lock b/yarn.lock index 5395a591a..77bb0b233 100644 --- a/yarn.lock +++ b/yarn.lock @@ -381,10 +381,10 @@ jsonc-parser "^3.0.0" reflect-metadata "^0.1.13" -"@eclipse-che/workspace-client@0.0.1-1671793076": - version "0.0.1-1671793076" - resolved "https://registry.yarnpkg.com/@eclipse-che/workspace-client/-/workspace-client-0.0.1-1671793076.tgz#87bd373048762b12ce2353306cd1e4b357857d00" - integrity sha512-zmX5m0RcP15Z+kArofjAUKODbWWHK7M7CDMZ3Vadj2zD8P0K26doT3IJZRiJGsMxOB8Y6onGypnCGJrtJEiJTA== +"@eclipse-che/workspace-client@0.0.1-1672830275": + version "0.0.1-1672830275" + resolved "https://registry.yarnpkg.com/@eclipse-che/workspace-client/-/workspace-client-0.0.1-1672830275.tgz#7724fbe74fa8ee86a23f888e23d7031cb608787e" + integrity sha512-QgbLxTns7m/efbWZbFRqd9UxH4ELN5Np3JzYWzqN5i/R3cra/xuV9KwDedctarZ6iwnW9ptZFsfDcU5zedZKKQ== dependencies: "@eclipse-che/api" "^7.0.0-beta-4.0" axios "^0.21.4" From 49126119b126a9840c9a4a0d772bb81923b76094 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Fri, 13 Jan 2023 18:49:00 +0200 Subject: [PATCH 2/6] fix: Code refactoring Signed-off-by: Oleksii Orel --- .../GitServicesToolbar/index.tsx | 171 ++++++++++++++++++ .../GitServicesTab/__tests__/index.spec.tsx | 4 +- .../UserPreferences/GitServicesTab/index.tsx | 122 ++----------- 3 files changed, 193 insertions(+), 104 deletions(-) create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx new file mode 100644 index 000000000..18b038aa1 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { + AlertVariant, + Button, + ButtonVariant, + Toolbar, + ToolbarContent, + ToolbarItem, +} from '@patternfly/react-core'; +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { lazyInject } from '../../../../inversify.config'; +import { AppAlerts } from '../../../../services/alerts/appAlerts'; +import { AlertItem } from '../../../../services/helpers/types'; +import RevokeGitServicesModal from '../Modals/RevokeGitServicesModal'; +import { api, helpers } from '@eclipse-che/common'; +import * as GitOauthConfig from '../../../../store/GitOauthConfig'; +import { isEqual } from 'lodash'; +import { AppState } from '../../../../store'; +import { selectGitOauth } from '../../../../store/GitOauthConfig/selectors'; + +type Props = MappedProps & { + callbacks: { + onChangeSelection?: (selectedItems: api.GitOauthProvider[]) => void; + }; + selectedItems: api.GitOauthProvider[]; +}; + +type State = { + currentGitOauth: api.GitOauthProvider | undefined; + currentGitOauthIndex: number; + isRevokeModalOpen: boolean; +}; + +export class GitServicesToolbar extends React.PureComponent { + @lazyInject(AppAlerts) + private readonly appAlerts: AppAlerts; + + constructor(props: Props) { + super(props); + + this.state = { + currentGitOauth: undefined, + currentGitOauthIndex: -1, + isRevokeModalOpen: false, + }; + } + + private onChangeSelection(selectedItems: api.GitOauthProvider[]): void { + if (this.props.callbacks.onChangeSelection) { + this.props.callbacks.onChangeSelection(selectedItems); + } + } + + public componentDidUpdate(prevProps: Props, prevState: State): void { + const gitOauth = this.props.gitOauth; + if (!isEqual(prevProps.gitOauth, gitOauth)) { + const selectedItems: api.GitOauthProvider[] = []; + this.props.selectedItems.forEach(selectedItem => { + if (gitOauth.map(val => val.name).indexOf(selectedItem) !== -1) { + selectedItems.push(selectedItem); + } + }); + this.onChangeSelection(selectedItems); + } + if (prevState.currentGitOauthIndex !== this.state.currentGitOauthIndex) { + const currentGitOauth = gitOauth[this.state.currentGitOauthIndex]?.name; + this.setState({ currentGitOauth }); + } + } + + private showAlert(alert: AlertItem): void { + this.appAlerts.showAlert(alert); + } + + public showOnRevokeGitOauthModal(rowIndex: number): void { + this.setState({ currentGitOauthIndex: rowIndex, isRevokeModalOpen: true }); + } + + private async revokeOauth(gitOauth: api.GitOauthProvider): Promise { + try { + await this.props.revokeOauth(gitOauth); + this.showAlert({ + key: 'revoke-github', + variant: AlertVariant.success, + title: `Git oauth '${gitOauth}' successfully deleted.`, + }); + } catch (e) { + this.showAlert({ + key: 'revoke-fail', + variant: AlertVariant.danger, + title: helpers.errors.getMessage(e), + }); + } + } + + private async handleRevoke(gitOauth?: api.GitOauthProvider): Promise { + this.setState({ isRevokeModalOpen: false, currentGitOauthIndex: -1 }); + if (gitOauth === undefined) { + for (const selectedItem of this.props.selectedItems) { + await this.revokeOauth(selectedItem); + } + this.onChangeSelection([]); + } else { + await this.revokeOauth(gitOauth); + } + } + + private setRevokeModalStatus(isRevokeModalOpen: boolean): void { + if (this.state.isRevokeModalOpen === isRevokeModalOpen) { + return; + } + this.setState({ isRevokeModalOpen }); + } + + private handleModalShow(): void { + this.setState({ currentGitOauthIndex: -1, isRevokeModalOpen: true }); + } + + render(): React.ReactNode { + const { selectedItems } = this.props; + const { isRevokeModalOpen, currentGitOauth } = this.state; + return ( + + this.setRevokeModalStatus(false)} + onRevoke={() => this.handleRevoke(currentGitOauth)} + isOpen={isRevokeModalOpen} + gitOauth={currentGitOauth} + /> + + + + + + + + + ); + } +} + +const mapStateToProps = (state: AppState) => ({ + gitOauth: selectGitOauth(state), +}); + +const connector = connect(mapStateToProps, GitOauthConfig.actionCreators, null, { + forwardRef: true, +}); + +type MappedProps = ConnectedProps; +export default connector(GitServicesToolbar); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/index.spec.tsx index b94bb11c5..70cc26fca 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/index.spec.tsx @@ -20,6 +20,7 @@ import { GitServicesTab } from '..'; import { FakeGitOauthBuilder } from './__mocks__/gitOauthRowBuilder'; import { FakeStoreBuilder } from '../../../../store/__mocks__/storeBuilder'; import { selectIsLoading, selectGitOauth } from '../../../../store/GitOauthConfig/selectors'; +import { actionCreators } from '../../../../store/GitOauthConfig'; describe('GitServices', () => { const mockRevokeOauth = jest.fn(); @@ -83,6 +84,7 @@ describe('GitServices', () => { }); it('should revoke a git service', () => { + const spyRevokeOauth = jest.spyOn(actionCreators, 'revokeOauth'); const component = getComponent( new FakeStoreBuilder() .withGitOauthConfig([ @@ -112,6 +114,6 @@ describe('GitServices', () => { expect(revokeButton).toBeEnabled(); userEvent.click(revokeButton); - expect(mockRevokeOauth).toBeCalledWith('github'); + expect(spyRevokeOauth).toBeCalledWith('github'); }); }); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx index 6d2c79c55..4aa78ff7d 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx @@ -10,29 +10,19 @@ * Red Hat, Inc. - initial API and implementation */ -import { - AlertVariant, - Button, - ButtonVariant, - PageSection, - Toolbar, - ToolbarContent, - ToolbarItem, -} from '@patternfly/react-core'; +import { PageSection } from '@patternfly/react-core'; import { Table, TableBody, TableHeader } from '@patternfly/react-table'; import React from 'react'; import { connect, ConnectedProps } from 'react-redux'; import ProgressIndicator from '../../../components/Progress'; -import { lazyInject } from '../../../inversify.config'; -import { AppAlerts } from '../../../services/alerts/appAlerts'; -import { AlertItem } from '../../../services/helpers/types'; import { AppState } from '../../../store'; import { selectIsLoading, selectGitOauth } from '../../../store/GitOauthConfig/selectors'; import EmptyState from './EmptyState'; -import RevokeGitServicesModal from './Modals/RevokeGitServicesModal'; -import { api, helpers } from '@eclipse-che/common'; +import { api } from '@eclipse-che/common'; import * as GitOauthConfig from '../../../store/GitOauthConfig'; import { isEqual } from 'lodash'; +import { IGitOauth } from '../../../store/GitOauthConfig/types'; +import GitServicesToolbar, { GitServicesToolbar as Toolbar } from './GitServicesToolbar'; export const providersMap = { github: 'GitHub', @@ -44,29 +34,25 @@ type Props = MappedProps; type State = { selectedItems: api.GitOauthProvider[]; - gitOauth: { name: api.GitOauthProvider; endpointUrl: string }[]; - currentGitOauth: api.GitOauthProvider | undefined; - currentGitOauthIndex: number; - isRevokeModalOpen: boolean; - isEditModalOpen: boolean; + gitOauth: IGitOauth[]; }; export class GitServicesTab extends React.PureComponent { - @lazyInject(AppAlerts) - private readonly appAlerts: AppAlerts; + private readonly gitServicesToolbarRef: React.RefObject; + private readonly callbacks: { + onChangeSelection?: (selectedItems: api.GitOauthProvider[]) => void; + }; constructor(props: Props) { super(props); + this.gitServicesToolbarRef = React.createRef(); + const gitOauth = this.props.gitOauth; this.state = { gitOauth, - currentGitOauth: undefined, selectedItems: [], - currentGitOauthIndex: -1, - isEditModalOpen: false, - isRevokeModalOpen: false, }; } @@ -94,27 +80,13 @@ export class GitServicesTab extends React.PureComponent { } } - public componentDidUpdate(prevProps: Props, prevState: State): void { + public componentDidUpdate(prevProps: Props): void { const gitOauth = this.props.gitOauth; if (!isEqual(prevProps.gitOauth, gitOauth)) { - const selectedItems: api.GitOauthProvider[] = []; - this.state.selectedItems.forEach(selectedItem => { - if (gitOauth.map(val => val.name).indexOf(selectedItem) !== -1) { - selectedItems.push(selectedItem); - } - }); - this.setState({ gitOauth, selectedItems }); - } - if (prevState.currentGitOauthIndex !== this.state.currentGitOauthIndex) { - const currentGitOauth = gitOauth[this.state.currentGitOauthIndex]?.name; - this.setState({ currentGitOauth }); + this.setState({ gitOauth }); } } - private showAlert(alert: AlertItem): void { - this.appAlerts.showAlert(alert); - } - private buildGitOauthRow(gitOauth: api.GitOauthProvider, server: string): React.ReactNode[] { const oauthRow: React.ReactNode[] = [{providersMap[gitOauth]}]; @@ -134,55 +106,15 @@ export class GitServicesTab extends React.PureComponent { } private showOnRevokeGitOauthModal(rowIndex: number): void { - this.setState({ currentGitOauthIndex: rowIndex, isRevokeModalOpen: true }); - } - - private async revokeOauth(gitOauth: api.GitOauthProvider): Promise { - try { - await this.props.revokeOauth(gitOauth); - this.showAlert({ - key: 'revoke-github', - variant: AlertVariant.success, - title: `Git oauth '${gitOauth}' successfully deleted.`, - }); - } catch (e) { - this.showAlert({ - key: 'revoke-fail', - variant: AlertVariant.danger, - title: helpers.errors.getMessage(e), - }); - } - } - - private async handleRevoke(gitOauth?: api.GitOauthProvider): Promise { - this.setState({ isRevokeModalOpen: false, currentGitOauthIndex: -1 }); - if (gitOauth === undefined) { - for (const selectedItem of this.state.selectedItems) { - await this.revokeOauth(selectedItem); - } - this.setState({ selectedItems: [] }); - } else { - await this.revokeOauth(gitOauth); - } - } - - private setRevokeModalStatus(isRevokeModalOpen: boolean): void { - if (this.state.isRevokeModalOpen === isRevokeModalOpen) { - return; - } - this.setState({ isRevokeModalOpen }); - } - - private handleModalShow(): void { - this.setState({ currentGitOauthIndex: -1, isRevokeModalOpen: true }); + this.gitServicesToolbarRef.current?.showOnRevokeGitOauthModal(rowIndex); } render(): React.ReactNode { const { isLoading } = this.props; - const { isRevokeModalOpen, currentGitOauth, selectedItems } = this.state; + const { selectedItems, gitOauth } = this.state; const columns = ['Name', 'Server']; const rows = - this.state.gitOauth.map(provider => ({ + gitOauth.map(provider => ({ cells: this.buildGitOauthRow(provider.name, provider.endpointUrl), selected: selectedItems.includes(provider.name), })) || []; @@ -201,27 +133,11 @@ export class GitServicesTab extends React.PureComponent { ) : ( - this.setRevokeModalStatus(false)} - onRevoke={() => this.handleRevoke(currentGitOauth)} - isOpen={isRevokeModalOpen} - gitOauth={currentGitOauth} /> - - - - - - - Date: Mon, 16 Jan 2023 13:37:30 +0200 Subject: [PATCH 3/6] fixup! fix: Code refactoring --- .../GitServicesTab/GitServicesToolbar/index.tsx | 13 +++++-------- .../Modals/__tests__/RevokeRegistriesModal.spec.tsx | 8 -------- .../RevokeRegistriesModal.spec.tsx.snap | 3 --- .../pages/UserPreferences/GitServicesTab/index.tsx | 4 ++-- 4 files changed, 7 insertions(+), 21 deletions(-) delete mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/__snapshots__/RevokeRegistriesModal.spec.tsx.snap diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx index 18b038aa1..35507e824 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx @@ -58,7 +58,7 @@ export class GitServicesToolbar extends React.PureComponent { } private onChangeSelection(selectedItems: api.GitOauthProvider[]): void { - if (this.props.callbacks.onChangeSelection) { + if (this.props.callbacks?.onChangeSelection) { this.props.callbacks.onChangeSelection(selectedItems); } } @@ -68,7 +68,7 @@ export class GitServicesToolbar extends React.PureComponent { if (!isEqual(prevProps.gitOauth, gitOauth)) { const selectedItems: api.GitOauthProvider[] = []; this.props.selectedItems.forEach(selectedItem => { - if (gitOauth.map(val => val.name).indexOf(selectedItem) !== -1) { + if (gitOauth.find(val => val.name === selectedItem) !== undefined) { selectedItems.push(selectedItem); } }); @@ -117,11 +117,8 @@ export class GitServicesToolbar extends React.PureComponent { } } - private setRevokeModalStatus(isRevokeModalOpen: boolean): void { - if (this.state.isRevokeModalOpen === isRevokeModalOpen) { - return; - } - this.setState({ isRevokeModalOpen }); + private handleModalHide(): void { + this.setState({ isRevokeModalOpen: false }); } private handleModalShow(): void { @@ -135,7 +132,7 @@ export class GitServicesToolbar extends React.PureComponent { this.setRevokeModalStatus(false)} + onCancel={() => this.handleModalHide()} onRevoke={() => this.handleRevoke(currentGitOauth)} isOpen={isRevokeModalOpen} gitOauth={currentGitOauth} diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/RevokeRegistriesModal.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/RevokeRegistriesModal.spec.tsx index 2f40ecdd5..43ee14b97 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/RevokeRegistriesModal.spec.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/RevokeRegistriesModal.spec.tsx @@ -42,14 +42,6 @@ describe('Revoke Registries Modal', () => { jest.clearAllMocks(); }); - it('should correctly render the closed modal component', () => { - const component = getComponent(false, []); - - const json = renderer.create(component).toJSON(); - - expect(json).toMatchSnapshot(); - }); - it('should correctly render the component', () => { const component = getComponent(true, [], 'gitlab'); render(component); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/__snapshots__/RevokeRegistriesModal.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/__snapshots__/RevokeRegistriesModal.spec.tsx.snap deleted file mode 100644 index 19a011373..000000000 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/__snapshots__/RevokeRegistriesModal.spec.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Revoke Registries Modal should correctly render the closed modal component 1`] = `null`; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx index 4aa78ff7d..96746bb1d 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx @@ -59,7 +59,7 @@ export class GitServicesTab extends React.PureComponent { private onChangeSelection(isSelected: boolean, rowIndex: number) { const { gitOauth } = this.state; if (rowIndex === -1) { - const selectedItems = isSelected ? gitOauth.map(val => val.name) : []; + const selectedItems = gitOauth?.length && isSelected ? gitOauth.map(val => val.name) : []; this.setState({ selectedItems }); } else { const selectedItem = gitOauth[rowIndex]?.name; @@ -114,7 +114,7 @@ export class GitServicesTab extends React.PureComponent { const { selectedItems, gitOauth } = this.state; const columns = ['Name', 'Server']; const rows = - gitOauth.map(provider => ({ + gitOauth?.map(provider => ({ cells: this.buildGitOauthRow(provider.name, provider.endpointUrl), selected: selectedItems.includes(provider.name), })) || []; From 9e21e9c9a5d83fb4d72700c0637c6d2c1924f867 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Mon, 16 Jan 2023 16:57:19 +0200 Subject: [PATCH 4/6] fixup! fixup! fix: Code refactoring --- .../UserPreferences/GitServicesTab/index.tsx | 33 +++++++------------ .../src/store/GitOauthConfig/index.ts | 22 +++++++------ 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx index 96746bb1d..56566357d 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx @@ -20,8 +20,6 @@ import { selectIsLoading, selectGitOauth } from '../../../store/GitOauthConfig/s import EmptyState from './EmptyState'; import { api } from '@eclipse-che/common'; import * as GitOauthConfig from '../../../store/GitOauthConfig'; -import { isEqual } from 'lodash'; -import { IGitOauth } from '../../../store/GitOauthConfig/types'; import GitServicesToolbar, { GitServicesToolbar as Toolbar } from './GitServicesToolbar'; export const providersMap = { @@ -34,7 +32,6 @@ type Props = MappedProps; type State = { selectedItems: api.GitOauthProvider[]; - gitOauth: IGitOauth[]; }; export class GitServicesTab extends React.PureComponent { @@ -48,18 +45,15 @@ export class GitServicesTab extends React.PureComponent { this.gitServicesToolbarRef = React.createRef(); - const gitOauth = this.props.gitOauth; - this.state = { - gitOauth, selectedItems: [], }; } private onChangeSelection(isSelected: boolean, rowIndex: number) { - const { gitOauth } = this.state; + const { gitOauth } = this.props; if (rowIndex === -1) { - const selectedItems = gitOauth?.length && isSelected ? gitOauth.map(val => val.name) : []; + const selectedItems = isSelected ? gitOauth.map(val => val.name) : []; this.setState({ selectedItems }); } else { const selectedItem = gitOauth[rowIndex]?.name; @@ -80,13 +74,6 @@ export class GitServicesTab extends React.PureComponent { } } - public componentDidUpdate(prevProps: Props): void { - const gitOauth = this.props.gitOauth; - if (!isEqual(prevProps.gitOauth, gitOauth)) { - this.setState({ gitOauth }); - } - } - private buildGitOauthRow(gitOauth: api.GitOauthProvider, server: string): React.ReactNode[] { const oauthRow: React.ReactNode[] = [{providersMap[gitOauth]}]; @@ -110,20 +97,22 @@ export class GitServicesTab extends React.PureComponent { } render(): React.ReactNode { - const { isLoading } = this.props; - const { selectedItems, gitOauth } = this.state; + const { isLoading, gitOauth } = this.props; + const { selectedItems } = this.state; const columns = ['Name', 'Server']; - const rows = - gitOauth?.map(provider => ({ - cells: this.buildGitOauthRow(provider.name, provider.endpointUrl), - selected: selectedItems.includes(provider.name), - })) || []; const actions = [ { title: 'Revoke', onClick: (event, rowIndex) => this.showOnRevokeGitOauthModal(rowIndex), }, ]; + const rows = + gitOauth.length > 0 + ? gitOauth.map(provider => ({ + cells: this.buildGitOauthRow(provider.name, provider.endpointUrl), + selected: selectedItems.includes(provider.name), + })) + : []; return ( diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts index daa0aad76..21775d344 100644 --- a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts @@ -70,21 +70,23 @@ export const actionCreators: ActionCreators = { async (dispatch): Promise => { await dispatch({ type: Type.REQUEST_GIT_OAUTH_CONFIG, + check: AUTHORIZED, }); const gitOauth: IGitOauth[] = []; try { const oAuthProviders = await cheWorkspaceClient.restApiClient.getOAuthProviders(); + const promises: Promise[] = []; for (const { name, endpointUrl } of oAuthProviders) { - try { - await cheWorkspaceClient.restApiClient.getOAuthToken(name); - gitOauth.push({ - name: name as api.GitOauthProvider, - endpointUrl, - }); - } catch (e) { - // no-op - } + promises.push( + cheWorkspaceClient.restApiClient.getOAuthToken(name).then(() => { + gitOauth.push({ + name: name as api.GitOauthProvider, + endpointUrl, + }); + }), + ); } + await Promise.allSettled(promises); dispatch({ type: Type.RECEIVE_GIT_OAUTH_CONFIG, @@ -119,7 +121,7 @@ export const actionCreators: ActionCreators = { type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR, error: errorMessage, }); - throw errorMessage; + throw e; } }, }; From 17347eb16eaf4f7d35c6ecc18e1cd28361cea5f5 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Mon, 16 Jan 2023 17:37:05 +0200 Subject: [PATCH 5/6] fixup! fixup! fixup! fix: Code refactoring --- .../src/pages/UserPreferences/GitServicesTab/index.tsx | 2 +- packages/dashboard-frontend/src/store/GitOauthConfig/index.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx index 56566357d..d1d925625 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx @@ -53,7 +53,7 @@ export class GitServicesTab extends React.PureComponent { private onChangeSelection(isSelected: boolean, rowIndex: number) { const { gitOauth } = this.props; if (rowIndex === -1) { - const selectedItems = isSelected ? gitOauth.map(val => val.name) : []; + const selectedItems = isSelected && gitOauth.length > 0 ? gitOauth.map(val => val.name) : []; this.setState({ selectedItems }); } else { const selectedItem = gitOauth[rowIndex]?.name; diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts index 21775d344..c435d13db 100644 --- a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts @@ -155,10 +155,9 @@ export const reducer: Reducer = ( gitOauth: action.gitOauth, }); case Type.DELETE_OAUTH: - delete gitOauth[action.provider]; return createObject(state, { isLoading: false, - gitOauth, + gitOauth: gitOauth.filter(v => v.name !== action.provider), }); case Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR: return createObject(state, { From 0bbb129b50103b0797d8512dbd06f3afa6541194 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Mon, 16 Jan 2023 17:40:41 +0200 Subject: [PATCH 6/6] fixup! fixup! fixup! fixup! fix: Code refactoring --- packages/dashboard-frontend/src/store/GitOauthConfig/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts index c435d13db..2a4a85017 100644 --- a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts @@ -140,8 +140,6 @@ export const reducer: Reducer = ( return unloadedState; } - const gitOauth = Object.assign({}, state.gitOauth); - const action = incomingAction as KnownAction; switch (action.type) { case Type.REQUEST_GIT_OAUTH_CONFIG: @@ -157,7 +155,7 @@ export const reducer: Reducer = ( case Type.DELETE_OAUTH: return createObject(state, { isLoading: false, - gitOauth: gitOauth.filter(v => v.name !== action.provider), + gitOauth: state.gitOauth.filter(v => v.name !== action.provider), }); case Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR: return createObject(state, {