SONAR-13400 Portfolios pages are not available while indexation is in progress SONAR-13398 Project page is not available while indexation is in progress SONAR-13402 Application page is not available while indexation is in progresstags/8.4.0.35506
@@ -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; | |||
@@ -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 |
@@ -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); |
@@ -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); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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} /> |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
}; | |||
} |
@@ -126,6 +126,7 @@ declare namespace T { | |||
isFavorite?: boolean; | |||
leakPeriodDate?: string; | |||
name: string; | |||
needIssueSync?: boolean; | |||
path?: string; | |||
refKey?: string; | |||
qualityProfiles?: ComponentQualityProfile[]; |
@@ -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 |