diff options
11 files changed, 307 insertions, 3 deletions
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 cb82b50cc0b..ecab0962600 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -42,6 +42,7 @@ import { BranchLike } from '../../types/branch-like'; import { isPortfolioLike } from '../../types/component'; import ComponentContainerNotFound from './ComponentContainerNotFound'; import { ComponentContext } from './ComponentContext'; +import PageUnavailableDueToIndexation from './indexation/PageUnavailableDueToIndexation'; import ComponentNav from './nav/component/ComponentNav'; interface Props { @@ -322,6 +323,10 @@ export class ComponentContainer extends React.PureComponent<Props, State> { return <ComponentContainerNotFound />; } + if (component?.needIssueSync) { + return <PageUnavailableDueToIndexation component={component} />; + } + const { branchLike, branchLikes, currentTask, isPending, tasksInProgress } = this.state; const isInProgress = tasksInProgress && tasksInProgress.length > 0; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index a69eb36c59b..6d12970fe6b 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -29,6 +29,7 @@ import { mockBranch, mockMainBranch, mockPullRequest } from '../../../helpers/mo import { mockComponent, mockLocation, mockRouter } from '../../../helpers/testMocks'; import { ComponentQualifier } from '../../../types/component'; import { ComponentContainer } from '../ComponentContainer'; +import PageUnavailableDueToIndexation from '../indexation/PageUnavailableDueToIndexation'; jest.mock('../../../api/branches', () => { const { mockMainBranch, mockPullRequest } = require.requireActual( @@ -210,6 +211,18 @@ it('should redirect if the component is a portfolio', async () => { expect(replace).toBeCalledWith({ pathname: '/portfolio', query: { id: componentKey } }); }); +it('should display display the unavailable page if the component needs issue sync', async () => { + (getComponentData as jest.Mock).mockResolvedValueOnce({ + component: { key: 'test', qualifier: ComponentQualifier.Project, needIssueSync: true } + }); + + const wrapper = shallowRender(); + + await waitAndUpdate(wrapper); + + expect(wrapper.find(PageUnavailableDueToIndexation).exists()).toBe(true); +}); + function shallowRender(props: Partial<ComponentContainer['props']> = {}) { return shallow<ComponentContainer>( <ComponentContainer diff --git a/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx b/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx index 16cfa0a4caf..3beaf4c1f4b 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx +++ b/server/sonar-web/src/main/js/app/components/extensions/PortfoliosPage.tsx @@ -18,8 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; +import { PageContext } from '../indexation/PageUnavailableDueToIndexation'; import GlobalPageExtension from './GlobalPageExtension'; -export default function PortfoliosPage() { +export function PortfoliosPage() { return <GlobalPageExtension params={{ pluginKey: 'governance', extensionKey: 'portfolios' }} />; } + +export default withIndexationGuard(PortfoliosPage, PageContext.Portfolios); diff --git a/server/sonar-web/src/main/js/app/components/indexation/PageUnavailableDueToIndexation.tsx b/server/sonar-web/src/main/js/app/components/indexation/PageUnavailableDueToIndexation.tsx new file mode 100644 index 00000000000..ef88aa8cef6 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/indexation/PageUnavailableDueToIndexation.tsx @@ -0,0 +1,79 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Alert } from 'sonar-ui-common/components/ui/Alert'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import withIndexationContext, { + WithIndexationContextProps +} from '../../../components/hoc/withIndexationContext'; + +interface Props extends WithIndexationContextProps { + pageContext?: PageContext; + component?: Pick<T.Component, 'qualifier' | 'name'>; +} + +export enum PageContext { + Issues = 'issues', + Portfolios = 'portfolios' +} + +export class PageUnavailableDueToIndexation extends React.PureComponent<Props> { + componentDidUpdate() { + if (this.props.indexationContext?.status.isCompleted) { + window.location.reload(); + } + } + + render() { + const { pageContext, component } = this.props; + let messageKey = 'indexation.page_unavailable.title'; + + if (pageContext) { + messageKey = `${messageKey}.${pageContext}`; + } + + return ( + <div className="page-wrapper-simple"> + <div className="page-simple"> + <h1 className="big-spacer-bottom"> + <FormattedMessage + id={messageKey} + defaultMessage={translate(messageKey)} + values={{ + componentQualifier: translate('qualifier', component?.qualifier ?? ''), + componentName: <em>{component?.name}</em> + }} + /> + </h1> + <Alert variant="info"> + <p>{translate('indexation.page_unavailable.description')}</p> + <p className="spacer-top"> + {translate('indexation.page_unavailable.description.additional_information')} + </p> + </Alert> + </div> + </div> + ); + } +} + +export default withIndexationContext(PageUnavailableDueToIndexation); diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx b/server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx new file mode 100644 index 00000000000..d988c8764dc --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { ComponentQualifier } from '../../../../types/component'; +import { PageContext, PageUnavailableDueToIndexation } from '../PageUnavailableDueToIndexation'; + +it('should render correctly', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); +}); + +it('should refresh the page once the indexation is complete', () => { + const reload = jest.fn(); + delete window.location; + (window as any).location = { reload }; + + const wrapper = shallowRender(); + + expect(reload).not.toHaveBeenCalled(); + + wrapper.setProps({ indexationContext: { status: { isCompleted: true } } }); + wrapper.update(); + + expect(reload).toHaveBeenCalled(); +}); + +function shallowRender(props?: PageUnavailableDueToIndexation['props']) { + return shallow( + <PageUnavailableDueToIndexation + indexationContext={{ + status: { isCompleted: false } + }} + pageContext={PageContext.Issues} + component={{ qualifier: ComponentQualifier.Portfolio, name: 'test-portfolio' }} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/PageUnavailableDueToIndexation-test.tsx.snap b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/PageUnavailableDueToIndexation-test.tsx.snap new file mode 100644 index 00000000000..be114de0437 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/PageUnavailableDueToIndexation-test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<div + className="page-wrapper-simple" +> + <div + className="page-simple" + > + <h1 + className="big-spacer-bottom" + > + <FormattedMessage + defaultMessage="indexation.page_unavailable.title.issues" + id="indexation.page_unavailable.title.issues" + values={ + Object { + "componentName": <em> + test-portfolio + </em>, + "componentQualifier": "qualifier.VW", + } + } + /> + </h1> + <Alert + variant="info" + > + <p> + indexation.page_unavailable.description + </p> + <p + className="spacer-top" + > + indexation.page_unavailable.description.additional_information + </p> + </Alert> + </div> +</div> +`; diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index cc3c004f5e0..091ca9f1457 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -65,8 +65,10 @@ import systemRoutes from '../../apps/system/routes'; import usersRoutes from '../../apps/users/routes'; import webAPIRoutes from '../../apps/web-api/routes'; import webhooksRoutes from '../../apps/webhooks/routes'; +import withIndexationGuard from '../../components/hoc/withIndexationGuard'; import App from '../components/App'; import GlobalContainer from '../components/GlobalContainer'; +import { PageContext } from '../components/indexation/PageUnavailableDueToIndexation'; import MigrationContainer from '../components/MigrationContainer'; import * as theme from '../theme'; import getStore from './getStore'; @@ -300,7 +302,10 @@ export default function startReactApp( import('../components/extensions/GlobalPageExtension') )} /> - <Route path="issues" component={Issues} /> + <Route + path="issues" + component={withIndexationGuard(Issues, PageContext.Issues)} + /> <RouteWithChildRoutes path="organizations" childRoutes={organizationsRoutes} /> <RouteWithChildRoutes path="projects" childRoutes={projectsRoutes} /> <RouteWithChildRoutes path="quality_gates" childRoutes={qualityGatesRoutes} /> diff --git a/server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx b/server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx new file mode 100644 index 00000000000..fca47cf1b8d --- /dev/null +++ b/server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { mount } from 'enzyme'; +import * as React from 'react'; +import { IndexationContext } from '../../../app/components/indexation/IndexationContext'; +import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation'; +import { IndexationContextInterface } from '../../../types/indexation'; +import withIndexationGuard from '../withIndexationGuard'; + +it('should render correctly', () => { + let wrapper = mountRender(); + expect(wrapper.find(TestComponent).exists()).toBe(false); + + wrapper = mountRender({ status: { isCompleted: true } }); + expect(wrapper.find(TestComponent).exists()).toBe(true); +}); + +function mountRender(context?: Partial<IndexationContextInterface>) { + return mount( + <IndexationContext.Provider value={{ status: { isCompleted: false }, ...context }}> + <TestComponentWithGuard /> + </IndexationContext.Provider> + ); +} + +class TestComponent extends React.PureComponent { + render() { + return <h1>TestComponent</h1>; + } +} + +const TestComponentWithGuard = withIndexationGuard(TestComponent, PageContext.Issues); diff --git a/server/sonar-web/src/main/js/components/hoc/withIndexationGuard.tsx b/server/sonar-web/src/main/js/components/hoc/withIndexationGuard.tsx new file mode 100644 index 00000000000..0036b19fdec --- /dev/null +++ b/server/sonar-web/src/main/js/components/hoc/withIndexationGuard.tsx @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 * as React from 'react'; +import { IndexationContext } from '../../app/components/indexation/IndexationContext'; +import PageUnavailableDueToIndexation, { + PageContext +} from '../../app/components/indexation/PageUnavailableDueToIndexation'; + +export default function withIndexationGuard<P>( + WrappedComponent: React.ComponentType<P>, + pageContext: PageContext +) { + return class WithIndexationGuard extends React.PureComponent<P> { + render() { + return ( + <IndexationContext.Consumer> + {context => + context?.status.isCompleted ? ( + <WrappedComponent {...this.props} /> + ) : ( + <PageUnavailableDueToIndexation pageContext={pageContext} /> + ) + } + </IndexationContext.Consumer> + ); + } + }; +} diff --git a/server/sonar-web/src/main/js/types/types.d.ts b/server/sonar-web/src/main/js/types/types.d.ts index 72b81c469c0..9f92aa7cd85 100644 --- a/server/sonar-web/src/main/js/types/types.d.ts +++ b/server/sonar-web/src/main/js/types/types.d.ts @@ -126,6 +126,7 @@ declare namespace T { isFavorite?: boolean; leakPeriodDate?: string; name: string; + needIssueSync?: boolean; path?: string; refKey?: string; qualityProfiles?: ComponentQualityProfile[]; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index bd2f2512a13..97bd3af3747 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3566,7 +3566,11 @@ indexation.in_progress=SonarQube is reloading project data. Some projects will b indexation.in_progress.details={0}% completed. indexation.in_progress.admin_details=See {link}. indexation.completed=All project data has been reloaded. - +indexation.page_unavailable.title.issues=Issues page is temporarily unavailable +indexation.page_unavailable.title.portfolios=Portfolios page is temporarily unavailable +indexation.page_unavailable.title={componentQualifier} {componentName} is temporarily unavailable +indexation.page_unavailable.description=This page will be available after the data is reloaded. This might take a while depending on the amount of projects and issues in your SonarQube instance. +indexation.page_unavailable.description.additional_information=You can keep analyzing your projects during this process. #------------------------------------------------------------------------------ # # HOMEPAGE |