From c57ec26123f58b58e9092ea650e2b7cf9a43079b Mon Sep 17 00:00:00 2001 From: Oleksii Orel Date: Wed, 8 Nov 2023 05:33:55 +0200 Subject: [PATCH] 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 | 116 ++++++----- .../src/pages/UserPreferences/index.tsx | 3 + .../src/services/backend-client/oAuthApi.ts | 24 ++- .../src/store/GitOauthConfig/index.ts | 182 +++++++++++++---- .../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, 908 insertions(+), 258 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 381994682b..306c26ac69 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 fd1313b70f..99a2428b08 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 25b1253ab7..2b40c1f9ee 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 0000000000..eb2561e436 --- /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 0000000000..55aa5a4fc3 --- /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 0000000000..392ab55a7f --- /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 3e362a1ba6..f2844529de 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 0000000000..f4d6429e71 --- /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 0000000000..df790b5af8 --- /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 0000000000..39a04cfd5d --- /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 2fbc8eb773..69ac04bdbd 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 0000000000..d12fe3f44b --- /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 35a3354eed..0000000000 --- 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 9ab45d19b9..ed774636ff 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 81d1c70f39..44c2b98a15 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 7a8d86d455..afc96e134a 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']; @@ -69,38 +75,21 @@ export class GitServices extends React.PureComponent { } public async componentDidMount(): Promise { - const { isLoading, requestGitOauthConfig } = this.props; + const { isLoading, requestGitOauthConfig, requestSkipAuthorisationProviders } = this.props; if (!isLoading) { - requestGitOauthConfig(); + await requestGitOauthConfig(); + await requestSkipAuthorisationProviders(); } } 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 +101,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 +184,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 +205,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 37beedd681..b256761d88 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx @@ -10,6 +10,7 @@ * Red Hat, Inc. - initial API and implementation */ +import { che } from '@eclipse-che/api'; import { PageSection, PageSectionVariants, Tab, Tabs, Title } from '@patternfly/react-core'; import { History } from 'history'; import React from 'react'; @@ -26,6 +27,7 @@ import { UserPreferencesTab } from '@/services/helpers/types'; import { AppState } from '@/store'; import { actionCreators } from '@/store/GitOauthConfig'; import { selectIsLoading } from '@/store/GitOauthConfig/selectors'; +import event = che.workspace.event; const CONTAINER_REGISTRIES_TAB: UserPreferencesTab = 'container-registries'; const GIT_SERVICES_TAB: UserPreferencesTab = 'git-services'; @@ -77,6 +79,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 ec85fb22e0..38a4521aeb 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 71b5e79799..f20b7461d9 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,141 @@ 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 interface DeleteSkipOauth extends Action { + type: Type.DELETE_SKIP_OAUTH; + provider: api.GitOauthProvider; +} + export type KnownAction = - | RequestGitOauthConfigAction + | RequestGitOAuthAction + | ReceiveGitOAuthConfigAction + | ReceiveSkipOauthProvidersAction | DeleteOauthAction - | ReceiveGitOauthConfigAction - | ReceivedGitOauthConfigErrorAction; + | DeleteSkipOauth + | 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); }), ); } 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 +176,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 +191,46 @@ 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); + dispatch({ + type: Type.DELETE_SKIP_OAUTH, + provider: oauthProvider, + }); + } catch (e) { + const errorMessage = common.helpers.errors.getMessage(e); + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_ERROR, error: errorMessage, }); throw e; @@ -152,6 +241,8 @@ export const actionCreators: ActionCreators = { const unloadedState: State = { isLoading: false, gitOauth: [], + providersWithToken: [], + skipOauthProviders: [], error: undefined, }; @@ -165,22 +256,37 @@ 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, + skipOauthProviders: action.skipOauthProviders, + }); + case Type.DELETE_GIT_OAUTH_TOKEN: return createObject(state, { isLoading: false, - gitOauth: action.gitOauth, + providersWithToken: state.providersWithToken.filter( + provider => provider !== action.provider, + ), }); - case Type.DELETE_OAUTH: + case Type.DELETE_SKIP_OAUTH: return createObject(state, { isLoading: false, - gitOauth: state.gitOauth.filter(v => v.name !== action.provider), + skipOauthProviders: state.skipOauthProviders.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 2fb1c6253b..e470786f04 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 b63499dd7d..8216df5114 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 15a2e1042b..a8501277bd 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 0000000000..970f015184 --- /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); + }; +});