diff --git a/packages/common/src/dto/api.ts b/packages/common/src/dto/api.ts index 520a217d9..3e084aef1 100644 --- a/packages/common/src/dto/api.ts +++ b/packages/common/src/dto/api.ts @@ -12,6 +12,8 @@ import { V220DevfileComponents } from '@devfile/api'; +export type GitOauthProvider = 'github' | 'gitlab' | 'bitbucket'; + export interface IPatch { op: string; path: string; diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/helpers/retryableExec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/helpers/retryableExec.ts index 00d44294c..d31d3bb83 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/helpers/retryableExec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/helpers/retryableExec.ts @@ -11,6 +11,7 @@ */ import { delay } from '../../../services/helpers'; +import { helpers } from '@eclipse-che/common'; export async function retryableExec(callback: () => Promise, maxAttempt = 5): Promise { let error: unknown; @@ -19,6 +20,9 @@ export async function retryableExec(callback: () => Promise, maxAttempt = return await callback(); } catch (e) { error = e; + if (helpers.errors.isKubeClientError(error) && error.statusCode === 404) { + return Promise.reject(error); + } console.error(e); } await delay(1000); diff --git a/packages/dashboard-backend/src/localRun/hooks/authorizationHooks.ts b/packages/dashboard-backend/src/localRun/hooks/authorizationHooks.ts index 27ff730c5..afb34b0b7 100644 --- a/packages/dashboard-backend/src/localRun/hooks/authorizationHooks.ts +++ b/packages/dashboard-backend/src/localRun/hooks/authorizationHooks.ts @@ -15,7 +15,7 @@ import { FastifyInstance } from 'fastify'; export function addAuthorizationHooks(server: FastifyInstance) { server.addHook('onResponse', (request, reply, done) => { if ( - (request.url.startsWith('/api/') || request.url.startsWith('/dashboard/api/')) && + request.url.startsWith('/dashboard/api/') && request.method === 'GET' && reply.statusCode === 401 ) { diff --git a/packages/dashboard-frontend/package.json b/packages/dashboard-frontend/package.json index 24c55c331..19c2ebd49 100644 --- a/packages/dashboard-frontend/package.json +++ b/packages/dashboard-frontend/package.json @@ -36,7 +36,7 @@ "@eclipse-che/che-code-devworkspace-handler": "1.74.0-dev-e701cae", "@eclipse-che/che-theia-devworkspace-handler": "0.0.1-1667484092", "@eclipse-che/devfile-converter": "0.0.1-d624e3e", - "@eclipse-che/workspace-client": "0.0.1-1663851810", + "@eclipse-che/workspace-client": "0.0.1-1671716979", "@patternfly/react-core": "4.120.0", "@patternfly/react-icons": "^4.3.5", "@patternfly/react-table": "^4.5.7", diff --git a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap b/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap deleted file mode 100644 index 76346f384..000000000 --- a/packages/dashboard-frontend/src/pages/UserAccount/__tests__/__snapshots__/index.spec.tsx.snap +++ /dev/null @@ -1,217 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UserAccount page should correctly render the component which contains profile data 1`] = ` -
-
-
-

- Account -

-

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

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

- Account -

-

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

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

+ No Git Services +

+
+
+`; diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/index.spec.tsx new file mode 100644 index 000000000..d6c5c9257 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/__tests__/index.spec.tsx @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; +import EmptyState from '../index'; + +describe('No git services component for empty state', () => { + it('should render title correctly', () => { + const element = ; + + expect(renderer.create(element).toJSON()).toMatchSnapshot(); + }); +}); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/index.tsx new file mode 100644 index 000000000..4a5c4b240 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/EmptyState/index.tsx @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import React from 'react'; +import { Title, EmptyState, EmptyStateVariant, EmptyStateIcon } from '@patternfly/react-core'; +import { RegistryIcon } from '@patternfly/react-icons'; + +type Props = { + text: string; +}; + +export default class Empty extends React.PureComponent { + public render(): React.ReactElement { + return ( + + + + {this.props.text} + + + ); + } +} diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/RevokeGitServicesModal.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/RevokeGitServicesModal.tsx new file mode 100644 index 000000000..b6b6a0f25 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/Modals/RevokeGitServicesModal.tsx @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import React from 'react'; +import { + Button, + ButtonVariant, + ModalVariant, + Modal, + TextContent, + Text, + Checkbox, +} from '@patternfly/react-core'; +import { api } from '@eclipse-che/common'; + +type Props = { + gitOauth?: api.GitOauthProvider; + selectedItems: api.GitOauthProvider[]; + isOpen: boolean; + onRevoke: () => void; + onCancel: () => void; +}; +type State = { + warningInfoCheck: boolean; +}; + +export default class RevokeGitServicesModal extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { warningInfoCheck: false }; + } + + public componentDidUpdate(prevProps: Props): void { + if (prevProps.isOpen === this.props.isOpen && this.props.isOpen) { + return; + } + + this.setState({ warningInfoCheck: false }); + } + + private getRevokeModalContent(): React.ReactNode { + const { gitOauth, selectedItems } = this.props; + + let text = 'Would you like to revoke '; + + if (gitOauth) { + text += `git service '${gitOauth}'`; + } else { + if (selectedItems.length < 2) { + text += `git service '${selectedItems[0]}'`; + } else { + text += `${selectedItems.length} git services`; + } + } + text += '?'; + + return ( + + {text} + { + this.setState({ warningInfoCheck: !this.state.warningInfoCheck }); + }} + id="revoke-warning-info-check" + label="I understand, this operation cannot be reverted." + /> + + ); + } + + public render(): React.ReactElement { + const { isOpen, onCancel, onRevoke, gitOauth } = this.props; + const { warningInfoCheck } = this.state; + + return ( + + + + + } + > + {this.getRevokeModalContent()} + + ); + } +} diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx new file mode 100644 index 000000000..c7fbc6021 --- /dev/null +++ b/packages/dashboard-frontend/src/pages/UserPreferences/GitServicesTab/index.tsx @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { + AlertVariant, + Button, + ButtonVariant, + PageSection, + Toolbar, + ToolbarContent, + ToolbarItem, +} from '@patternfly/react-core'; +import { Table, TableBody, TableHeader } from '@patternfly/react-table'; +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import ProgressIndicator from '../../../components/Progress'; +import { lazyInject } from '../../../inversify.config'; +import { AppAlerts } from '../../../services/alerts/appAlerts'; +import { AlertItem } from '../../../services/helpers/types'; +import { AppState } from '../../../store'; +import { selectIsLoading, selectGitOauth } from '../../../store/GitOauthConfig/selectors'; +import EmptyState from './EmptyState'; +import RevokeGitServicesModal from './Modals/RevokeGitServicesModal'; +import { api, helpers } from '@eclipse-che/common'; +import * as GitOauthConfig from '../../../store/GitOauthConfig'; + +enum providers { + 'github' = 'GitHub', + 'gitlab' = 'GitLab', + 'bitbucket' = 'Bitbucket', +} + +type Props = MappedProps; + +type State = { + selectedItems: api.GitOauthProvider[]; + gitOauth: api.GitOauthProvider[]; + currentGitOauth: api.GitOauthProvider | undefined; + currentGitOauthIndex: number; + isRevokeModalOpen: boolean; + isEditModalOpen: boolean; +}; + +export class GitServicesTab extends React.PureComponent { + @lazyInject(AppAlerts) + private readonly appAlerts: AppAlerts; + + constructor(props: Props) { + super(props); + + const gitOauth = this.props.gitOauth; + + this.state = { + gitOauth, + currentGitOauth: undefined, + selectedItems: [], + currentGitOauthIndex: -1, + isEditModalOpen: false, + isRevokeModalOpen: false, + }; + } + + private onChangeRegistrySelection(isSelected: boolean, rowIndex: number) { + const { gitOauth } = this.state; + if (rowIndex === -1) { + const selectedItems = isSelected ? gitOauth : []; + this.setState({ selectedItems }); + } else { + const selectedItem = gitOauth[rowIndex]; + this.setState((prevState: State) => { + return { + selectedItems: isSelected + ? [...prevState.selectedItems, selectedItem] + : prevState.selectedItems.filter(item => item !== selectedItem), + }; + }); + } + } + + public async componentDidMount(): Promise { + const { isLoading, requestGitOauthConfig } = this.props; + if (!isLoading) { + requestGitOauthConfig(); + } + } + + public componentDidUpdate(prevProps: Props, prevState: State): void { + const gitOauth = this.props.gitOauth; + if (prevProps.gitOauth !== gitOauth) { + const selectedItems: api.GitOauthProvider[] = []; + this.state.selectedItems.forEach(selectedItem => { + if (gitOauth.indexOf(selectedItem) !== -1) { + selectedItems.push(selectedItem); + } + }); + this.setState({ gitOauth, selectedItems }); + } + if (prevState.currentGitOauthIndex !== this.state.currentGitOauthIndex) { + const currentGitOauth = gitOauth[this.state.currentGitOauthIndex]; + this.setState({ currentGitOauth }); + } + } + + private showAlert(alert: AlertItem): void { + this.appAlerts.showAlert(alert); + } + + private buildGitOauthRow( + gitOauth: api.GitOauthProvider | providers, + server: string, + ): React.ReactNode[] { + if (/^http[s]?:\/\/.*/.test(server)) { + return [ + {gitOauth}, + + + {server} + + , + ]; + } + + return [{gitOauth}, {server}]; + } + + private showOnRevokeGitOauthModal(rowIndex: number): void { + this.setState({ currentGitOauthIndex: rowIndex, isRevokeModalOpen: true }); + } + + private async deleteOauth(gitOauth: api.GitOauthProvider): Promise { + try { + await this.props.deleteOauth(gitOauth); + this.showAlert({ + key: 'delete-github', + variant: AlertVariant.success, + title: `Git oauth '${gitOauth}' successfully deleted.`, + }); + } catch (e) { + this.showAlert({ + key: 'delete-fail', + variant: AlertVariant.danger, + title: helpers.errors.getMessage(e), + }); + } + } + + private async onRevoke(gitOauth?: api.GitOauthProvider): Promise { + this.setState({ isRevokeModalOpen: false, currentGitOauthIndex: -1 }); + if (gitOauth === undefined) { + this.state.selectedItems.forEach(async gitOauth => { + await this.deleteOauth(gitOauth); + }); + this.setState({ selectedItems: [] }); + } else { + await this.deleteOauth(gitOauth); + } + } + + private setRevokeModalStatus(isRevokeModalOpen: boolean): void { + if (this.state.isRevokeModalOpen === isRevokeModalOpen) { + return; + } + this.setState({ isRevokeModalOpen }); + } + + private handleRevoke(): void { + this.setState({ currentGitOauthIndex: -1, isRevokeModalOpen: true }); + } + + render(): React.ReactNode { + const { isLoading } = this.props; + const { isRevokeModalOpen, currentGitOauth, selectedItems } = this.state; + const columns = ['Name', 'Server']; + const rows = + this.state.gitOauth.map(provider => ({ + cells: this.buildGitOauthRow(providers[provider] ? providers[provider] : provider, '-'), + selected: selectedItems.includes(provider as api.GitOauthProvider), + })) || []; + const actions = [ + { + title: 'Revoke', + onClick: (event, rowIndex) => this.showOnRevokeGitOauthModal(rowIndex), + }, + ]; + + return ( + + + + {rows.length === 0 ? ( + + ) : ( + + this.setRevokeModalStatus(false)} + onRevoke={() => this.onRevoke(currentGitOauth)} + isOpen={isRevokeModalOpen} + gitOauth={currentGitOauth} + /> + + + + + + + + { + this.onChangeRegistrySelection(isSelected, rowIndex); + }} + canSelectAll={true} + aria-label="Git services" + variant="compact" + > + + +
+
+ )} +
+
+ ); + } +} + +const mapStateToProps = (state: AppState) => ({ + gitOauth: selectGitOauth(state), + isLoading: selectIsLoading(state), +}); + +const connector = connect(mapStateToProps, GitOauthConfig.actionCreators); + +type MappedProps = ConnectedProps; +export default connector(GitServicesTab); diff --git a/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx b/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx index 7d2ec3a96..9558e4f8d 100644 --- a/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx +++ b/packages/dashboard-frontend/src/pages/UserPreferences/index.tsx @@ -18,9 +18,11 @@ import Head from '../../components/Head'; import { UserPreferencesTab } from '../../services/helpers/types'; import { ROUTE } from '../../Routes/routes'; import { AppState } from '../../store'; -import { selectIsLoading, selectRegistries } from '../../store/DockerConfig/selectors'; +import { selectIsLoading } from '../../store/GitOauthConfig/selectors'; +import { actionCreators } from '../../store/GitOauthConfig'; const ContanerRegistryList = React.lazy(() => import('./ContainerRegistriesTab')); +const GitServicesTab = React.lazy(() => import('./GitServicesTab')); type Props = { history: History; @@ -63,11 +65,16 @@ export class UserPreferences extends React.PureComponent { this.setState({ activeTabKey: activeTabKey as UserPreferencesTab, }); + + if (activeTabKey === 'git-services') { + if (!this.props.isLoading) { + this.props.requestGitOauthConfig(); + } + } } render(): React.ReactNode { const { activeTabKey } = this.state; - const containerRegistriesTab: UserPreferencesTab = 'container-registries'; return ( @@ -83,10 +90,13 @@ export class UserPreferences extends React.PureComponent { > - + + + + @@ -95,11 +105,10 @@ export class UserPreferences extends React.PureComponent { } const mapStateToProps = (state: AppState) => ({ - registries: selectRegistries(state), isLoading: selectIsLoading(state), }); -const connector = connect(mapStateToProps); +const connector = connect(mapStateToProps, actionCreators); type MappedProps = ConnectedProps; export default connector(UserPreferences); diff --git a/packages/dashboard-frontend/src/services/dashboard-backend-client/clusterConfig.ts b/packages/dashboard-frontend/src/services/dashboard-backend-client/clusterConfigApi.ts similarity index 100% rename from packages/dashboard-frontend/src/services/dashboard-backend-client/clusterConfig.ts rename to packages/dashboard-frontend/src/services/dashboard-backend-client/clusterConfigApi.ts diff --git a/packages/dashboard-frontend/src/services/dashboard-backend-client/clusterInfo.ts b/packages/dashboard-frontend/src/services/dashboard-backend-client/clusterInfoApi.ts similarity index 100% rename from packages/dashboard-frontend/src/services/dashboard-backend-client/clusterInfo.ts rename to packages/dashboard-frontend/src/services/dashboard-backend-client/clusterInfoApi.ts diff --git a/packages/dashboard-frontend/src/services/helpers/types.ts b/packages/dashboard-frontend/src/services/helpers/types.ts index 9baac97fd..645474d4a 100644 --- a/packages/dashboard-frontend/src/services/helpers/types.ts +++ b/packages/dashboard-frontend/src/services/helpers/types.ts @@ -101,6 +101,6 @@ export enum WorkspaceAction { EDIT_WORKSPACE = 'Edit Workspace', } -export type UserPreferencesTab = 'container-registries'; +export type UserPreferencesTab = 'container-registries' | 'git-services'; export type WorkspacesLogs = Map; diff --git a/packages/dashboard-frontend/src/store/ClusterConfig/index.ts b/packages/dashboard-frontend/src/store/ClusterConfig/index.ts index ad9f03c97..ba6176942 100644 --- a/packages/dashboard-frontend/src/store/ClusterConfig/index.ts +++ b/packages/dashboard-frontend/src/store/ClusterConfig/index.ts @@ -15,7 +15,7 @@ import common, { ClusterConfig } from '@eclipse-che/common'; import { AppThunk } from '..'; import { createObject } from '../helpers'; import * as BannerAlertStore from '../BannerAlert'; -import { fetchClusterConfig } from '../../services/dashboard-backend-client/clusterConfig'; +import { fetchClusterConfig } from '../../services/dashboard-backend-client/clusterConfigApi'; import { AddBannerAction } from '../BannerAlert'; import { AUTHORIZED, SanityCheckAction } from '../sanityCheckMiddleware'; diff --git a/packages/dashboard-frontend/src/store/ClusterInfo/index.ts b/packages/dashboard-frontend/src/store/ClusterInfo/index.ts index bce180610..7ce830f9b 100644 --- a/packages/dashboard-frontend/src/store/ClusterInfo/index.ts +++ b/packages/dashboard-frontend/src/store/ClusterInfo/index.ts @@ -14,7 +14,7 @@ import { Action, Reducer } from 'redux'; import common, { ClusterInfo } from '@eclipse-che/common'; import { AppThunk } from '..'; import { createObject } from '../helpers'; -import { fetchClusterInfo } from '../../services/dashboard-backend-client/clusterInfo'; +import { fetchClusterInfo } from '../../services/dashboard-backend-client/clusterInfoApi'; import { AUTHORIZED, SanityCheckAction } from '../sanityCheckMiddleware'; export interface State { diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts new file mode 100644 index 000000000..cbf75f895 --- /dev/null +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/index.ts @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { Action, Reducer } from 'redux'; +import common, { api } from '@eclipse-che/common'; +import { AppThunk } from '..'; +import { createObject } from '../helpers'; +import { AUTHORIZED } from '../sanityCheckMiddleware'; +import { container } from '../../inversify.config'; +import { CheWorkspaceClient } from '../../services/workspace-client/cheworkspace/cheWorkspaceClient'; + +export interface State { + isLoading: boolean; + gitOauth: api.GitOauthProvider[]; + error: string | undefined; +} + +const cheWorkspaceClient = container.get(CheWorkspaceClient); + +export enum Type { + REQUEST_GIT_OAUTH_CONFIG = 'REQUEST_GIT_OAUTH_CONFIG', + DELETE_OAUTH = 'DELETE_OAUTH', + RECEIVE_GIT_OAUTH_CONFIG = 'RECEIVE_GIT_OAUTH_CONFIG', + RECEIVE_GIT_OAUTH_CONFIG_ERROR = 'RECEIVE_GIT_OAUTH_CONFIG_ERROR', +} + +export interface RequestGitOauthConfigAction extends Action { + type: Type.REQUEST_GIT_OAUTH_CONFIG; +} + +export interface DeleteOauthAction extends Action { + type: Type.DELETE_OAUTH; + provider: api.GitOauthProvider; +} + +export interface ReceiveGitOauthConfigAction extends Action { + type: Type.RECEIVE_GIT_OAUTH_CONFIG; + gitOauth: api.GitOauthProvider[]; +} + +export interface ReceivedGitOauthConfigErrorAction extends Action { + type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR; + error: string; +} + +export type KnownAction = + | RequestGitOauthConfigAction + | DeleteOauthAction + | ReceiveGitOauthConfigAction + | ReceivedGitOauthConfigErrorAction; + +export type ActionCreators = { + requestGitOauthConfig: () => AppThunk>; + deleteOauth: (oauthProvider: api.GitOauthProvider) => AppThunk>; +}; + +export const actionCreators: ActionCreators = { + requestGitOauthConfig: + (): AppThunk> => + async (dispatch): Promise => { + await dispatch({ + type: Type.REQUEST_GIT_OAUTH_CONFIG, + }); + const gitOauth: api.GitOauthProvider[] = []; + try { + const oAuthProviders = await cheWorkspaceClient.restApiClient.getOAuthProviders(); + for (const provider of oAuthProviders) { + try { + await cheWorkspaceClient.restApiClient.getOAuthToken(provider); + gitOauth.push(provider as api.GitOauthProvider); + } catch (e) { + // no-op + } + } + + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_CONFIG, + gitOauth, + }); + } catch (e) { + const errorMessage = common.helpers.errors.getMessage(e); + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR, + error: errorMessage, + }); + throw errorMessage; + } + }, + + deleteOauth: + (oauthProvider: api.GitOauthProvider): AppThunk> => + async (dispatch): Promise => { + await dispatch({ + type: Type.REQUEST_GIT_OAUTH_CONFIG, + check: AUTHORIZED, + }); + try { + await cheWorkspaceClient.restApiClient.deleteOAuthToken(oauthProvider); + dispatch({ + type: Type.DELETE_OAUTH, + provider: oauthProvider, + }); + } catch (e) { + const errorMessage = common.helpers.errors.getMessage(e); + dispatch({ + type: Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR, + error: errorMessage, + }); + throw errorMessage; + } + }, +}; + +const unloadedState: State = { + isLoading: false, + gitOauth: [], + error: undefined, +}; + +export const reducer: Reducer = ( + state: State | undefined, + incomingAction: Action, +): State => { + if (state === undefined) { + return unloadedState; + } + + const gitOauth = Object.assign({}, state.gitOauth); + + const action = incomingAction as KnownAction; + switch (action.type) { + case Type.REQUEST_GIT_OAUTH_CONFIG: + return createObject(state, { + isLoading: true, + error: undefined, + }); + case Type.RECEIVE_GIT_OAUTH_CONFIG: + return createObject(state, { + isLoading: false, + gitOauth: action.gitOauth, + }); + case Type.DELETE_OAUTH: + delete gitOauth[action.provider]; + return createObject(state, { + isLoading: false, + gitOauth, + }); + case Type.RECEIVE_GIT_OAUTH_CONFIG_ERROR: + return createObject(state, { + isLoading: false, + error: action.error, + }); + default: + return state; + } +}; diff --git a/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts b/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts new file mode 100644 index 000000000..45d92b018 --- /dev/null +++ b/packages/dashboard-frontend/src/store/GitOauthConfig/selectors.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2018-2021 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { createSelector } from 'reselect'; +import { AppState } from '..'; +import { State } from './index'; + +const selectState = (state: AppState) => state.dwGitOauthConfig; + +export const selectIsLoading = createSelector(selectState, state => { + return state.isLoading; +}); + +export const selectGitOauth = createSelector(selectState, (state: State) => { + return state.gitOauth; +}); + +export const selectError = createSelector(selectState, state => { + return state.error; +}); diff --git a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts index bd1944e08..ae703ff04 100644 --- a/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts +++ b/packages/dashboard-frontend/src/store/__mocks__/storeBuilder.ts @@ -101,6 +101,11 @@ export class FakeStoreBuilder { isLoading: false, data: {}, } as BrandingState, + dwGitOauthConfig: { + isLoading: false, + gitOauth: [], + error: undefined, + }, devfileRegistries: { isLoading: false, devfiles: {}, diff --git a/packages/dashboard-frontend/src/store/index.ts b/packages/dashboard-frontend/src/store/index.ts index 817e7188b..8ad00f2c9 100644 --- a/packages/dashboard-frontend/src/store/index.ts +++ b/packages/dashboard-frontend/src/store/index.ts @@ -31,6 +31,7 @@ import * as WorkspacesStore from './Workspaces'; import * as CheWorkspacesStore from './Workspaces/cheWorkspaces'; import * as DevWorkspacesStore from './Workspaces/devWorkspaces'; import * as WorkspacesSettingsStore from './Workspaces/Settings'; +import * as GitOauthConfigStore from './GitOauthConfig'; // the top-level state object export interface AppState { @@ -42,6 +43,7 @@ export interface AppState { clusterInfo: ClusterInfo.State; devWorkspaces: DevWorkspacesStore.State; devfileRegistries: DevfileRegistriesStore.State; + dwGitOauthConfig: GitOauthConfigStore.State; dwDockerConfig: DwDockerConfigStore.State; dwPlugins: DwPluginsStore.State; dwServerConfig: DwServerConfigStore.State; @@ -64,6 +66,7 @@ export const reducers = { clusterInfo: ClusterInfo.reducer, devWorkspaces: DevWorkspacesStore.reducer, devfileRegistries: DevfileRegistriesStore.reducer, + dwGitOauthConfig: GitOauthConfigStore.reducer, dwDockerConfig: DwDockerConfigStore.reducer, dwPlugins: DwPluginsStore.reducer, dwServerConfig: DwServerConfigStore.reducer, diff --git a/yarn.lock b/yarn.lock index 3ee466e91..dae787652 100644 --- a/yarn.lock +++ b/yarn.lock @@ -381,16 +381,16 @@ jsonc-parser "^3.0.0" reflect-metadata "^0.1.13" -"@eclipse-che/workspace-client@0.0.1-1663851810": - version "0.0.1-1663851810" - resolved "https://registry.yarnpkg.com/@eclipse-che/workspace-client/-/workspace-client-0.0.1-1663851810.tgz#575c6e9c359a536ec3b1f359d7142bcd6b59d735" - integrity sha512-oV+zTmsrMcV+SvnZBuHWbrwYimqUqWil2BHGjQYpK1TroEMHiOgfs0Xjh7FxdrgcYUc05Rnb7yvwe4QSjw7iTQ== +"@eclipse-che/workspace-client@0.0.1-1671716979": + version "0.0.1-1671716979" + resolved "https://registry.yarnpkg.com/@eclipse-che/workspace-client/-/workspace-client-0.0.1-1671716979.tgz#233c1be0b79cf2414066f2132364fbe44a2c936f" + integrity sha512-mtoQPx010ysWKuSiU0Ly9i6sMqaNVG4JBK8Igk6Jbim/g/eI/0qdbuyi0DwMGxg1Z1gPlSVZDyMYTVPo2O8IVg== dependencies: "@eclipse-che/api" "^7.0.0-beta-4.0" - axios "^0.21.1" + axios "^0.21.4" qs "^6.9.4" tunnel "0.0.6" - websocket "1.0.23" + websocket "1.0.34" "@eslint/eslintrc@^0.4.3": version "0.4.3" @@ -3782,6 +3782,13 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +bufferutil@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" + integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== + dependencies: + node-gyp-build "^4.3.0" + builtins@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz" @@ -4659,6 +4666,14 @@ csstype@^3.0.2: resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz" integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + dargs@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz" @@ -5216,6 +5231,24 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.62" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + es6-promise@^4.0.3: version "4.2.8" resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz" @@ -5228,6 +5261,14 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" @@ -5608,6 +5649,13 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +ext@^1.1.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz" @@ -8854,11 +8902,6 @@ mute-stream@0.0.8, mute-stream@~0.0.4: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.3.3: - version "2.15.0" - resolved "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz" - integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== - nanoid@^3.3.4: version "3.3.4" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" @@ -8896,6 +8939,11 @@ neo-async@^2.6.0, neo-async@^2.6.2: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" @@ -8921,6 +8969,11 @@ node-forge@^1: resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== +node-gyp-build@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" + integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== + node-gyp@^8.4.1: version "8.4.1" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz" @@ -12138,7 +12191,17 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typedarray-to-buffer@^3.1.2, typedarray-to-buffer@^3.1.5: +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + +typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== @@ -12268,6 +12331,13 @@ use@^3.1.0: resolved "https://registry.npmjs.org/use/-/use-3.1.1.tgz" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -12674,15 +12744,17 @@ websocket-extensions@>=0.1.1: resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -websocket@1.0.23: - version "1.0.23" - resolved "https://registry.npmjs.org/websocket/-/websocket-1.0.23.tgz" - integrity sha1-IN6OxKcSawlGVXjNXbspqcKWqsY= +websocket@1.0.34: + version "1.0.34" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111" + integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ== dependencies: + bufferutil "^4.0.1" debug "^2.2.0" - nan "^2.3.3" - typedarray-to-buffer "^3.1.2" - yaeti "^0.0.4" + es5-ext "^0.10.50" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" whatwg-encoding@^1.0.5: version "1.0.5" @@ -12900,10 +12972,10 @@ y18n@^5.0.5: resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yaeti@^0.0.4: - version "0.0.4" - resolved "https://registry.npmjs.org/yaeti/-/yaeti-0.0.4.tgz" - integrity sha1-if5znEWsRJECiXMZMmKoN2k6ZrY= +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug== yallist@^4.0.0: version "4.0.0"