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/GitServicesToolbar/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx new file mode 100644 index 000000000..35507e824 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/GitServicesToolbar/index.tsx @@ -0,0 +1,168 @@ +/* + * 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.find(val => val.name === selectedItem) !== undefined) { + 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 handleModalHide(): void { + this.setState({ isRevokeModalOpen: false }); + } + + private handleModalShow(): void { + this.setState({ currentGitOauthIndex: -1, isRevokeModalOpen: true }); + } + + render(): React.ReactNode { + const { selectedItems } = this.props; + const { isRevokeModalOpen, currentGitOauth } = this.state; + return ( + + this.handleModalHide()} + 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/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..43ee14b97 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/__tests__/RevokeRegistriesModal.spec.tsx @@ -0,0 +1,107 @@ +/* + * 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 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/__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..70cc26fca --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/__tests__/index.spec.tsx @@ -0,0 +1,119 @@ +/* + * 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'; +import { actionCreators } from '../../../../store/GitOauthConfig'; + +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 spyRevokeOauth = jest.spyOn(actionCreators, 'revokeOauth'); + 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(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 new file mode 100644 index 000000000..d1d925625 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx @@ -0,0 +1,160 @@ +/* + * 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 { 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 { AppState } from '../../../store'; +import { selectIsLoading, selectGitOauth } from '../../../store/GitOauthConfig/selectors'; +import EmptyState from './EmptyState'; +import { api } from '@eclipse-che/common'; +import * as GitOauthConfig from '../../../store/GitOauthConfig'; +import GitServicesToolbar, { GitServicesToolbar as Toolbar } from './GitServicesToolbar'; + +export const providersMap = { + github: 'GitHub', + gitlab: 'GitLab', + bitbucket: 'Bitbucket', +}; + +type Props = MappedProps; + +type State = { + selectedItems: api.GitOauthProvider[]; +}; + +export class GitServicesTab extends React.PureComponent { + private readonly gitServicesToolbarRef: React.RefObject; + private readonly callbacks: { + onChangeSelection?: (selectedItems: api.GitOauthProvider[]) => void; + }; + + constructor(props: Props) { + super(props); + + this.gitServicesToolbarRef = React.createRef(); + + this.state = { + selectedItems: [], + }; + } + + private onChangeSelection(isSelected: boolean, rowIndex: number) { + const { gitOauth } = this.props; + if (rowIndex === -1) { + const selectedItems = isSelected && gitOauth.length > 0 ? 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(); + } + } + + 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.gitServicesToolbarRef.current?.showOnRevokeGitOauthModal(rowIndex); + } + + render(): React.ReactNode { + const { isLoading, gitOauth } = this.props; + const { selectedItems } = this.state; + const columns = ['Name', 'Server']; + 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 ( + + + + {rows.length === 0 ? ( + + ) : ( + + + { + 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..2a4a85017 --- /dev/null +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts @@ -0,0 +1,168 @@ +/* + * 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, + check: AUTHORIZED, + }); + const gitOauth: IGitOauth[] = []; + try { + const oAuthProviders = await cheWorkspaceClient.restApiClient.getOAuthProviders(); + const promises: Promise[] = []; + for (const { name, endpointUrl } of oAuthProviders) { + 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, + 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 e; + } + }, +}; + +const unloadedState: State = { + isLoading: false, + gitOauth: [], + error: undefined, +}; + +export const reducer: Reducer = ( + state: State | undefined, + incomingAction: Action, +): State => { + if (state === undefined) { + return unloadedState; + } + + 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: + return createObject(state, { + isLoading: false, + gitOauth: state.gitOauth.filter(v => v.name !== action.provider), + }); + 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"