aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2023-08-08 16:09:33 +0200
committersonartech <sonartech@sonarsource.com>2023-08-09 20:03:37 +0000
commit714d0e3853dc1adff11e9b4928ba074fecc4d2ca (patch)
tree348650f288aa9f8d75bf62da5be6b0a1b4b408f2 /server
parent3890d518d85f6cd63627d974f2cdbc931b1e068b (diff)
downloadsonarqube-714d0e3853dc1adff11e9b4928ba074fecc4d2ca.tar.gz
sonarqube-714d0e3853dc1adff11e9b4928ba074fecc4d2ca.zip
SONAR-19789 Display message when permission sync is pending on onboard project page
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/design-system/src/components/index.ts2
-rw-r--r--server/sonar-web/src/main/js/api/ce.ts4
-rw-r--r--server/sonar-web/src/main/js/api/mocks/ComputeEngineServiceMock.ts26
-rw-r--r--server/sonar-web/src/main/js/app/components/ComponentContainer.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/App.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/EmptyOverview.tsx43
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/__tests__/App-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/queries/component.ts33
-rw-r--r--server/sonar-web/src/main/js/types/tasks.ts1
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',