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';
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);
}
getActivity,
getStatus,
getTask,
+ getTasksForComponent,
getTypes,
getWorkers,
setWorkerCount,
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);
}
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
*/
qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier,
});
- getCurrentTask = (current: Task) => {
+ getCurrentTask = (current?: Task) => {
if (!current || !this.isReportRelatedTask(current)) {
return undefined;
}
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);
branchLike={branchLike}
branchLikes={branchLikes}
component={component}
- hasAnalyses={isPending ?? isInProgress}
/>
)}
* 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';
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>
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)} />;
{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>
)}
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 () => {
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 })}>
--- /dev/null
+/*
+ * 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,
+ });
+}
Report = 'REPORT',
IssueSync = 'ISSUE_SYNC',
GithubProvisioning = 'GITHUB_AUTH_PROVISIONING',
+ GithubProjectPermissionsProvisioning = 'GITHUB_PROJECT_PERMISSIONS_PROVISIONING',
AppRefresh = 'APP_REFRESH',
ViewRefresh = 'VIEW_REFRESH',
ProjectExport = 'PROJECT_EXPORT',
provisioning.only_provisioned=Only Provisioned
provisioning.only_provisioned.tooltip=Provisioned projects are projects that have been created, but have not been analyzed yet.
provisioning.no_analysis.application=No analysis has been performed since creation. Analyze a project to see information here.
+provisioning.permission_synch_in_progress=Project permissions are being synchronized.
#------------------------------------------------------------------------------