From 6c943618d5c02fa475d26f7c9bd308d5a526ba19 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Thu, 2 Nov 2023 18:26:24 +0200 Subject: [PATCH 1/5] feat: add reject the authorisation API Signed-off-by: Oleksii Orel --- packages/common/src/dto/api/index.ts | 10 +- packages/dashboard-backend/src/app.ts | 3 + .../src/constants/schemas.ts | 13 +++ .../src/devworkspaceClient/__mocks__/index.ts | 4 + .../src/devworkspaceClient/index.ts | 6 ++ .../services/devWorkspacePreferencesApi.ts | 96 +++++++++++++++++++ .../src/devworkspaceClient/types/index.ts | 14 +++ .../src/models/restParams.ts | 5 + .../src/routes/api/workspacePreferences.ts | 59 ++++++++++++ 9 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 packages/dashboard-backend/src/devworkspaceClient/services/devWorkspacePreferencesApi.ts create mode 100644 packages/dashboard-backend/src/routes/api/workspacePreferences.ts diff --git a/packages/common/src/dto/api/index.ts b/packages/common/src/dto/api/index.ts index ea83e08c2..81700c6c4 100644 --- a/packages/common/src/dto/api/index.ts +++ b/packages/common/src/dto/api/index.ts @@ -136,15 +136,13 @@ export interface IUserProfile { email: string; username: string; } +export interface IDevWorkspacePreferences { + 'skip-authorisation': GitProvider[]; + [key: string]: string | string[]; +} export type IEventList = CoreV1EventList; export type IPodList = V1PodList; -export type PodLogs = { - [containerName: string]: { - logs: string; - failure: boolean; - }; -}; export interface IDevWorkspaceList { apiVersion?: string; diff --git a/packages/dashboard-backend/src/app.ts b/packages/dashboard-backend/src/app.ts index e4832a911..c4024ce89 100644 --- a/packages/dashboard-backend/src/app.ts +++ b/packages/dashboard-backend/src/app.ts @@ -38,6 +38,7 @@ import { registerServerConfigRoute } from '@/routes/api/serverConfig'; import { registerSShKeysRoutes } from '@/routes/api/sshKeys'; import { registerUserProfileRoute } from '@/routes/api/userProfile'; import { registerWebsocket } from '@/routes/api/websocket'; +import { registerWorkspacePreferencesRoute } from '@/routes/api/workspacePreferences'; import { registerYamlResolverRoute } from '@/routes/api/yamlResolver'; import { registerFactoryAcceptanceRedirect } from '@/routes/factoryAcceptanceRedirect'; import { registerWorkspaceRedirect } from '@/routes/workspaceRedirect'; @@ -117,5 +118,7 @@ export default async function buildApp(server: FastifyInstance): Promise { + try { + const response = await this.coreV1API.readNamespacedConfigMap( + DEV_WORKSPACE_PREFERENCES_CONFIGMAP, + namespace, + ); + const data = response.body.data; + if (data === undefined) { + throw new Error('Data is empty'); + } + + const skipAuthorisation = + data[SKIP_AUTORIZATION_KEY] && data[SKIP_AUTORIZATION_KEY] !== '[]' + ? data[SKIP_AUTORIZATION_KEY].replace(/^\[/, '').replace(/\]$/, '').split(', ') + : []; + + return Object.assign({}, data, { + [SKIP_AUTORIZATION_KEY]: skipAuthorisation, + }) as api.IDevWorkspacePreferences; + } catch (e) { + throw createError(e, ERROR_LABEL, 'Unable to get workspace preferences data'); + } + } + + public async removeProviderFromSkipAuthorization( + namespace: string, + provider: GitProvider, + ): Promise { + const devWorkspacePreferences = await this.getWorkspacePreferences(namespace); + + const skipAuthorisation = devWorkspacePreferences[SKIP_AUTORIZATION_KEY].filter( + (val: string) => val !== provider, + ); + const skipAuthorisationStr = + skipAuthorisation.length > 0 ? `[${skipAuthorisation.join(', ')}]` : '[]'; + const data = Object.assign({}, devWorkspacePreferences, { + [SKIP_AUTORIZATION_KEY]: skipAuthorisationStr, + }); + + try { + await this.coreV1API.patchNamespacedConfigMap( + DEV_WORKSPACE_PREFERENCES_CONFIGMAP, + namespace, + { data }, + undefined, + undefined, + undefined, + undefined, + undefined, + { + headers: { + 'content-type': k8s.PatchUtils.PATCH_FORMAT_STRATEGIC_MERGE_PATCH, + }, + }, + ); + } catch (error) { + const message = `Unable to update workspace preferences in the namespace "${namespace}"`; + throw createError(undefined, ERROR_LABEL, message); + } + } +} diff --git a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts index 5efda685e..23849f6b4 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts @@ -16,6 +16,7 @@ import { V221DevfileComponents, } from '@devfile/api'; import { api } from '@eclipse-che/common'; +import { GitProvider } from '@eclipse-che/common/lib/dto/api'; import * as k8s from '@kubernetes/client-node'; import { IncomingHttpHeaders } from 'http'; @@ -348,6 +349,18 @@ export interface IUserProfileApi { getUserProfile(namespace: string): Promise; } +export interface IDevWorkspacePreferencesApi { + /** + * Returns workspace preferences object that contains skip-authorisation info. + */ + getWorkspacePreferences(namespace: string): Promise; + + /** + * Removes the target provider from skip-authorisation property from the workspace preferences object. + */ + removeProviderFromSkipAuthorization(namespace: string, provider: GitProvider): Promise; +} + export interface IPersonalAccessTokenApi { /** * Reads all the PAT secrets from the specified namespace. @@ -390,6 +403,7 @@ export interface IDevWorkspaceClient { gitConfigApi: IGitConfigApi; gettingStartedSampleApi: IGettingStartedSampleApi; sshKeysApi: IShhKeysApi; + devWorkspacePreferencesApi: IDevWorkspacePreferencesApi; } export interface IWatcherService> { diff --git a/packages/dashboard-backend/src/models/restParams.ts b/packages/dashboard-backend/src/models/restParams.ts index 926d576f1..89c63e19a 100644 --- a/packages/dashboard-backend/src/models/restParams.ts +++ b/packages/dashboard-backend/src/models/restParams.ts @@ -11,10 +11,15 @@ */ import { V1alpha2DevWorkspace, V1alpha2DevWorkspaceTemplate } from '@devfile/api'; +import { api } from '@eclipse-che/common'; export interface INamespacedParams { namespace: string; } +export interface IWorkspacePreferencesParams { + namespace: string; + provider: api.GitProvider; +} export interface IDockerConfigParams { dockerconfig: string; diff --git a/packages/dashboard-backend/src/routes/api/workspacePreferences.ts b/packages/dashboard-backend/src/routes/api/workspacePreferences.ts new file mode 100644 index 000000000..fd1313b70 --- /dev/null +++ b/packages/dashboard-backend/src/routes/api/workspacePreferences.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2018-2023 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 { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; + +import { baseApiPath } from '@/constants/config'; +import { namespacedSchema, namespacedWorkspacePreferencesSchema } from '@/constants/schemas'; +import { restParams } from '@/models'; +import { getDevWorkspaceClient } from '@/routes/api/helpers/getDevWorkspaceClient'; +import { getToken } from '@/routes/api/helpers/getToken'; +import { getSchema } from '@/services/helpers'; + +const tags = ['WorkspacePreferences']; + +export function registerWorkspacePreferencesRoute(instance: FastifyInstance) { + instance.register(async server => { + server.get( + `${baseApiPath}/workspacepreferences/:namespace`, + getSchema({ tags, params: namespacedSchema }), + async function (request: FastifyRequest) { + const { namespace } = request.params as restParams.INamespacedParams; + const token = getToken(request); + const { devWorkspacePreferencesApi } = getDevWorkspaceClient(token); + return devWorkspacePreferencesApi.getWorkspacePreferences(namespace); + }, + ); + + server.delete( + `${baseApiPath}/workspacepreferences/:namespace/skip-authorisation/:provider`, + getSchema({ + tags, + params: namespacedWorkspacePreferencesSchema, + response: { + 204: { + description: 'The Provider is successfully removed from skip-authorisation list', + type: 'null', + }, + }, + }), + async function (request: FastifyRequest, reply: FastifyReply) { + const { namespace, provider } = request.params as restParams.IWorkspacePreferencesParams; + const token = getToken(request); + const { devWorkspacePreferencesApi } = getDevWorkspaceClient(token); + await devWorkspacePreferencesApi.removeProviderFromSkipAuthorization(namespace, provider); + reply.code(204); + return reply.send(); + }, + ); + }); +} From 748cbc927681f37d1eea9cd5fc1cf265b8c04e14 Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Wed, 8 Nov 2023 05:33:55 +0200 Subject: [PATCH 2/5] feat: implemented an ability to reject the authorisation opt-out flag Signed-off-by: Oleksii Orel --- .../sshKeysApi/__tests__/index.spec.ts | 6 - .../src/routes/api/workspacePreferences.ts | 4 +- packages/dashboard-frontend/jest.setup.ts | 1 + .../CheTooltip/__tests__/CheTooltip.spec.tsx | 36 ++++ .../__snapshots__/CheTooltip.spec.tsx.snap | 10 + .../src/components/CheTooltip/index.tsx | 40 ++++ .../ContainerRegistriesTab/index.tsx | 11 +- .../__tests__/ProviderIcon.spec.tsx | 78 ++++++++ .../__snapshots__/ProviderIcon.spec.tsx.snap | 79 ++++++++ .../GitServicesTab/ProviderIcon/index.tsx | 118 +++++++++++ ...ndex.spec.tsx => ProviderWarning.spec.tsx} | 31 +-- .../ProviderWarning.spec.tsx.snap | 46 +++++ .../__snapshots__/index.spec.tsx.snap | 32 --- .../GitServicesTab/ProviderWarning/index.tsx | 39 ++-- .../__snapshots__/index.spec.tsx.snap | 187 +++++++++++++----- .../GitServicesTab/__tests__/index.spec.tsx | 77 +++++--- .../UserPreferences/GitServicesTab/index.tsx | 113 ++++++----- .../src/pages/UserPreferences/index.tsx | 1 + .../src/services/backend-client/oAuthApi.ts | 24 ++- .../src/store/GitOauthConfig/index.ts | 167 ++++++++++++---- .../src/store/GitOauthConfig/selectors.ts | 8 + .../src/store/GitOauthConfig/types.ts | 4 +- .../src/store/__mocks__/storeBuilder.ts | 10 +- .../src/utils/che-tooltip.ts | 24 +++ 24 files changed, 889 insertions(+), 257 deletions(-) create mode 100644 packages/dashboard-frontend/src/components/CheTooltip/__tests__/CheTooltip.spec.tsx create mode 100644 packages/dashboard-frontend/src/components/CheTooltip/__tests__/__snapshots__/CheTooltip.spec.tsx.snap create mode 100644 packages/dashboard-frontend/src/components/CheTooltip/index.tsx create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/ProviderIcon.spec.tsx create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/__snapshots__/ProviderIcon.spec.tsx.snap create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/index.tsx rename packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/{index.spec.tsx => ProviderWarning.spec.tsx} (52%) create mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/ProviderWarning.spec.tsx.snap delete mode 100644 packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/index.spec.tsx.snap create mode 100644 packages/dashboard-frontend/src/utils/che-tooltip.ts diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/sshKeysApi/__tests__/index.spec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/sshKeysApi/__tests__/index.spec.ts index 381994682..306c26ac6 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/sshKeysApi/__tests__/index.spec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/sshKeysApi/__tests__/index.spec.ts @@ -66,12 +66,6 @@ describe('SSH Keys API', () => { response: {} as IncomingMessage, }); }, - // replaceNamespacedSecret: () => { - // return Promise.resolve({ - // body: {} as V1Secret, - // response: {} as IncomingMessage, - // }); - // }, deleteNamespacedSecret: () => { return Promise.resolve({ body: undefined, diff --git a/packages/dashboard-backend/src/routes/api/workspacePreferences.ts b/packages/dashboard-backend/src/routes/api/workspacePreferences.ts index fd1313b70..99a2428b0 100644 --- a/packages/dashboard-backend/src/routes/api/workspacePreferences.ts +++ b/packages/dashboard-backend/src/routes/api/workspacePreferences.ts @@ -24,7 +24,7 @@ const tags = ['WorkspacePreferences']; export function registerWorkspacePreferencesRoute(instance: FastifyInstance) { instance.register(async server => { server.get( - `${baseApiPath}/workspacepreferences/:namespace`, + `${baseApiPath}/workspace-preferences/namespace/:namespace`, getSchema({ tags, params: namespacedSchema }), async function (request: FastifyRequest) { const { namespace } = request.params as restParams.INamespacedParams; @@ -35,7 +35,7 @@ export function registerWorkspacePreferencesRoute(instance: FastifyInstance) { ); server.delete( - `${baseApiPath}/workspacepreferences/:namespace/skip-authorisation/:provider`, + `${baseApiPath}/workspace-preferences/namespace/:namespace/skip-authorisation/:provider`, getSchema({ tags, params: namespacedWorkspacePreferencesSchema, diff --git a/packages/dashboard-frontend/jest.setup.ts b/packages/dashboard-frontend/jest.setup.ts index 25b1253ab..2b40c1f9e 100644 --- a/packages/dashboard-frontend/jest.setup.ts +++ b/packages/dashboard-frontend/jest.setup.ts @@ -11,3 +11,4 @@ */ import '@testing-library/jest-dom'; +import '@/utils/che-tooltip'; diff --git a/packages/dashboard-frontend/src/components/CheTooltip/__tests__/CheTooltip.spec.tsx b/packages/dashboard-frontend/src/components/CheTooltip/__tests__/CheTooltip.spec.tsx new file mode 100644 index 000000000..eb2561e43 --- /dev/null +++ b/packages/dashboard-frontend/src/components/CheTooltip/__tests__/CheTooltip.spec.tsx @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018-2023 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, { ReactTestRendererJSON } from 'react-test-renderer'; + +import CheTooltip from '@/components/CheTooltip'; + +describe('CheTooltip component', () => { + it('should render CheTooltip component correctly', () => { + const content = Tooltip text.; + + const component = ( + + <>some text + + ); + + expect(getComponentSnapshot(component)).toMatchSnapshot(); + }); +}); + +function getComponentSnapshot( + component: React.ReactElement, +): null | ReactTestRendererJSON | ReactTestRendererJSON[] { + return renderer.create(component).toJSON(); +} diff --git a/packages/dashboard-frontend/src/components/CheTooltip/__tests__/__snapshots__/CheTooltip.spec.tsx.snap b/packages/dashboard-frontend/src/components/CheTooltip/__tests__/__snapshots__/CheTooltip.spec.tsx.snap new file mode 100644 index 000000000..55aa5a4fc --- /dev/null +++ b/packages/dashboard-frontend/src/components/CheTooltip/__tests__/__snapshots__/CheTooltip.spec.tsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CheTooltip component should render CheTooltip component correctly 1`] = ` +
+ some text + + Tooltip text. + +
+`; diff --git a/packages/dashboard-frontend/src/components/CheTooltip/index.tsx b/packages/dashboard-frontend/src/components/CheTooltip/index.tsx new file mode 100644 index 000000000..392ab55a7 --- /dev/null +++ b/packages/dashboard-frontend/src/components/CheTooltip/index.tsx @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018-2023 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 { Tooltip, TooltipPosition } from '@patternfly/react-core'; +import React from 'react'; + +type Props = { + children: React.ReactElement; + content: React.ReactNode; + position?: TooltipPosition; +}; + +class CheTooltip extends React.PureComponent { + public render(): React.ReactElement { + const { content, position, children } = this.props; + + return ( + + {children} + + ); + } +} + +export default CheTooltip; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/index.tsx index 3e362a1ba..f2844529d 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/ContainerRegistriesTab/index.tsx @@ -275,11 +275,17 @@ export class ContainerRegistries extends React.PureComponent { const actions = [ { title: 'Edit registry', - onClick: (event, rowIndex) => this.showOnEditRegistryModal(rowIndex), + onClick: (event, rowIndex) => { + event.stopPropagation(); + this.showOnEditRegistryModal(rowIndex); + }, }, { title: 'Delete registry', - onClick: (event, rowIndex) => this.showOnDeleteRegistryModal(rowIndex), + onClick: (event, rowIndex) => { + event.stopPropagation(); + this.showOnDeleteRegistryModal(rowIndex); + }, }, ]; @@ -342,6 +348,7 @@ export class ContainerRegistries extends React.PureComponent { actions={actions} rows={rows} onSelect={(event, isSelected, rowIndex) => { + event.stopPropagation(); this.onChangeRegistrySelection(isSelected, rowIndex); }} canSelectAll={true} diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/ProviderIcon.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/ProviderIcon.spec.tsx new file mode 100644 index 000000000..f4d6429e7 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/ProviderIcon.spec.tsx @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2018-2023 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'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { AnyAction } from 'redux'; +import { MockStoreEnhanced } from 'redux-mock-store'; +import { ThunkDispatch } from 'redux-thunk'; + +import { ProviderIcon } from '@/pages/UserPreferences/GitServicesTab/ProviderIcon'; +import getComponentRenderer from '@/services/__mocks__/getComponentRenderer'; +import { AppState } from '@/store'; +import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; +import { + selectProvidersWithToken, + selectSkipOauthProviders, +} from '@/store/GitOauthConfig/selectors'; + +const { createSnapshot } = getComponentRenderer(getComponent); + +function getComponent( + store: MockStoreEnhanced>, + gitOauth: api.GitOauthProvider, +): React.ReactElement { + const state = store.getState(); + return ( + + + + ); +} + +describe('ProviderIcon component', () => { + it('should render ProviderIcon component correctly when the user has been authorized successfully.', () => { + const gitOauth: api.GitOauthProvider = 'github'; + const store = new FakeStoreBuilder().withGitOauthConfig([], ['github'], []).build(); + + const snapshot = createSnapshot(store, gitOauth); + + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + it('should render ProviderIcon component correctly when authorization has been rejected by user.', () => { + const gitOauth: api.GitOauthProvider = 'github'; + const store = new FakeStoreBuilder().withGitOauthConfig([], [], ['github']).build(); + + const snapshot = createSnapshot(store, gitOauth); + + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + it('should render ProviderIcon component correctly when the user has not been authorized yet.', () => { + const gitOauth: api.GitOauthProvider = 'github'; + const store = new FakeStoreBuilder().withGitOauthConfig([], [], []).build(); + + const snapshot = createSnapshot(store, gitOauth); + + expect(snapshot.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/__snapshots__/ProviderIcon.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/__snapshots__/ProviderIcon.spec.tsx.snap new file mode 100644 index 000000000..df790b5af --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/__snapshots__/ProviderIcon.spec.tsx.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProviderIcon component should render ProviderIcon component correctly when authorization has been rejected by user. 1`] = ` +
+ + + + + Authorization has been rejected by user. + +
+`; + +exports[`ProviderIcon component should render ProviderIcon component correctly when the user has been authorized successfully. 1`] = ` +
+ + + + + User has been authorized successfully. + +
+`; + +exports[`ProviderIcon component should render ProviderIcon component correctly when the user has not been authorized yet. 1`] = ` +
+ + + + + User has not been authorized yet. + +
+`; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/index.tsx new file mode 100644 index 000000000..39a04cfd5 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/index.tsx @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2018-2023 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'; +import { TooltipPosition } from '@patternfly/react-core'; +import { + CheckCircleIcon, + ExclamationTriangleIcon, + ResourcesEmptyIcon, +} from '@patternfly/react-icons'; +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import CheTooltip from '@/components/CheTooltip'; +import { AppState } from '@/store'; +import * as GitOauthConfig from '@/store/GitOauthConfig'; +import { + selectProvidersWithToken, + selectSkipOauthProviders, +} from '@/store/GitOauthConfig/selectors'; + +type State = { + hasOauthToken: boolean; + isSkipOauth: boolean; +}; + +type Props = MappedProps & { + gitProvider: api.GitOauthProvider; +}; + +export class ProviderIcon extends React.PureComponent { + constructor(props: Props) { + super(props); + const hasOauthToken = this.hasOauthToken(this.props.gitProvider); + const isSkipOauth = this.isSkipOauth(this.props.gitProvider); + this.state = { + hasOauthToken, + isSkipOauth, + }; + } + + public async componentDidMount(): Promise { + const hasOauthToken = this.hasOauthToken(this.props.gitProvider); + const isSkipOauth = this.isSkipOauth(this.props.gitProvider); + this.setState({ + hasOauthToken, + isSkipOauth, + }); + } + + public async componentDidUpdate(): Promise { + const hasOauthToken = this.hasOauthToken(this.props.gitProvider); + const isSkipOauth = this.isSkipOauth(this.props.gitProvider); + this.setState({ + hasOauthToken, + isSkipOauth, + }); + } + + private isSkipOauth(providerName: api.GitOauthProvider): boolean { + return this.props.skipOauthProviders.includes(providerName); + } + + private hasOauthToken(providerName: api.GitOauthProvider): boolean { + return this.props.providersWithToken.includes(providerName); + } + + public render(): React.ReactElement { + const { hasOauthToken, isSkipOauth } = this.state; + if (hasOauthToken) { + return ( + User has been authorized successfully.} + position={TooltipPosition.top} + > + + + ); + } else if (isSkipOauth) { + return ( + Authorization has been rejected by user.} + position={TooltipPosition.top} + > + + + ); + } + + return ( + User has not been authorized yet.} + position={TooltipPosition.top} + > + + + ); + } +} + +const mapStateToProps = (state: AppState) => ({ + providersWithToken: selectProvidersWithToken(state), + skipOauthProviders: selectSkipOauthProviders(state), +}); + +const connector = connect(mapStateToProps, GitOauthConfig.actionCreators); + +type MappedProps = ConnectedProps; +export default connector(ProviderIcon); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/ProviderWarning.spec.tsx similarity index 52% rename from packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/index.spec.tsx rename to packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/ProviderWarning.spec.tsx index 2fbc8eb77..69ac04bdb 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/ProviderWarning.spec.tsx @@ -15,38 +15,9 @@ import renderer from 'react-test-renderer'; import ProviderWarning from '..'; -jest.mock('@patternfly/react-core', () => { - return { - Tooltip: (props: any) => { - return ( - <> - {props.children} - {props.content} - - ); - }, - TooltipPosition: { - right: 'right', - }, - }; -}); - describe('ProviderWarning component', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - it('should render ProviderWarning correctly', () => { - const element = ( - - Provided API does not support the automatic token revocation. You can revoke it manually - on link. - - } - /> - ); + const element = ; expect(renderer.create(element).toJSON()).toMatchSnapshot(); }); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/ProviderWarning.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/ProviderWarning.spec.tsx.snap new file mode 100644 index 000000000..d12fe3f44 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/ProviderWarning.spec.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProviderWarning component should render ProviderWarning correctly 1`] = ` +
+ + + + + + Provided API does not support the automatic token revocation. You can revoke it manually on   + + http://dummy.ref + + . +
+`; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/index.spec.tsx.snap deleted file mode 100644 index 35a3354ee..000000000 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/index.spec.tsx.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ProviderWarning component should render ProviderWarning correctly 1`] = ` -[ - - - , - "Provided API does not support the automatic token revocation. You can revoke it manually on ", - - link - , - ".", -] -`; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/index.tsx index 9ab45d19b..ed774636f 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/index.tsx @@ -10,29 +10,38 @@ * Red Hat, Inc. - initial API and implementation */ -import { Tooltip, TooltipPosition } from '@patternfly/react-core'; import { WarningTriangleIcon } from '@patternfly/react-icons'; import React from 'react'; +import CheTooltip from '@/components/CheTooltip'; + type Props = { - warning: React.ReactNode; + serverURI: string; }; - export default class ProviderWarning extends React.PureComponent { public render(): React.ReactElement { + const content = ( + <> + Provided API does not support the automatic token revocation. You can revoke it manually on +   + + {this.props.serverURI} + + . + + ); + return ( - - - + + + + + ); } } 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 index 81d1c70f3..44c2b98a1 100644 --- 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 @@ -107,9 +107,18 @@ exports[`GitServices should correctly render the component which contains four g > Server - + Authorization + + +
+ + + + + User has been authorized successfully. + +
+ + GitLab - - Provided API does not support the automatic token revocation. You can revoke it manually on   - - https://gitlab.dummy.endpoint.com - - . - +
+ + + + + User has not been authorized yet. + +
+ + Bitbucket Server (OAuth 1.0) - - Provided API does not support the automatic token revocation. You can revoke it manually on   - - https://bitbucket.dummy.endpoint.org - - . - +
+ + + + + User has not been authorized yet. + +
+ + Microsoft Azure DevOps - - Provided API does not support the automatic token revocation. You can revoke it manually on   - - https://azure.dummy.endpoint.com/ - - . - +
+ + + + + User has not been authorized yet. + +
+ + { - return function ProviderWarning(props: { warning: React.ReactNode }): React.ReactElement { - return {props.warning}; - }; -}); describe('GitServices', () => { const mockRevokeOauth = jest.fn(); const requestGitOauthConfig = jest.fn(); + const requestSkipAuthorisationProviders = jest.fn(); + const deleteSkipOauth = jest.fn(); const getComponent = (store: Store): React.ReactElement => { const state = store.getState(); const gitOauth = selectGitOauth(state); const isLoading = selectIsLoading(state); + const providersWithToken = selectProvidersWithToken(state); + const skipOauthProviders = selectSkipOauthProviders(state); return ( ); @@ -71,24 +79,28 @@ describe('GitServices', () => { it('should correctly render the component which contains four git services', () => { const component = getComponent( new FakeStoreBuilder() - .withGitOauthConfig([ - new FakeGitOauthBuilder() - .withName('github') - .withEndpointUrl('https://github.dummy.endpoint.com') - .build(), - new FakeGitOauthBuilder() - .withName('gitlab') - .withEndpointUrl('https://gitlab.dummy.endpoint.com') - .build(), - new FakeGitOauthBuilder() - .withName('bitbucket') - .withEndpointUrl('https://bitbucket.dummy.endpoint.org') - .build(), - new FakeGitOauthBuilder() - .withName('azure-devops') - .withEndpointUrl('https://azure.dummy.endpoint.com/') - .build(), - ]) + .withGitOauthConfig( + [ + new FakeGitOauthBuilder() + .withName('github') + .withEndpointUrl('https://github.dummy.endpoint.com') + .build(), + new FakeGitOauthBuilder() + .withName('gitlab') + .withEndpointUrl('https://gitlab.dummy.endpoint.com') + .build(), + new FakeGitOauthBuilder() + .withName('bitbucket') + .withEndpointUrl('https://bitbucket.dummy.endpoint.org') + .build(), + new FakeGitOauthBuilder() + .withName('azure-devops') + .withEndpointUrl('https://azure.dummy.endpoint.com/') + .build(), + ], + ['github'], + [], + ) .build(), ); render(component); @@ -113,17 +125,22 @@ describe('GitServices', () => { const spyRevokeOauth = jest.spyOn(actionCreators, 'revokeOauth'); const component = getComponent( new FakeStoreBuilder() - .withGitOauthConfig([ - new FakeGitOauthBuilder() - .withName('github') - .withEndpointUrl('https://github.com') - .build(), - ]) + .withGitOauthConfig( + [ + new FakeGitOauthBuilder() + .withName('github') + .withEndpointUrl('https://github.com') + .build(), + ], + ['github'], + [], + ) .build(), ); render(component); const menuButton = screen.getByLabelText('Actions'); + expect(menuButton).not.toBeDisabled(); userEvent.click(menuButton); const revokeItem = screen.getByRole('menuitem', { name: /Revoke/i }); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx index 7a8d86d45..447efe755 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx @@ -12,7 +12,7 @@ import { api } from '@eclipse-che/common'; import { PageSection } from '@patternfly/react-core'; -import { Table, TableBody, TableHeader } from '@patternfly/react-table'; +import { IActionsResolver, OnSelect, Table, TableBody, TableHeader } from '@patternfly/react-table'; import React from 'react'; import { connect, ConnectedProps } from 'react-redux'; @@ -22,10 +22,16 @@ import EmptyState from '@/pages/UserPreferences/GitServicesTab/EmptyState'; import GitServicesToolbar, { GitServicesToolbar as Toolbar, } from '@/pages/UserPreferences/GitServicesTab/GitServicesToolbar'; +import ProviderIcon from '@/pages/UserPreferences/GitServicesTab/ProviderIcon'; import ProviderWarning from '@/pages/UserPreferences/GitServicesTab/ProviderWarning'; import { AppState } from '@/store'; import * as GitOauthConfig from '@/store/GitOauthConfig'; -import { selectGitOauth, selectIsLoading } from '@/store/GitOauthConfig/selectors'; +import { + selectGitOauth, + selectIsLoading, + selectProvidersWithToken, + selectSkipOauthProviders, +} from '@/store/GitOauthConfig/selectors'; export const enabledProviders: api.GitOauthProvider[] = ['github', 'github_2']; @@ -71,36 +77,18 @@ export class GitServices extends React.PureComponent { public async componentDidMount(): Promise { const { isLoading, requestGitOauthConfig } = this.props; if (!isLoading) { - requestGitOauthConfig(); + await requestGitOauthConfig(); } } private buildGitOauthRow(gitOauth: api.GitOauthProvider, server: string): React.ReactNode[] { const oauthRow: React.ReactNode[] = []; - const isDisabled = this.isDisabled(gitOauth); + const hasWarningMessage = this.isDisabled(gitOauth) && this.hasOauthToken(gitOauth); oauthRow.push( {GIT_OAUTH_PROVIDERS[gitOauth]} - {isDisabled && ( - - Provided API does not support the automatic token revocation. You can revoke it - manually on   - - {server} - - . - - } - /> - )} + {hasWarningMessage && } , ); @@ -112,37 +100,74 @@ export class GitServices extends React.PureComponent { , ); + oauthRow.push( + + + , + ); + return oauthRow; } - private showOnRevokeGitOauthModal(rowIndex: number): void { - this.gitServicesToolbarRef.current?.showOnRevokeGitOauthModal(rowIndex); + private isDisabled(providerName: api.GitOauthProvider): boolean { + return !enabledProviders.includes(providerName) || !this.hasOauthToken(providerName); } - private isDisabled(providerName: api.GitOauthProvider): boolean { - return !enabledProviders.includes(providerName); + private isSkipOauth(providerName: api.GitOauthProvider): boolean { + return this.props.skipOauthProviders.includes(providerName); + } + + private hasOauthToken(providerName: api.GitOauthProvider): boolean { + return this.props.providersWithToken.includes(providerName); } 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 columns = ['Name', 'Server', 'Authorization']; const rows = gitOauth.length > 0 - ? gitOauth.map(provider => ({ - cells: this.buildGitOauthRow(provider.name, provider.endpointUrl), - selected: selectedItems.includes(provider.name), - disableSelection: this.isDisabled(provider.name), - disableActions: this.isDisabled(provider.name), - })) + ? gitOauth.map(provider => { + const canRevoke = !this.isDisabled(provider.name); + const canClear = this.isSkipOauth(provider.name); + return { + cells: this.buildGitOauthRow(provider.name, provider.endpointUrl), + selected: selectedItems.includes(provider.name), + disableSelection: !canRevoke, + disableActions: !canRevoke && !canClear, + isValid: !this.isSkipOauth(provider.name), + }; + }) : []; + const actionResolver: IActionsResolver = rowData => { + if (!rowData.isValid) { + return [ + { + title: 'Clear', + onClick: (event, rowIndex) => { + event.stopPropagation(); + this.props.deleteSkipOauth(gitOauth[rowIndex].name); + }, + }, + ]; + } + return [ + { + title: 'Revoke', + onClick: (event, rowIndex) => { + event.stopPropagation(); + this.gitServicesToolbarRef.current?.showOnRevokeGitOauthModal(rowIndex); + }, + }, + ]; + }; + + const onSelect: OnSelect = (event, isSelected, rowIndex) => { + event.stopPropagation(); + this.onChangeSelection(isSelected, rowIndex); + }; + return ( @@ -158,12 +183,10 @@ export class GitServices extends React.PureComponent { /> !!rowData.disableActions} rows={rows} - onSelect={(event, isSelected, rowIndex) => { - this.onChangeSelection(isSelected, rowIndex); - }} + onSelect={onSelect} canSelectAll={false} aria-label="Git services" variant="compact" @@ -181,6 +204,8 @@ export class GitServices extends React.PureComponent { const mapStateToProps = (state: AppState) => ({ gitOauth: selectGitOauth(state), + providersWithToken: selectProvidersWithToken(state), + skipOauthProviders: selectSkipOauthProviders(state), isLoading: selectIsLoading(state), }); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx index 37beedd68..fd397025b 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx @@ -77,6 +77,7 @@ export class UserPreferences extends React.PureComponent { _event: React.MouseEvent, activeTabKey: React.ReactText, ): void { + _event.stopPropagation(); this.props.history.push(`${ROUTE.USER_PREFERENCES}?tab=${activeTabKey}`); this.setState({ diff --git a/packages/dashboard-frontend/src/services/backend-client/oAuthApi.ts b/packages/dashboard-frontend/src/services/backend-client/oAuthApi.ts index ec85fb22e..38a4521ae 100644 --- a/packages/dashboard-frontend/src/services/backend-client/oAuthApi.ts +++ b/packages/dashboard-frontend/src/services/backend-client/oAuthApi.ts @@ -13,7 +13,8 @@ import { api } from '@eclipse-che/common'; import axios from 'axios'; -import { cheServerPrefix } from '@/services/backend-client/const'; +import { AxiosWrapper } from '@/services/axios-wrapper/axiosWrapper'; +import { cheServerPrefix, dashboardBackendPrefix } from '@/services/backend-client/const'; import { IGitOauth } from '@/store/GitOauthConfig/types'; export async function getOAuthProviders(): Promise { @@ -33,3 +34,24 @@ export async function deleteOAuthToken(provider: api.GitOauthProvider): Promise< return Promise.resolve(); } + +export async function getDevWorkspacePreferences( + namespace: string, +): Promise { + const response = await AxiosWrapper.createToRetryMissedBearerTokenError().get( + `${dashboardBackendPrefix}/workspace-preferences/namespace/${namespace}`, + ); + + return response.data; +} + +export async function deleteSkipOauthProvider( + namespace: string, + provider: api.GitOauthProvider, +): Promise { + await AxiosWrapper.createToRetryMissedBearerTokenError().delete( + `${dashboardBackendPrefix}/workspace-preferences/namespace/${namespace}/skip-authorisation/${provider}`, + ); + + return Promise.resolve(); +} diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts index 71b5e7979..5d1144606 100644 --- a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts @@ -15,11 +15,14 @@ import { Action, Reducer } from 'redux'; import { deleteOAuthToken, + deleteSkipOauthProvider, + getDevWorkspacePreferences, getOAuthProviders, getOAuthToken, } from '@/services/backend-client/oAuthApi'; import { IGitOauth } from '@/store/GitOauthConfig/types'; import { createObject } from '@/store/helpers'; +import { selectDefaultNamespace } from '@/store/InfrastructureNamespaces/selectors'; import { selectAsyncIsAuthorized, selectSanityCheckError } from '@/store/SanityCheck/selectors'; import { AUTHORIZED } from '@/store/sanityCheckMiddleware'; @@ -28,88 +31,136 @@ import { AppThunk } from '..'; export interface State { isLoading: boolean; gitOauth: IGitOauth[]; + providersWithToken: api.GitOauthProvider[]; + skipOauthProviders: api.GitOauthProvider[]; error: string | undefined; } 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', + REQUEST_GIT_OAUTH = 'REQUEST_GIT_OAUTH', + DELETE_GIT_OAUTH_TOKEN = 'DELETE_GIT_OAUTH_TOKEN', + RECEIVE_GIT_OAUTH_PROVIDERS = 'RECEIVE_GIT_OAUTH_PROVIDERS', + RECEIVE_SKIP_OAUTH_PROVIDERS = 'RECEIVE_SKIP_OAUTH_PROVIDERS', + DELETE_SKIP_OAUTH = 'DELETE_SKIP_OAUTH', + RECEIVE_GIT_OAUTH_ERROR = 'RECEIVE_GIT_OAUTH_ERROR', } -export interface RequestGitOauthConfigAction extends Action { - type: Type.REQUEST_GIT_OAUTH_CONFIG; +export interface RequestGitOAuthAction extends Action { + type: Type.REQUEST_GIT_OAUTH; } export interface DeleteOauthAction extends Action { - type: Type.DELETE_OAUTH; + type: Type.DELETE_GIT_OAUTH_TOKEN; provider: api.GitOauthProvider; } -export interface ReceiveGitOauthConfigAction extends Action { - type: Type.RECEIVE_GIT_OAUTH_CONFIG; - gitOauth: IGitOauth[]; +export interface ReceiveGitOAuthConfigAction extends Action { + type: Type.RECEIVE_GIT_OAUTH_PROVIDERS; + supportedGitOauth: IGitOauth[]; + providersWithToken: api.GitOauthProvider[]; } -export interface ReceivedGitOauthConfigErrorAction extends Action { - type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR; +export interface ReceivedGitOauthErrorAction extends Action { + type: Type.RECEIVE_GIT_OAUTH_ERROR; error: string; } +export interface ReceiveSkipOauthProvidersAction extends Action { + type: Type.RECEIVE_SKIP_OAUTH_PROVIDERS; + skipOauthProviders: api.GitOauthProvider[]; +} + export type KnownAction = - | RequestGitOauthConfigAction + | RequestGitOAuthAction + | ReceiveGitOAuthConfigAction + | ReceiveSkipOauthProvidersAction | DeleteOauthAction - | ReceiveGitOauthConfigAction - | ReceivedGitOauthConfigErrorAction; + | ReceivedGitOauthErrorAction; export type ActionCreators = { + requestSkipAuthorisationProviders: () => AppThunk>; requestGitOauthConfig: () => AppThunk>; revokeOauth: (oauthProvider: api.GitOauthProvider) => AppThunk>; + deleteSkipOauth: (oauthProvider: api.GitOauthProvider) => AppThunk>; }; export const actionCreators: ActionCreators = { + requestSkipAuthorisationProviders: + (): AppThunk> => + async (dispatch, getState): Promise => { + dispatch({ + type: Type.REQUEST_GIT_OAUTH, + check: AUTHORIZED, + }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_ERROR, + error, + }); + throw new Error(error); + } + + const defaultKubernetesNamespace = selectDefaultNamespace(getState()); + try { + const devWorkspacePreferences = await getDevWorkspacePreferences( + defaultKubernetesNamespace.name, + ); + + const skipOauthProviders = devWorkspacePreferences['skip-authorisation'] || []; + dispatch({ + type: Type.RECEIVE_SKIP_OAUTH_PROVIDERS, + skipOauthProviders, + }); + } catch (e) { + const errorMessage = common.helpers.errors.getMessage(e); + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_ERROR, + error: errorMessage, + }); + throw e; + } + }, + requestGitOauthConfig: (): AppThunk> => async (dispatch, getState): Promise => { dispatch({ - type: Type.REQUEST_GIT_OAUTH_CONFIG, + type: Type.REQUEST_GIT_OAUTH, check: AUTHORIZED, }); if (!(await selectAsyncIsAuthorized(getState()))) { const error = selectSanityCheckError(getState()); dispatch({ - type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR, + type: Type.RECEIVE_GIT_OAUTH_ERROR, error, }); throw new Error(error); } - const gitOauth: IGitOauth[] = []; + const providersWithToken: api.GitOauthProvider[] = []; try { - const oAuthProviders = await getOAuthProviders(); + const supportedGitOauth = await getOAuthProviders(); const promises: Promise[] = []; - for (const { name, endpointUrl, links } of oAuthProviders) { + for (const { name } of supportedGitOauth) { promises.push( getOAuthToken(name).then(() => { - gitOauth.push({ - name: name as api.GitOauthProvider, - endpointUrl, - links, - }); + providersWithToken.push(name); }), ); } + promises.push(dispatch(actionCreators.requestSkipAuthorisationProviders())); await Promise.allSettled(promises); dispatch({ - type: Type.RECEIVE_GIT_OAUTH_CONFIG, - gitOauth, + type: Type.RECEIVE_GIT_OAUTH_PROVIDERS, + supportedGitOauth, + providersWithToken, }); } catch (e) { const errorMessage = common.helpers.errors.getMessage(e); dispatch({ - type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR, + type: Type.RECEIVE_GIT_OAUTH_ERROR, error: errorMessage, }); throw e; @@ -120,13 +171,13 @@ export const actionCreators: ActionCreators = { (oauthProvider: api.GitOauthProvider): AppThunk> => async (dispatch, getState): Promise => { dispatch({ - type: Type.REQUEST_GIT_OAUTH_CONFIG, + type: Type.REQUEST_GIT_OAUTH, check: AUTHORIZED, }); if (!(await selectAsyncIsAuthorized(getState()))) { const error = selectSanityCheckError(getState()); dispatch({ - type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR, + type: Type.RECEIVE_GIT_OAUTH_ERROR, error, }); throw new Error(error); @@ -135,13 +186,43 @@ export const actionCreators: ActionCreators = { try { await deleteOAuthToken(oauthProvider); dispatch({ - type: Type.DELETE_OAUTH, + type: Type.DELETE_GIT_OAUTH_TOKEN, provider: oauthProvider, }); } catch (e) { const errorMessage = common.helpers.errors.getMessage(e); dispatch({ - type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR, + type: Type.RECEIVE_GIT_OAUTH_ERROR, + error: errorMessage, + }); + throw e; + } + }, + + deleteSkipOauth: + (oauthProvider: api.GitOauthProvider): AppThunk> => + async (dispatch, getState): Promise => { + dispatch({ + type: Type.REQUEST_GIT_OAUTH, + check: AUTHORIZED, + }); + if (!(await selectAsyncIsAuthorized(getState()))) { + const error = selectSanityCheckError(getState()); + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_ERROR, + error, + }); + throw new Error(error); + } + + const defaultKubernetesNamespace = selectDefaultNamespace(getState()); + try { + await deleteSkipOauthProvider(defaultKubernetesNamespace.name, oauthProvider); + await dispatch(actionCreators.requestSkipAuthorisationProviders()); + } catch (e) { + const errorMessage = common.helpers.errors.getMessage(e); + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_ERROR, error: errorMessage, }); throw e; @@ -152,6 +233,8 @@ export const actionCreators: ActionCreators = { const unloadedState: State = { isLoading: false, gitOauth: [], + providersWithToken: [], + skipOauthProviders: [], error: undefined, }; @@ -165,22 +248,30 @@ export const reducer: Reducer = ( const action = incomingAction as KnownAction; switch (action.type) { - case Type.REQUEST_GIT_OAUTH_CONFIG: + case Type.REQUEST_GIT_OAUTH: return createObject(state, { isLoading: true, error: undefined, }); - case Type.RECEIVE_GIT_OAUTH_CONFIG: + case Type.RECEIVE_GIT_OAUTH_PROVIDERS: + return createObject(state, { + isLoading: false, + gitOauth: action.supportedGitOauth, + providersWithToken: action.providersWithToken, + }); + case Type.RECEIVE_SKIP_OAUTH_PROVIDERS: return createObject(state, { isLoading: false, - gitOauth: action.gitOauth, + skipOauthProviders: action.skipOauthProviders, }); - case Type.DELETE_OAUTH: + case Type.DELETE_GIT_OAUTH_TOKEN: return createObject(state, { isLoading: false, - gitOauth: state.gitOauth.filter(v => v.name !== action.provider), + providersWithToken: state.providersWithToken.filter( + provider => provider !== action.provider, + ), }); - case Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR: + case Type.RECEIVE_GIT_OAUTH_ERROR: return createObject(state, { isLoading: false, error: action.error, diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts index 2fb1c6253..e470786f0 100644 --- a/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts @@ -26,6 +26,14 @@ export const selectGitOauth = createSelector(selectState, (state: State) => { return state.gitOauth; }); +export const selectProvidersWithToken = createSelector(selectState, (state: State) => { + return state.providersWithToken; +}); + +export const selectSkipOauthProviders = createSelector(selectState, (state: State) => { + return state.skipOauthProviders; +}); + 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 index b63499dd7..8216df511 100644 --- a/packages/dashboard-frontend/src/store/GitOauthConfig/types.ts +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/types.ts @@ -11,10 +11,10 @@ */ import * as cheApi from '@eclipse-che/api'; -import { api as commonApi } from '@eclipse-che/common'; +import { api } from '@eclipse-che/common'; export interface IGitOauth { - name: commonApi.GitOauthProvider; + name: api.GitOauthProvider; endpointUrl: string; links?: cheApi.che.core.rest.Link[]; } diff --git a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts index 15a2e1042..a8501277b 100644 --- a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts +++ b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts @@ -126,6 +126,8 @@ export class FakeStoreBuilder { gitOauthConfig: { isLoading: false, gitOauth: [], + providersWithToken: [], + skipOauthProviders: [], error: undefined, }, devfileRegistries: { @@ -198,12 +200,16 @@ export class FakeStoreBuilder { public withGitOauthConfig( gitOauth: IGitOauth[], + providersWithToken: api.GitOauthProvider[], + skipOauthProviders: api.GitOauthProvider[], isLoading = false, error?: string, ): FakeStoreBuilder { this.state.gitOauthConfig.gitOauth = gitOauth; - this.state.dockerConfig.isLoading = isLoading; - this.state.dockerConfig.error = error; + this.state.gitOauthConfig.providersWithToken = providersWithToken; + this.state.gitOauthConfig.skipOauthProviders = skipOauthProviders; + this.state.gitOauthConfig.isLoading = isLoading; + this.state.gitOauthConfig.error = error; return this; } diff --git a/packages/dashboard-frontend/src/utils/che-tooltip.ts b/packages/dashboard-frontend/src/utils/che-tooltip.ts new file mode 100644 index 000000000..970f01518 --- /dev/null +++ b/packages/dashboard-frontend/src/utils/che-tooltip.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2018-2023 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 { TooltipPosition } from '@patternfly/react-core'; +import React from 'react'; + +jest.mock('@/components/CheTooltip', () => { + return function CheTooltip(props: { + children: React.ReactElement; + content: React.ReactNode; + position?: TooltipPosition; + }): React.ReactElement { + return React.createElement('div', null, props.children, props.content); + }; +}); From d85abfaccba0c102bb3075d793069bdc37ad6fe5 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Fri, 10 Nov 2023 17:06:06 +0200 Subject: [PATCH 3/5] fix: code cleanup Signed-off-by: Oleksii Kurinnyi --- .../services/devWorkspacePreferencesApi.ts | 23 ++-- .../src/devworkspaceClient/types/index.ts | 6 +- .../src/routes/api/workspacePreferences.ts | 5 +- .../__tests__/ProviderIcon.spec.tsx | 8 +- .../__snapshots__/ProviderIcon.spec.tsx.snap | 22 ++-- ...ning.spec.tsx.snap => index.spec.tsx.snap} | 0 ...roviderWarning.spec.tsx => index.spec.tsx} | 8 +- .../__snapshots__/index.spec.tsx.snap | 6 +- .../GitServicesTab/__tests__/index.spec.tsx | 105 ++++++++++-------- .../UserPreferences/GitServicesTab/index.tsx | 42 +++---- .../src/pages/UserPreferences/index.tsx | 6 +- .../src/services/workspace-client/helpers.ts | 3 +- .../src/store/GitOauthConfig/index.ts | 8 +- 13 files changed, 131 insertions(+), 111 deletions(-) rename packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/{ProviderWarning.spec.tsx.snap => index.spec.tsx.snap} (100%) rename packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/{ProviderWarning.spec.tsx => index.spec.tsx} (64%) diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/devWorkspacePreferencesApi.ts b/packages/dashboard-backend/src/devworkspaceClient/services/devWorkspacePreferencesApi.ts index 1c420a8b2..4f189f451 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/devWorkspacePreferencesApi.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/devWorkspacePreferencesApi.ts @@ -11,7 +11,6 @@ */ import { api } from '@eclipse-che/common'; -import { GitProvider } from '@eclipse-che/common/lib/dto/api'; import * as k8s from '@kubernetes/client-node'; import { createError } from '@/devworkspaceClient/services/helpers/createError'; @@ -24,7 +23,7 @@ import { IDevWorkspacePreferencesApi } from '@/devworkspaceClient/types'; const ERROR_LABEL = 'CORE_V1_API_ERROR'; const DEV_WORKSPACE_PREFERENCES_CONFIGMAP = 'workspace-preferences-configmap'; -const SKIP_AUTORIZATION_KEY = 'skip-authorisation'; +const SKIP_AUTHORIZATION_KEY = 'skip-authorisation'; export class DevWorkspacePreferencesApiService implements IDevWorkspacePreferencesApi { private readonly coreV1API: CoreV1API; @@ -44,32 +43,32 @@ export class DevWorkspacePreferencesApiService implements IDevWorkspacePreferenc throw new Error('Data is empty'); } - const skipAuthorisation = - data[SKIP_AUTORIZATION_KEY] && data[SKIP_AUTORIZATION_KEY] !== '[]' - ? data[SKIP_AUTORIZATION_KEY].replace(/^\[/, '').replace(/\]$/, '').split(', ') + const skipAuthorization = + data[SKIP_AUTHORIZATION_KEY] && data[SKIP_AUTHORIZATION_KEY] !== '[]' + ? data[SKIP_AUTHORIZATION_KEY].replace(/^\[/, '').replace(/\]$/, '').split(', ') : []; return Object.assign({}, data, { - [SKIP_AUTORIZATION_KEY]: skipAuthorisation, + [SKIP_AUTHORIZATION_KEY]: skipAuthorization, }) as api.IDevWorkspacePreferences; } catch (e) { throw createError(e, ERROR_LABEL, 'Unable to get workspace preferences data'); } } - public async removeProviderFromSkipAuthorization( + public async removeProviderFromSkipAuthorizationList( namespace: string, - provider: GitProvider, + provider: api.GitProvider, ): Promise { const devWorkspacePreferences = await this.getWorkspacePreferences(namespace); - const skipAuthorisation = devWorkspacePreferences[SKIP_AUTORIZATION_KEY].filter( + const skipAuthorization = devWorkspacePreferences[SKIP_AUTHORIZATION_KEY].filter( (val: string) => val !== provider, ); - const skipAuthorisationStr = - skipAuthorisation.length > 0 ? `[${skipAuthorisation.join(', ')}]` : '[]'; + const skipAuthorizationStr = + skipAuthorization.length > 0 ? `[${skipAuthorization.sort().join(', ')}]` : '[]'; const data = Object.assign({}, devWorkspacePreferences, { - [SKIP_AUTORIZATION_KEY]: skipAuthorisationStr, + [SKIP_AUTHORIZATION_KEY]: skipAuthorizationStr, }); try { diff --git a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts index 23849f6b4..64bc95dc8 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts @@ -16,7 +16,6 @@ import { V221DevfileComponents, } from '@devfile/api'; import { api } from '@eclipse-che/common'; -import { GitProvider } from '@eclipse-che/common/lib/dto/api'; import * as k8s from '@kubernetes/client-node'; import { IncomingHttpHeaders } from 'http'; @@ -358,7 +357,10 @@ export interface IDevWorkspacePreferencesApi { /** * Removes the target provider from skip-authorisation property from the workspace preferences object. */ - removeProviderFromSkipAuthorization(namespace: string, provider: GitProvider): Promise; + removeProviderFromSkipAuthorizationList( + namespace: string, + provider: api.GitProvider, + ): Promise; } export interface IPersonalAccessTokenApi { diff --git a/packages/dashboard-backend/src/routes/api/workspacePreferences.ts b/packages/dashboard-backend/src/routes/api/workspacePreferences.ts index 99a2428b0..8771a096d 100644 --- a/packages/dashboard-backend/src/routes/api/workspacePreferences.ts +++ b/packages/dashboard-backend/src/routes/api/workspacePreferences.ts @@ -50,7 +50,10 @@ export function registerWorkspacePreferencesRoute(instance: FastifyInstance) { const { namespace, provider } = request.params as restParams.IWorkspacePreferencesParams; const token = getToken(request); const { devWorkspacePreferencesApi } = getDevWorkspaceClient(token); - await devWorkspacePreferencesApi.removeProviderFromSkipAuthorization(namespace, provider); + await devWorkspacePreferencesApi.removeProviderFromSkipAuthorizationList( + namespace, + provider, + ); reply.code(204); return reply.send(); }, diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/ProviderIcon.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/ProviderIcon.spec.tsx index f4d6429e7..e99130345 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/ProviderIcon.spec.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/ProviderIcon.spec.tsx @@ -39,7 +39,7 @@ function getComponent( gitProvider={gitOauth} providersWithToken={selectProvidersWithToken(state)} skipOauthProviders={selectSkipOauthProviders(state)} - requestSkipAuthorisationProviders={jest.fn()} + requestSkipAuthorizationProviders={jest.fn()} requestGitOauthConfig={jest.fn()} revokeOauth={jest.fn()} deleteSkipOauth={jest.fn()} @@ -49,7 +49,7 @@ function getComponent( } describe('ProviderIcon component', () => { - it('should render ProviderIcon component correctly when the user has been authorized successfully.', () => { + test('snapshot for the successfully authorized provider', () => { const gitOauth: api.GitOauthProvider = 'github'; const store = new FakeStoreBuilder().withGitOauthConfig([], ['github'], []).build(); @@ -58,7 +58,7 @@ describe('ProviderIcon component', () => { expect(snapshot.toJSON()).toMatchSnapshot(); }); - it('should render ProviderIcon component correctly when authorization has been rejected by user.', () => { + test('snapshot for rejected provider', () => { const gitOauth: api.GitOauthProvider = 'github'; const store = new FakeStoreBuilder().withGitOauthConfig([], [], ['github']).build(); @@ -67,7 +67,7 @@ describe('ProviderIcon component', () => { expect(snapshot.toJSON()).toMatchSnapshot(); }); - it('should render ProviderIcon component correctly when the user has not been authorized yet.', () => { + test('snapshot for the provider not authorized yet', () => { const gitOauth: api.GitOauthProvider = 'github'; const store = new FakeStoreBuilder().withGitOauthConfig([], [], []).build(); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/__snapshots__/ProviderIcon.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/__snapshots__/ProviderIcon.spec.tsx.snap index df790b5af..d6fc14d17 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/__snapshots__/ProviderIcon.spec.tsx.snap +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/__tests__/__snapshots__/ProviderIcon.spec.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ProviderIcon component should render ProviderIcon component correctly when authorization has been rejected by user. 1`] = ` +exports[`ProviderIcon component snapshot for rejected provider 1`] = `
`; -exports[`ProviderIcon component should render ProviderIcon component correctly when the user has been authorized successfully. 1`] = ` +exports[`ProviderIcon component snapshot for the provider not authorized yet 1`] = `
- User has been authorized successfully. + User has not been authorized yet.
`; -exports[`ProviderIcon component should render ProviderIcon component correctly when the user has not been authorized yet. 1`] = ` +exports[`ProviderIcon component snapshot for the successfully authorized provider 1`] = `
- User has not been authorized yet. + User has been authorized successfully.
`; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/ProviderWarning.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/index.spec.tsx.snap similarity index 100% rename from packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/ProviderWarning.spec.tsx.snap rename to packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/__snapshots__/index.spec.tsx.snap diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/ProviderWarning.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/index.spec.tsx similarity index 64% rename from packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/ProviderWarning.spec.tsx rename to packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/index.spec.tsx index 69ac04bdb..cfe39f3d3 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/ProviderWarning.spec.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/__tests__/index.spec.tsx @@ -11,14 +11,16 @@ */ import React from 'react'; -import renderer from 'react-test-renderer'; + +import getComponentRenderer from '@/services/__mocks__/getComponentRenderer'; import ProviderWarning from '..'; describe('ProviderWarning component', () => { it('should render ProviderWarning correctly', () => { - const element = ; + const getComponent = () => ; + const { createSnapshot } = getComponentRenderer(getComponent); - expect(renderer.create(element).toJSON()).toMatchSnapshot(); + expect(createSnapshot().toJSON()).toMatchSnapshot(); }); }); 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 index 44c2b98a1..50a769845 100644 --- 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 @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`GitServices should correctly render the component which contains four git services 1`] = ` +exports[`GitServices with 4 git services snapshot 1`] = ` [

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 48e492e30..b7dea1613 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 @@ -10,14 +10,13 @@ * Red Hat, Inc. - initial API and implementation */ -import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; -import renderer from 'react-test-renderer'; import { Store } from 'redux'; import { FakeGitOauthBuilder } from '@/pages/UserPreferences/GitServicesTab/__tests__/__mocks__/gitOauthRowBuilder'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; import { actionCreators } from '@/store/GitOauthConfig'; import { @@ -35,10 +34,12 @@ console.error = jest.fn(); describe('GitServices', () => { const mockRevokeOauth = jest.fn(); const requestGitOauthConfig = jest.fn(); - const requestSkipAuthorisationProviders = jest.fn(); + const requestSkipAuthorizationProviders = jest.fn(); const deleteSkipOauth = jest.fn(); - const getComponent = (store: Store): React.ReactElement => { + const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); + + function getComponent(store: Store): React.ReactElement { const state = store.getState(); const gitOauth = selectGitOauth(state); const isLoading = selectIsLoading(state); @@ -52,33 +53,44 @@ describe('GitServices', () => { revokeOauth={mockRevokeOauth} deleteSkipOauth={deleteSkipOauth} requestGitOauthConfig={requestGitOauthConfig} - requestSkipAuthorisationProviders={requestSkipAuthorisationProviders} + requestSkipAuthorizationProviders={requestSkipAuthorizationProviders} providersWithToken={providersWithToken} skipOauthProviders={skipOauthProviders} /> ); - }; + } afterEach(() => { jest.clearAllMocks(); }); - it('should correctly render the component without git services', () => { - const component = getComponent(new FakeStoreBuilder().build()); - render(component); + describe('without git services', () => { + let store: Store; + + beforeEach(() => { + store = new FakeStoreBuilder().build(); + }); - const emptyStateText = screen.queryByText('No Git Services'); - expect(emptyStateText).toBeTruthy(); + test('snapshot', () => { + const snapshot = createSnapshot(store); - const json = renderer.create(component).toJSON(); + expect(snapshot.toJSON()).toMatchSnapshot(); + }); - expect(json).toMatchSnapshot(); + test('empty state text', () => { + renderComponent(store); + + const emptyStateText = screen.queryByText('No Git Services'); + expect(emptyStateText).toBeTruthy(); + }); }); - it('should correctly render the component which contains four git services', () => { - const component = getComponent( - new FakeStoreBuilder() + describe('with 4 git services', () => { + let store: Store; + + beforeEach(() => { + store = new FakeStoreBuilder() .withGitOauthConfig( [ new FakeGitOauthBuilder() @@ -101,43 +113,46 @@ describe('GitServices', () => { ['github'], [], ) - .build(), - ); - render(component); + .build(); + }); + + test('providers actions depending on authorization state', () => { + renderComponent(store); - const emptyStateText = screen.queryByText('No Git Services'); - expect(emptyStateText).not.toBeTruthy(); + const emptyStateText = screen.queryByText('No Git Services'); + expect(emptyStateText).not.toBeTruthy(); - const actions = screen.queryAllByRole('button', { name: /actions/i }); + const actions = screen.queryAllByRole('button', { name: /actions/i }); - expect(actions.length).toEqual(4); - expect(actions[0]).not.toBeDisabled(); - expect(actions[1]).toBeDisabled(); - expect(actions[2]).toBeDisabled(); - expect(actions[3]).toBeDisabled(); + expect(actions.length).toEqual(4); + expect(actions[0]).not.toBeDisabled(); + expect(actions[1]).toBeDisabled(); + expect(actions[2]).toBeDisabled(); + expect(actions[3]).toBeDisabled(); + }); - const json = renderer.create(component).toJSON(); + test('snapshot', () => { + const snapshot = createSnapshot(store); - expect(json).toMatchSnapshot(); + expect(snapshot.toJSON()).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(), - ], - ['github'], - [], - ) - .build(), - ); - render(component); + const store = new FakeStoreBuilder() + .withGitOauthConfig( + [ + new FakeGitOauthBuilder() + .withName('github') + .withEndpointUrl('https://github.com') + .build(), + ], + ['github'], + [], + ) + .build(); + renderComponent(store); const menuButton = screen.getByLabelText('Actions'); expect(menuButton).not.toBeDisabled(); @@ -157,6 +172,6 @@ describe('GitServices', () => { expect(revokeButton).toBeEnabled(); userEvent.click(revokeButton); - expect(spyRevokeOauth).toBeCalledWith('github'); + expect(spyRevokeOauth).toHaveBeenLastCalledWith('github'); }); }); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx index 447efe755..166c1336d 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx @@ -12,7 +12,7 @@ import { api } from '@eclipse-che/common'; import { PageSection } from '@patternfly/react-core'; -import { IActionsResolver, OnSelect, Table, TableBody, TableHeader } from '@patternfly/react-table'; +import { IActionsResolver, Table, TableBody, TableHeader } from '@patternfly/react-table'; import React from 'react'; import { connect, ConnectedProps } from 'react-redux'; @@ -81,36 +81,38 @@ export class GitServices extends React.PureComponent { } } - private buildGitOauthRow(gitOauth: api.GitOauthProvider, server: string): React.ReactNode[] { - const oauthRow: React.ReactNode[] = []; + private buildGitOauthRow(gitOauth: api.GitOauthProvider, serverUrl: string): React.ReactNode[] { const hasWarningMessage = this.isDisabled(gitOauth) && this.hasOauthToken(gitOauth); - oauthRow.push( + const name = ( {GIT_OAUTH_PROVIDERS[gitOauth]} - {hasWarningMessage && } - , + {hasWarningMessage && } + ); - oauthRow.push( - - - {server} + const server = ( + + + {serverUrl} - , + ); - oauthRow.push( + const authorization = ( - , + ); - return oauthRow; + return [name, server, authorization]; } private isDisabled(providerName: api.GitOauthProvider): boolean { - return !enabledProviders.includes(providerName) || !this.hasOauthToken(providerName); + return ( + enabledProviders.includes(providerName) === false || + this.hasOauthToken(providerName) === false + ); } private isSkipOauth(providerName: api.GitOauthProvider): boolean { @@ -163,11 +165,6 @@ export class GitServices extends React.PureComponent { ]; }; - const onSelect: OnSelect = (event, isSelected, rowIndex) => { - event.stopPropagation(); - this.onChangeSelection(isSelected, rowIndex); - }; - return ( @@ -186,7 +183,10 @@ export class GitServices extends React.PureComponent { actionResolver={actionResolver} areActionsDisabled={rowData => !!rowData.disableActions} rows={rows} - onSelect={onSelect} + onSelect={(event, isSelected, rowIndex) => { + event.stopPropagation(); + this.onChangeSelection(isSelected, rowIndex); + }} canSelectAll={false} aria-label="Git services" variant="compact" diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx index fd397025b..76a1e5db1 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx @@ -74,10 +74,10 @@ export class UserPreferences extends React.PureComponent { } private handleTabClick( - _event: React.MouseEvent, - activeTabKey: React.ReactText, + event: React.MouseEvent, + activeTabKey: string | number, ): void { - _event.stopPropagation(); + event.stopPropagation(); this.props.history.push(`${ROUTE.USER_PREFERENCES}?tab=${activeTabKey}`); this.setState({ diff --git a/packages/dashboard-frontend/src/services/workspace-client/helpers.ts b/packages/dashboard-frontend/src/services/workspace-client/helpers.ts index da25f9d4b..728611c18 100644 --- a/packages/dashboard-frontend/src/services/workspace-client/helpers.ts +++ b/packages/dashboard-frontend/src/services/workspace-client/helpers.ts @@ -11,7 +11,6 @@ */ import common from '@eclipse-che/common'; -import { includesAxiosResponse } from '@eclipse-che/common/lib/helpers/errors'; import { dump, load } from 'js-yaml'; import { ThunkDispatch } from 'redux-thunk'; @@ -50,7 +49,7 @@ export function getErrorMessage(error: unknown): string { * Checks for login page in the axios response data */ export function hasLoginPage(error: unknown): boolean { - if (includesAxiosResponse(error)) { + if (common.helpers.errors.includesAxiosResponse(error)) { const response = error.response; if (typeof response.data === 'string') { try { diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts index 5d1144606..d34fe7bcc 100644 --- a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts @@ -78,14 +78,14 @@ export type KnownAction = | ReceivedGitOauthErrorAction; export type ActionCreators = { - requestSkipAuthorisationProviders: () => AppThunk>; + requestSkipAuthorizationProviders: () => AppThunk>; requestGitOauthConfig: () => AppThunk>; revokeOauth: (oauthProvider: api.GitOauthProvider) => AppThunk>; deleteSkipOauth: (oauthProvider: api.GitOauthProvider) => AppThunk>; }; export const actionCreators: ActionCreators = { - requestSkipAuthorisationProviders: + requestSkipAuthorizationProviders: (): AppThunk> => async (dispatch, getState): Promise => { dispatch({ @@ -149,7 +149,7 @@ export const actionCreators: ActionCreators = { }), ); } - promises.push(dispatch(actionCreators.requestSkipAuthorisationProviders())); + promises.push(dispatch(actionCreators.requestSkipAuthorizationProviders())); await Promise.allSettled(promises); dispatch({ @@ -218,7 +218,7 @@ export const actionCreators: ActionCreators = { const defaultKubernetesNamespace = selectDefaultNamespace(getState()); try { await deleteSkipOauthProvider(defaultKubernetesNamespace.name, oauthProvider); - await dispatch(actionCreators.requestSkipAuthorisationProviders()); + await dispatch(actionCreators.requestSkipAuthorizationProviders()); } catch (e) { const errorMessage = common.helpers.errors.getMessage(e); dispatch({ From 5174f2458bdefff9026049a6263c25ce055698d3 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 13 Nov 2023 16:13:07 +0200 Subject: [PATCH 4/5] fix: CheTooltip component Signed-off-by: Oleksii Kurinnyi --- packages/dashboard-frontend/jest.config.js | 2 +- packages/dashboard-frontend/jest.setup.ts | 14 ----- packages/dashboard-frontend/jest.setup.tsx | 40 +++++++++++++ .../CheTooltip/__tests__/CheTooltip.spec.tsx | 59 +++++++++++++++---- .../__snapshots__/CheTooltip.spec.tsx.snap | 27 +++++++-- .../src/components/CheTooltip/index.tsx | 22 ++----- .../__snapshots__/index.spec.tsx.snap | 26 ++++++-- .../__tests__/GetStartedTab.spec.tsx | 2 +- .../GitServicesTab/ProviderIcon/index.tsx | 18 ++---- .../GitServicesTab/ProviderWarning/index.tsx | 2 +- .../src/utils/che-tooltip.ts | 24 -------- 11 files changed, 142 insertions(+), 94 deletions(-) delete mode 100644 packages/dashboard-frontend/jest.setup.ts create mode 100644 packages/dashboard-frontend/jest.setup.tsx delete mode 100644 packages/dashboard-frontend/src/utils/che-tooltip.ts diff --git a/packages/dashboard-frontend/jest.config.js b/packages/dashboard-frontend/jest.config.js index 5dcf16187..89d202df5 100644 --- a/packages/dashboard-frontend/jest.config.js +++ b/packages/dashboard-frontend/jest.config.js @@ -34,7 +34,7 @@ module.exports = { }, ], }, - setupFilesAfterEnv: ['./jest.setup.ts'], + setupFilesAfterEnv: ['./jest.setup.tsx'], setupFiles: ['./src/inversify.config.ts'], collectCoverageFrom: [ ...base.collectCoverageFrom, diff --git a/packages/dashboard-frontend/jest.setup.ts b/packages/dashboard-frontend/jest.setup.ts deleted file mode 100644 index 2b40c1f9e..000000000 --- a/packages/dashboard-frontend/jest.setup.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) 2018-2023 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 '@testing-library/jest-dom'; -import '@/utils/che-tooltip'; diff --git a/packages/dashboard-frontend/jest.setup.tsx b/packages/dashboard-frontend/jest.setup.tsx new file mode 100644 index 000000000..91db413f8 --- /dev/null +++ b/packages/dashboard-frontend/jest.setup.tsx @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018-2023 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 '@testing-library/jest-dom'; + +import React from 'react'; + +jest.mock('@patternfly/react-core', () => { + return { + ...jest.requireActual('@patternfly/react-core'), + // mock the Tooltip component from @patternfly/react-core + Tooltip: jest.fn(props => { + const { content, children, ...rest } = props; + return ( +
+ {JSON.stringify(rest)} +
{content}
+
{children}
+
+ ); + }), + }; +}); + +jest.mock('@/components/CheTooltip', () => { + return { + CheTooltip: jest.fn(props => { + return React.createElement('div', null, props.children, props.content); + }), + }; +}); diff --git a/packages/dashboard-frontend/src/components/CheTooltip/__tests__/CheTooltip.spec.tsx b/packages/dashboard-frontend/src/components/CheTooltip/__tests__/CheTooltip.spec.tsx index eb2561e43..50ccc1dad 100644 --- a/packages/dashboard-frontend/src/components/CheTooltip/__tests__/CheTooltip.spec.tsx +++ b/packages/dashboard-frontend/src/components/CheTooltip/__tests__/CheTooltip.spec.tsx @@ -10,27 +10,60 @@ * Red Hat, Inc. - initial API and implementation */ +import { TooltipPosition } from '@patternfly/react-core'; import React from 'react'; -import renderer, { ReactTestRendererJSON } from 'react-test-renderer'; -import CheTooltip from '@/components/CheTooltip'; +import { CheTooltip, Props } from '@/components/CheTooltip'; +import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer'; + +// use actual CheTooltip component +jest.unmock('@/components/CheTooltip'); + +const { createSnapshot, renderComponent } = getComponentRenderer(getComponent); describe('CheTooltip component', () => { - it('should render CheTooltip component correctly', () => { - const content = Tooltip text.; + afterEach(() => { + jest.clearAllMocks(); + }); - const component = ( - - <>some text - + test('snapshot', () => { + const props = { + content: Tooltip text., + }; + + const snapshot = createSnapshot(props); + + expect(snapshot.toJSON()).toMatchSnapshot(); + }); + + test('passed props', () => { + const props: Props = { + position: TooltipPosition.right, + exitDelay: 500, + content: Tooltip text., + }; + + renderComponent(props); + + const tooltipProps = screen.getByTestId('tooltip-props'); + expect(tooltipProps).toHaveTextContent( + '{"isContentLeftAligned":true,"style":{"border":"1px solid","borderRadius":"3px","opacity":"0.9"},"position":"right","exitDelay":500}', ); - expect(getComponentSnapshot(component)).toMatchSnapshot(); + const tooltipContent = screen.getByTestId('tooltip-content'); + expect(tooltipContent).toHaveTextContent('Tooltip text.'); + + const tooltipPlacedTo = screen.getByTestId('tooltip-placed-to'); + expect(tooltipPlacedTo).toHaveTextContent('some text'); + + screen.debug(); }); }); -function getComponentSnapshot( - component: React.ReactElement, -): null | ReactTestRendererJSON | ReactTestRendererJSON[] { - return renderer.create(component).toJSON(); +function getComponent(props: Props): React.ReactElement { + return ( + +
some text
+
+ ); } diff --git a/packages/dashboard-frontend/src/components/CheTooltip/__tests__/__snapshots__/CheTooltip.spec.tsx.snap b/packages/dashboard-frontend/src/components/CheTooltip/__tests__/__snapshots__/CheTooltip.spec.tsx.snap index 55aa5a4fc..36f652655 100644 --- a/packages/dashboard-frontend/src/components/CheTooltip/__tests__/__snapshots__/CheTooltip.spec.tsx.snap +++ b/packages/dashboard-frontend/src/components/CheTooltip/__tests__/__snapshots__/CheTooltip.spec.tsx.snap @@ -1,10 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CheTooltip component should render CheTooltip component correctly 1`] = ` -
- some text - - Tooltip text. +exports[`CheTooltip component snapshot 1`] = ` +
+ + {"isContentLeftAligned":true,"style":{"border":"1px solid","borderRadius":"3px","opacity":"0.9"}} +
+ + Tooltip text. + +
+
+
+ some text +
+
`; diff --git a/packages/dashboard-frontend/src/components/CheTooltip/index.tsx b/packages/dashboard-frontend/src/components/CheTooltip/index.tsx index 392ab55a7..f9e2d3418 100644 --- a/packages/dashboard-frontend/src/components/CheTooltip/index.tsx +++ b/packages/dashboard-frontend/src/components/CheTooltip/index.tsx @@ -10,31 +10,19 @@ * Red Hat, Inc. - initial API and implementation */ -import { Tooltip, TooltipPosition } from '@patternfly/react-core'; +import { Tooltip, TooltipProps } from '@patternfly/react-core'; import React from 'react'; -type Props = { - children: React.ReactElement; - content: React.ReactNode; - position?: TooltipPosition; -}; +export type Props = Omit; -class CheTooltip extends React.PureComponent { +export class CheTooltip extends React.PureComponent { public render(): React.ReactElement { - const { content, position, children } = this.props; - return ( - {children} - + {...this.props} + /> ); } } - -export default CheTooltip; diff --git a/packages/dashboard-frontend/src/components/WorkspaceEvents/Item/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/components/WorkspaceEvents/Item/__tests__/__snapshots__/index.spec.tsx.snap index d30b6cb52..1c837df60 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceEvents/Item/__tests__/__snapshots__/index.spec.tsx.snap +++ b/packages/dashboard-frontend/src/components/WorkspaceEvents/Item/__tests__/__snapshots__/index.spec.tsx.snap @@ -30,11 +30,29 @@ exports[`WorkspaceEventsItem component snapshot 1`] = ` - - P - + + {"aria":"none","aria-live":"polite"} + +
+ Pod +
+
+ + P + +
+
pod-name
diff --git a/packages/dashboard-frontend/src/pages/GetStarted/GetStartedTab/__tests__/GetStartedTab.spec.tsx b/packages/dashboard-frontend/src/pages/GetStarted/GetStartedTab/__tests__/GetStartedTab.spec.tsx index 40d1885da..f89f483ee 100644 --- a/packages/dashboard-frontend/src/pages/GetStarted/GetStartedTab/__tests__/GetStartedTab.spec.tsx +++ b/packages/dashboard-frontend/src/pages/GetStarted/GetStartedTab/__tests__/GetStartedTab.spec.tsx @@ -56,7 +56,7 @@ jest.mock('../SamplesListGallery', () => { describe('Samples list tab', () => { afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); const renderComponent = (preferredStorageType: che.WorkspaceStorageType): RenderResult => { diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/index.tsx index 39a04cfd5..085730974 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderIcon/index.tsx @@ -11,7 +11,6 @@ */ import { api } from '@eclipse-che/common'; -import { TooltipPosition } from '@patternfly/react-core'; import { CheckCircleIcon, ExclamationTriangleIcon, @@ -20,7 +19,7 @@ import { import React from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import CheTooltip from '@/components/CheTooltip'; +import { CheTooltip } from '@/components/CheTooltip'; import { AppState } from '@/store'; import * as GitOauthConfig from '@/store/GitOauthConfig'; import { @@ -78,29 +77,20 @@ export class ProviderIcon extends React.PureComponent { const { hasOauthToken, isSkipOauth } = this.state; if (hasOauthToken) { return ( - User has been authorized successfully.} - position={TooltipPosition.top} - > + User has been authorized successfully.}> ); } else if (isSkipOauth) { return ( - Authorization has been rejected by user.} - position={TooltipPosition.top} - > + Authorization has been rejected by user.}> ); } return ( - User has not been authorized yet.} - position={TooltipPosition.top} - > + User has not been authorized yet.}> ); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/index.tsx index ed774636f..c1ebd8e49 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/ProviderWarning/index.tsx @@ -13,7 +13,7 @@ import { WarningTriangleIcon } from '@patternfly/react-icons'; import React from 'react'; -import CheTooltip from '@/components/CheTooltip'; +import { CheTooltip } from '@/components/CheTooltip'; type Props = { serverURI: string; diff --git a/packages/dashboard-frontend/src/utils/che-tooltip.ts b/packages/dashboard-frontend/src/utils/che-tooltip.ts deleted file mode 100644 index 970f01518..000000000 --- a/packages/dashboard-frontend/src/utils/che-tooltip.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2018-2023 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 { TooltipPosition } from '@patternfly/react-core'; -import React from 'react'; - -jest.mock('@/components/CheTooltip', () => { - return function CheTooltip(props: { - children: React.ReactElement; - content: React.ReactNode; - position?: TooltipPosition; - }): React.ReactElement { - return React.createElement('div', null, props.children, props.content); - }; -}); From 1abef30171817f80c844804cc3c9432ac7b0fdc6 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Tue, 14 Nov 2023 16:01:24 +0100 Subject: [PATCH 5/5] chore: show authorized git services based on existed OAuth token secrets (#986) * chore: show authorized git services based on existed OAuth token secrets Signed-off-by: Anatolii Bazko * Add test Signed-off-by: Anatolii Bazko * fix formatting Signed-off-by: Anatolii Bazko --------- Signed-off-by: Anatolii Bazko --- .../GitOauthConfig/__tests__/index.spec.ts | 95 +++++++++++++++++++ .../src/store/GitOauthConfig/index.ts | 37 +++++++- 2 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 packages/dashboard-frontend/src/store/GitOauthConfig/__tests__/index.spec.ts diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/__tests__/index.spec.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/__tests__/index.spec.ts new file mode 100644 index 000000000..d8687d760 --- /dev/null +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/__tests__/index.spec.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2018-2023 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 { MockStoreEnhanced } from 'redux-mock-store'; +import { ThunkDispatch } from 'redux-thunk'; + +import { AppState } from '@/store'; +import { FakeStoreBuilder } from '@/store/__mocks__/storeBuilder'; +import { IGitOauth } from '@/store/GitOauthConfig/types'; + +import * as TestStore from '..'; + +const gitOauth = [ + { + name: 'github', + endpointUrl: 'https://github.com', + }, + { + name: 'gitlab', + endpointUrl: 'https://gitlab.com', + }, + { + name: 'azure-devops', + endpointUrl: 'https://dev.azure.com', + }, +] as IGitOauth[]; + +const mockGetOAuthProviders = jest.fn().mockResolvedValue(gitOauth); +const mockGetDevWorkspacePreferences = jest.fn().mockResolvedValue({}); +const mockGetOAuthToken = jest.fn().mockImplementation(provider => { + if (provider === 'github') { + return new Promise(resolve => resolve('github-token')); + } + return new Promise((_resolve, reject) => reject(new Error('Token not found'))); +}); +const mockFetchTokens = jest.fn().mockResolvedValue([ + { + tokenName: 'github-personal-access-token', + gitProvider: 'oauth2-token', + gitProviderEndpoint: 'https://dev.azure.com/', + }, +] as any[]); + +jest.mock('../../../services/backend-client/oAuthApi', () => { + return { + getOAuthProviders: (...args: unknown[]) => mockGetOAuthProviders(...args), + getOAuthToken: (...args: unknown[]) => mockGetOAuthToken(...args), + getDevWorkspacePreferences: (...args: unknown[]) => mockGetDevWorkspacePreferences(...args), + }; +}); +jest.mock('../../../services/backend-client/personalAccessTokenApi', () => { + return { + fetchTokens: (...args: unknown[]) => mockFetchTokens(...args), + }; +}); + +// mute the outputs +console.error = jest.fn(); + +describe('GitOauthConfig store, actions', () => { + let store: MockStoreEnhanced>; + + beforeEach(() => { + store = new FakeStoreBuilder() + .withInfrastructureNamespace([{ name: 'user-che', attributes: { phase: 'Active' } }]) + .build(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should request GitOauthConfig', async () => { + await store.dispatch(TestStore.actionCreators.requestGitOauthConfig()); + + const actions = store.getActions(); + + const expectedAction: TestStore.KnownAction = { + supportedGitOauth: gitOauth, + providersWithToken: ['github', 'azure-devops'], + type: TestStore.Type.RECEIVE_GIT_OAUTH_PROVIDERS, + }; + + expect(actions).toContainEqual(expectedAction); + }); +}); diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts index d34fe7bcc..bc8b18cba 100644 --- a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts @@ -20,6 +20,7 @@ import { getOAuthProviders, getOAuthToken, } from '@/services/backend-client/oAuthApi'; +import { fetchTokens } from '@/services/backend-client/personalAccessTokenApi'; import { IGitOauth } from '@/store/GitOauthConfig/types'; import { createObject } from '@/store/helpers'; import { selectDefaultNamespace } from '@/store/InfrastructureNamespaces/selectors'; @@ -141,12 +142,40 @@ export const actionCreators: ActionCreators = { const providersWithToken: api.GitOauthProvider[] = []; try { const supportedGitOauth = await getOAuthProviders(); + + const defaultKubernetesNamespace = selectDefaultNamespace(getState()); + const tokens = await fetchTokens(defaultKubernetesNamespace.name); + const promises: Promise[] = []; - for (const { name } of supportedGitOauth) { + for (const gitOauth of supportedGitOauth) { promises.push( - getOAuthToken(name).then(() => { - providersWithToken.push(name); - }), + getOAuthToken(gitOauth.name) + .then(() => { + providersWithToken.push(gitOauth.name); + }) + + // if `api/oauth/token` doesn't return a user's token, + // then check if there is the user's token in a Kubernetes Secret + .catch(() => { + const normalizedGitOauthEndpoint = gitOauth.endpointUrl.endsWith('/') + ? gitOauth.endpointUrl.slice(0, -1) + : gitOauth.endpointUrl; + + for (const token of tokens) { + const normalizedTokenGitProviderEndpoint = token.gitProviderEndpoint.endsWith('/') + ? token.gitProviderEndpoint.slice(0, -1) + : token.gitProviderEndpoint; + + // compare Git OAuth Endpoint url ONLY with OAuth tokens + if ( + token.gitProvider.startsWith('oauth2') && + normalizedGitOauthEndpoint === normalizedTokenGitProviderEndpoint + ) { + providersWithToken.push(gitOauth.name); + break; + } + } + }), ); } promises.push(dispatch(actionCreators.requestSkipAuthorizationProviders()));