diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2023-08-08 16:09:33 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-08-09 20:03:37 +0000 |
commit | 714d0e3853dc1adff11e9b4928ba074fecc4d2ca (patch) | |
tree | 348650f288aa9f8d75bf62da5be6b0a1b4b408f2 /server | |
parent | 3890d518d85f6cd63627d974f2cdbc931b1e068b (diff) | |
download | sonarqube-714d0e3853dc1adff11e9b4928ba074fecc4d2ca.tar.gz sonarqube-714d0e3853dc1adff11e9b4928ba074fecc4d2ca.zip |
SONAR-19789 Display message when permission sync is pending on onboard project page
Diffstat (limited to 'server')
9 files changed, 138 insertions, 26 deletions
diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 5536c8be6cd..484ae86999d 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -31,7 +31,7 @@ export * from './CodeSnippet'; export * from './CodeSyntaxHighlighter'; export * from './ColorsLegend'; export * from './CoverageIndicator'; -export { DeferredSpinner } from './DeferredSpinner'; +export { DeferredSpinner, Spinner } from './DeferredSpinner'; export { ActionsDropdown, Dropdown } from './Dropdown'; export * from './DropdownMenu'; export { DropdownToggler } from './DropdownToggler'; diff --git a/server/sonar-web/src/main/js/api/ce.ts b/server/sonar-web/src/main/js/api/ce.ts index fad77f54f62..8ca0c07377e 100644 --- a/server/sonar-web/src/main/js/api/ce.ts +++ b/server/sonar-web/src/main/js/api/ce.ts @@ -66,7 +66,9 @@ export function cancelAllTasks(): Promise<any> { return post('/api/ce/cancel_all'); } -export function getTasksForComponent(component: string): Promise<{ queue: Task[]; current: Task }> { +export function getTasksForComponent( + component: string +): Promise<{ queue: Task[]; current?: Task }> { return getJSON('/api/ce/component', { component }).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts index 51544f39231..3cb3cbbf21a 100644 --- a/server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts @@ -30,6 +30,7 @@ import { getActivity, getStatus, getTask, + getTasksForComponent, getTypes, getWorkers, setWorkerCount, @@ -65,14 +66,15 @@ export default class ComputeEngineServiceMock { workers = { ...DEFAULT_WORKERS }; constructor() { - (cancelAllTasks as jest.Mock).mockImplementation(this.handleCancelAllTasks); - (cancelTask as jest.Mock).mockImplementation(this.handleCancelTask); + jest.mocked(cancelAllTasks).mockImplementation(this.handleCancelAllTasks); + jest.mocked(cancelTask).mockImplementation(this.handleCancelTask); jest.mocked(getActivity).mockImplementation(this.handleGetActivity); - (getStatus as jest.Mock).mockImplementation(this.handleGetStatus); - (getTypes as jest.Mock).mockImplementation(this.handleGetTypes); + jest.mocked(getStatus).mockImplementation(this.handleGetStatus); + jest.mocked(getTypes).mockImplementation(this.handleGetTypes); jest.mocked(getTask).mockImplementation(this.handleGetTask); - (getWorkers as jest.Mock).mockImplementation(this.handleGetWorkers); - (setWorkerCount as jest.Mock).mockImplementation(this.handleSetWorkerCount); + jest.mocked(getWorkers).mockImplementation(this.handleGetWorkers); + jest.mocked(setWorkerCount).mockImplementation(this.handleSetWorkerCount); + jest.mocked(getTasksForComponent).mockImplementation(this.handleGetTaskForComponent); this.tasks = cloneDeep(DEFAULT_TASKS); } @@ -197,6 +199,18 @@ export default class ComputeEngineServiceMock { return Promise.resolve(); }; + handleGetTaskForComponent = (componentKey: string) => { + const tasks = this.tasks.filter((t) => t.componentKey === componentKey); + return Promise.resolve({ + queue: tasks.filter( + (t) => t.status === TaskStatuses.InProgress || t.status === TaskStatuses.Pending + ), + current: tasks.find( + (t) => t.status === TaskStatuses.Success || t.status === TaskStatuses.Failed + ), + }); + }; + /* * Helpers */ diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index b0d4039c39e..2fde9f19ab0 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -223,7 +223,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> { qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier, }); - getCurrentTask = (current: Task) => { + getCurrentTask = (current?: Task) => { if (!current || !this.isReportRelatedTask(current)) { return undefined; } diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx index d6231334fcd..dc8d50d745a 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/App.tsx @@ -37,13 +37,11 @@ import EmptyOverview from './EmptyOverview'; interface AppProps extends WithAvailableFeaturesProps { component: Component; - isInProgress?: boolean; - isPending?: boolean; projectBinding?: ProjectAlmBindingResponse; } export function App(props: AppProps) { - const { component, projectBinding, isPending, isInProgress } = props; + const { component, projectBinding } = props; const branchSupportEnabled = props.hasFeature(Feature.BranchSupport); const { data } = useBranchesQuery(component); @@ -70,7 +68,6 @@ export function App(props: AppProps) { branchLike={branchLike} branchLikes={branchLikes} component={component} - hasAnalyses={isPending ?? isInProgress} /> )} diff --git a/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx b/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx index 5ef4b9708c0..783b6ed1a89 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx @@ -17,15 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { FlagMessage, LargeCenteredLayout, PageContentFontWrapper } from 'design-system'; +import { FlagMessage, LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system'; import * as React from 'react'; import { Navigate } from 'react-router-dom'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import { getBranchLikeDisplayName, isBranch, isMainBranch } from '../../../helpers/branch-like'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getProjectTutorialLocation } from '../../../helpers/urls'; +import { useTaskForComponentQuery } from '../../../queries/component'; import { BranchLike } from '../../../types/branch-like'; import { ComponentQualifier } from '../../../types/component'; +import { TaskTypes } from '../../../types/tasks'; import { Component } from '../../../types/types'; import { CurrentUser, isLoggedIn } from '../../../types/users'; @@ -34,20 +36,26 @@ export interface EmptyOverviewProps { branchLikes: BranchLike[]; component: Component; currentUser: CurrentUser; - hasAnalyses?: boolean; } export function EmptyOverview(props: EmptyOverviewProps) { - const { branchLike, branchLikes, component, currentUser, hasAnalyses } = props; + const { branchLike, branchLikes, component, currentUser } = props; + const { data, isLoading } = useTaskForComponentQuery(component); + const hasQueuedAnalyses = + data && data.queue.filter((task) => task.type === TaskTypes.Report).length > 0; + const hasPermissionSyncInProgess = + data && + data.queue.filter((task) => task.type === TaskTypes.GithubProjectPermissionsProvisioning) + .length > 0; + + if (isLoading) { + return <Spinner />; + } if (component.qualifier === ComponentQualifier.Application) { return ( <LargeCenteredLayout className="sw-pt-8"> - <FlagMessage - className="sw-w-full" - variant="warning" - aria-label={translate('provisioning.no_analysis.application')} - > + <FlagMessage className="sw-w-full" variant="warning"> {translate('provisioning.no_analysis.application')} </FlagMessage> </LargeCenteredLayout> @@ -61,7 +69,20 @@ export function EmptyOverview(props: EmptyOverviewProps) { branchLikes.length > 2 || (branchLikes.length === 2 && branchLikes.some((branch) => isBranch(branch))); - const showTutorial = isMainBranch(branchLike) && !hasBranches && !hasAnalyses; + if (hasPermissionSyncInProgess) { + return ( + <LargeCenteredLayout className="sw-pt-8"> + <PageContentFontWrapper> + <FlagMessage variant="warning"> + {translate('provisioning.permission_synch_in_progress')} + <Spinner className="sw-ml-8 sw-hidden" aria-hidden /> + </FlagMessage> + </PageContentFontWrapper> + </LargeCenteredLayout> + ); + } + + const showTutorial = isMainBranch(branchLike) && !hasBranches && !hasQueuedAnalyses; if (showTutorial && isLoggedIn(currentUser)) { return <Navigate replace to={getProjectTutorialLocation(component.key)} />; @@ -87,13 +108,13 @@ export function EmptyOverview(props: EmptyOverviewProps) { {isLoggedIn(currentUser) ? ( <> {hasBranches && ( - <FlagMessage className="sw-w-full" variant="warning" aria-label={warning}> + <FlagMessage className="sw-w-full" variant="warning"> {warning} </FlagMessage> )} </> ) : ( - <FlagMessage className="sw-w-full" variant="warning" aria-label={warning}> + <FlagMessage className="sw-w-full" variant="warning"> {warning} </FlagMessage> )} diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx index eeaeaf0c1c5..5bab683525a 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx @@ -20,18 +20,23 @@ import { screen } from '@testing-library/react'; import * as React from 'react'; import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock'; +import ComputeEngineServiceMock from '../../../../api/mocks/ComputeEngineServiceMock'; import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider'; import { mockBranch } from '../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../helpers/mocks/component'; +import { mockTask } from '../../../../helpers/mocks/tasks'; import { mockCurrentUser } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { ComponentQualifier } from '../../../../types/component'; +import { TaskStatuses, TaskTypes } from '../../../../types/tasks'; import { App } from '../App'; -const handler = new BranchesServiceMock(); +const handlerBranches = new BranchesServiceMock(); +const handlerCe = new ComputeEngineServiceMock(); beforeEach(() => { - handler.reset(); + handlerBranches.reset(); + handlerCe.reset(); }); it('should render Empty Overview for Application with no analysis', async () => { @@ -72,6 +77,45 @@ it('should not render for portfolios and subportfolios', () => { expect(rtl.container).toBeEmptyDOMElement(); }); +describe('Permission provisioning', () => { + beforeEach(() => { + jest.useFakeTimers({ advanceTimers: true }); + }); + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + it('should render warning when permission is sync', async () => { + handlerCe.addTask( + mockTask({ + componentKey: 'my-project', + type: TaskTypes.GithubProjectPermissionsProvisioning, + status: TaskStatuses.InProgress, + }) + ); + + renderApp(); + await jest.runOnlyPendingTimersAsync(); + + expect( + await screen.findByText('provisioning.permission_synch_in_progress') + ).toBeInTheDocument(); + + handlerCe.clearTasks(); + handlerCe.addTask( + mockTask({ + componentKey: 'my-project', + type: TaskTypes.GithubProjectPermissionsProvisioning, + status: TaskStatuses.Success, + }) + ); + + await jest.runOnlyPendingTimersAsync(); + + expect(screen.queryByText('provisioning.permission_synch_in_progress')).not.toBeInTheDocument(); + }); +}); + function renderApp(props = {}, userProps = {}) { return renderComponent( <CurrentUserContextProvider currentUser={mockCurrentUser({ isLoggedIn: true, ...userProps })}> diff --git a/server/sonar-web/src/main/js/queries/component.ts b/server/sonar-web/src/main/js/queries/component.ts new file mode 100644 index 00000000000..6d9a4d32362 --- /dev/null +++ b/server/sonar-web/src/main/js/queries/component.ts @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { useQuery } from '@tanstack/react-query'; +import { getTasksForComponent } from '../api/ce'; +import { Component } from '../types/types'; + +const TASK_RETRY = 10_000; + +export function useTaskForComponentQuery(component: Component) { + return useQuery({ + queryKey: ['component', component.key, 'tasks'] as const, + queryFn: ({ queryKey: [_, key] }) => getTasksForComponent(key), + refetchInterval: TASK_RETRY, + }); +} diff --git a/server/sonar-web/src/main/js/types/tasks.ts b/server/sonar-web/src/main/js/types/tasks.ts index 52794ab7137..1b8998f0b35 100644 --- a/server/sonar-web/src/main/js/types/tasks.ts +++ b/server/sonar-web/src/main/js/types/tasks.ts @@ -21,6 +21,7 @@ export enum TaskTypes { Report = 'REPORT', IssueSync = 'ISSUE_SYNC', GithubProvisioning = 'GITHUB_AUTH_PROVISIONING', + GithubProjectPermissionsProvisioning = 'GITHUB_PROJECT_PERMISSIONS_PROVISIONING', AppRefresh = 'APP_REFRESH', ViewRefresh = 'VIEW_REFRESH', ProjectExport = 'PROJECT_EXPORT', |