Просмотр исходного кода

SONAR-19777 Lock project security reports page

tags/10.2.0.77647
David Cho-Lerat 10 месяцев назад
Родитель
Сommit
3b618651aa

+ 2
- 2
server/sonar-web/design-system/src/components/NavBarTabs.tsx Просмотреть файл

@@ -24,9 +24,9 @@ import React from 'react';
import tw, { theme } from 'twin.macro';
import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
import { isDefined } from '../helpers/types';
import { ChevronDownIcon } from './icons/ChevronDownIcon';
import NavLink, { NavLinkProps } from './NavLink';
import Tooltip from './Tooltip';
import { ChevronDownIcon } from './icons/ChevronDownIcon';

interface Props extends React.HTMLAttributes<HTMLUListElement> {
children?: React.ReactNode;
@@ -126,7 +126,7 @@ const NavBarTabLinkWrapper = styled.li`
&:has(a.disabled-link) > a:hover,
&:has(a.disabled-link) > a.hover,
&:has(a.disabled-link)[aria-expanded='true'] {
${tw`sw-cursor-not-allowed`};
${tw`sw-cursor-default`};
border-bottom: ${themeBorder('xsActive', 'transparent', 1)};
color: ${themeContrast('subnavigationDisabled')};
}

+ 23
- 38
server/sonar-web/src/main/js/app/components/indexation/PageUnavailableDueToIndexation.tsx Просмотреть файл

@@ -17,26 +17,16 @@
* 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, Link } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import withIndexationContext, {
WithIndexationContextProps,
} from '../../../components/hoc/withIndexationContext';
import { Alert } from '../../../components/ui/Alert';
import { translate } from '../../../helpers/l10n';
import { Component } from '../../../types/types';

interface Props extends WithIndexationContextProps {
pageContext?: PageContext;
component?: Pick<Component, 'qualifier' | 'name'>;
}

export enum PageContext {
Issues = 'issues',
Portfolios = 'portfolios',
}

export class PageUnavailableDueToIndexation extends React.PureComponent<Props> {
export class PageUnavailableDueToIndexation extends React.PureComponent<WithIndexationContextProps> {
componentDidUpdate() {
if (
this.props.indexationContext.status.isCompleted &&
@@ -47,33 +37,28 @@ export class PageUnavailableDueToIndexation extends React.PureComponent<Props> {
}

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>
<FlagMessage className="sw-m-10" variant="info">
{translate('indexation.page_unavailable.description')}
<br />
<FormattedMessage
defaultMessage={translate(
'indexation.page_unavailable.description.additional_information'
)}
id="indexation.page_unavailable.description.additional_information"
values={{
link: (
<Link
className="sw-ml-4"
to="https://docs.sonarqube.org/latest/instance-administration/reindexing/"
>
{translate('learn_more')}
</Link>
),
}}
/>
</FlagMessage>
</div>
);
}

+ 6
- 6
server/sonar-web/src/main/js/app/components/indexation/__tests__/PageUnavailableDueToIndexation-test.tsx Просмотреть файл

@@ -17,13 +17,14 @@
* 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';
import { PageUnavailableDueToIndexation } from '../PageUnavailableDueToIndexation';

it('should render correctly', () => {
const wrapper = shallowRender();

expect(wrapper).toMatchSnapshot();
});

@@ -42,6 +43,7 @@ it('should not refresh the page once the indexation is complete if there were fa
wrapper.setProps({
indexationContext: { status: { isCompleted: true, percentCompleted: 100, hasFailures: true } },
});

wrapper.update();

expect(reload).not.toHaveBeenCalled();
@@ -62,20 +64,18 @@ it('should refresh the page once the indexation is complete if there were NO fai
wrapper.setProps({
indexationContext: { status: { isCompleted: true, percentCompleted: 100, hasFailures: false } },
});

wrapper.update();

expect(reload).toHaveBeenCalled();
});

function shallowRender(props?: PageUnavailableDueToIndexation['props']) {
function shallowRender() {
return shallow<PageUnavailableDueToIndexation>(
<PageUnavailableDueToIndexation
indexationContext={{
status: { isCompleted: false, percentCompleted: 23, hasFailures: false },
}}
pageContext={PageContext.Issues}
component={{ qualifier: ComponentQualifier.Portfolio, name: 'test-portfolio' }}
{...props}
/>
);
}

+ 19
- 30
server/sonar-web/src/main/js/app/components/indexation/__tests__/__snapshots__/PageUnavailableDueToIndexation-test.tsx.snap Просмотреть файл

@@ -4,37 +4,26 @@ exports[`should render correctly 1`] = `
<div
className="page-wrapper-simple"
>
<div
className="page-simple"
<FlagMessage
className="sw-m-10"
variant="info"
>
<h1
className="big-spacer-bottom"
>
<FormattedMessage
defaultMessage="indexation.page_unavailable.title.issues"
id="indexation.page_unavailable.title.issues"
values={
{
"componentName": <em>
test-portfolio
</em>,
"componentQualifier": "qualifier.VW",
}
indexation.page_unavailable.description
<br />
<FormattedMessage
defaultMessage="indexation.page_unavailable.description.additional_information"
id="indexation.page_unavailable.description.additional_information"
values={
{
"link": <StandoutLink
className="sw-ml-4"
to="https://docs.sonarqube.org/latest/instance-administration/reindexing/"
>
learn_more
</StandoutLink>,
}
/>
</h1>
<Alert
variant="info"
>
<p>
indexation.page_unavailable.description
</p>
<p
className="spacer-top"
>
indexation.page_unavailable.description.additional_information
</p>
</Alert>
</div>
}
/>
</FlagMessage>
</div>
`;

+ 99
- 60
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx Просмотреть файл

@@ -35,16 +35,18 @@ import { keyBy, omit, without } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { FormattedMessage } from 'react-intl';
import { searchIssues } from '../../../api/issues';
import { listIssues, searchIssues } from '../../../api/issues';
import { getRuleDetails } from '../../../api/rules';
import withComponentContext from '../../../app/components/componentContext/withComponentContext';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import { PageContext } from '../../../app/components/indexation/PageUnavailableDueToIndexation';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import EmptySearch from '../../../components/common/EmptySearch';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import ListFooter from '../../../components/controls/ListFooter';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import withIndexationContext, {
WithIndexationContextProps,
} from '../../../components/hoc/withIndexationContext';
import withIndexationGuard from '../../../components/hoc/withIndexationGuard';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
import IssueTabViewer from '../../../components/rules/IssueTabViewer';
@@ -106,7 +108,7 @@ import NoMyIssues from './NoMyIssues';
import PageActions from './PageActions';
import StyledHeader, { PSEUDO_SHADOW_HEIGHT } from './StyledHeader';

interface Props {
interface Props extends WithIndexationContextProps {
branchLike?: BranchLike;
component?: Component;
currentUser: CurrentUser;
@@ -147,6 +149,7 @@ export interface State {
const DEFAULT_QUERY = { resolved: 'false' };
const MAX_INITAL_FETCH = 1000;
const VARIANTS_FACET = 'codeVariants';
const ISSUES_PAGE_SIZE = 100;

export class App extends React.PureComponent<Props, State> {
mounted = false;
@@ -456,6 +459,19 @@ export class App extends React.PureComponent<Props, State> {
createdAfterIncludesTime = () => Boolean(this.props.location.query.createdAfter?.includes('T'));

fetchIssuesHelper = (query: RawQuery) => {
if (this.props.component?.needIssueSync) {
return listIssues({
...query,
}).then((response) => {
const { components, issues, rules } = response;
const parsedIssues = issues.map((issue) =>
parseIssueFromResponse(issue, components, undefined, rules)
);

return { ...response, issues: parsedIssues } as FetchIssuesPromise;
});
}

return searchIssues({
...query,
additionalFields: '_all',
@@ -487,15 +503,23 @@ export class App extends React.PureComponent<Props, State> {
facets = facets ? `${facets},${VARIANTS_FACET}` : VARIANTS_FACET;
}

const parameters: Dict<string | undefined> = {
...getBranchLikeQuery(this.props.branchLike),
componentKeys: component?.key,
s: 'FILE_LINE',
...serializeQuery(query),
ps: '100',
facets,
...additional,
};
const parameters: Dict<string | undefined> = component?.needIssueSync
? {
...getBranchLikeQuery(this.props.branchLike, true),
project: component?.key,
...serializeQuery(query),
ps: `${ISSUES_PAGE_SIZE}`,
...additional,
}
: {
...getBranchLikeQuery(this.props.branchLike),
componentKeys: component?.key,
s: 'FILE_LINE',
...serializeQuery(query),
ps: `${ISSUES_PAGE_SIZE}`,
facets,
...additional,
};

if (query.createdAfter !== undefined && this.createdAfterIncludesTime()) {
parameters.createdAfter = serializeDate(query.createdAfter);
@@ -535,50 +559,51 @@ export class App extends React.PureComponent<Props, State> {
fetchPromise = this.fetchIssues({}, true, firstRequest);
}

return fetchPromise.then(
({ effortTotal, facets, issues, paging, ...other }) => {
if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) {
const openIssue = getOpenIssue(this.props, issues);
let selected: string | undefined = undefined;

if (issues.length > 0) {
selected = openIssue ? openIssue.key : issues[0].key;
}

this.setState(({ showVariantsFilter }) => ({
cannotShowOpenIssue: Boolean(openIssueKey && !openIssue),
effortTotal,
facets: parseFacets(facets),
showVariantsFilter: firstRequest
? Boolean(facets.find((f) => f.property === VARIANTS_FACET)?.values.length)
: showVariantsFilter,
loading: false,
locationsNavigator: true,
issues,
openIssue,
paging,
referencedComponentsById: keyBy(other.components, 'uuid'),
referencedComponentsByKey: keyBy(other.components, 'key'),
referencedLanguages: keyBy(other.languages, 'key'),
referencedRules: keyBy(other.rules, 'key'),
referencedUsers: keyBy(other.users, 'login'),
selected,
selectedFlowIndex: 0,
selectedLocationIndex: undefined,
}));
}
return fetchPromise.then(this.parseFirstIssues(firstRequest, openIssueKey, prevQuery), () => {
if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) {
this.setState({ loading: false });
}

return issues;
},
() => {
if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) {
this.setState({ loading: false });
return [];
});
}

parseFirstIssues =
(firstRequest: boolean, openIssueKey: string | undefined, prevQuery: RawQuery) =>
({ effortTotal, facets, issues, paging, ...other }: FetchIssuesPromise) => {
if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) {
const openIssue = getOpenIssue(this.props, issues);
let selected: string | undefined = undefined;

if (issues.length > 0) {
selected = openIssue ? openIssue.key : issues[0].key;
}

return [];
this.setState(({ showVariantsFilter }) => ({
cannotShowOpenIssue: Boolean(openIssueKey && !openIssue),
effortTotal,
facets: parseFacets(facets),
showVariantsFilter: firstRequest
? Boolean(facets?.find((f) => f.property === VARIANTS_FACET)?.values.length)
: showVariantsFilter,
loading: false,
locationsNavigator: true,
issues,
openIssue,
paging,
referencedComponentsById: keyBy(other.components, 'uuid'),
referencedComponentsByKey: keyBy(other.components, 'key'),
referencedLanguages: keyBy(other.languages, 'key'),
referencedRules: keyBy(other.rules, 'key'),
referencedUsers: keyBy(other.users, 'login'),
selected,
selectedFlowIndex: 0,
selectedLocationIndex: undefined,
}));
}
);
}

return issues;
};

fetchIssuesPage = (p: number) => {
return this.fetchIssues({ p });
@@ -966,7 +991,7 @@ export class App extends React.PureComponent<Props, State> {
>
{warning && <div className="sw-pb-6">{warning}</div>}

{currentUser.isLoggedIn && (
{currentUser.isLoggedIn && !component?.needIssueSync && (
<div className="sw-flex sw-justify-start sw-mb-8">
<ToggleButton
onChange={this.handleMyIssuesChange}
@@ -1089,7 +1114,7 @@ export class App extends React.PureComponent<Props, State> {

let noIssuesMessage = null;

if (paging.total === 0 && !loading) {
if (issues.length === 0 && !loading) {
if (this.isFiltered()) {
noIssuesMessage = <EmptySearch />;
} else if (this.state.myIssues) {
@@ -1103,7 +1128,7 @@ export class App extends React.PureComponent<Props, State> {
<div>
<h2 className="a11y-hidden">{translate('list_of_issues')}</h2>

{paging.total > 0 && (
{issues.length > 0 && (
<IssuesList
branchLike={branchLike}
checked={this.state.checked}
@@ -1120,13 +1145,14 @@ export class App extends React.PureComponent<Props, State> {
/>
)}

{paging.total > 0 && (
{issues.length > 0 && (
<ListFooter
count={issues.length}
loadMore={() => {
this.fetchMoreIssues().catch(() => undefined);
}}
loading={loadingMore}
pageSize={ISSUES_PAGE_SIZE}
total={paging.total}
useMIUIButtons
/>
@@ -1158,7 +1184,7 @@ export class App extends React.PureComponent<Props, State> {
<PageActions
canSetHome={!this.props.component}
effortTotal={this.state.effortTotal}
paging={paging}
paging={this.props.component?.needIssueSync ? undefined : paging}
selectedIndex={selectedIndex}
/>
</div>
@@ -1302,9 +1328,22 @@ export class App extends React.PureComponent<Props, State> {
}
}

export default withIndexationGuard(
withRouter(withComponentContext(withCurrentUserContext(withBranchLikes(App)))),
PageContext.Issues
export default withRouter(
withComponentContext(
withCurrentUserContext(
withBranchLikes(
withIndexationContext(
withIndexationGuard<Props & WithIndexationContextProps>({
Component: App,
showIndexationMessage: ({ component, indexationContext }) =>
(!component && indexationContext.status.isCompleted === false) ||
(component?.qualifier !== ComponentQualifier.Project &&
component?.needIssueSync === true),
})
)
)
)
)
);

const PageWrapperStyle = styled.div`

+ 1
- 2
server/sonar-web/src/main/js/components/hoc/__tests__/withIndexationGuard-test.tsx Просмотреть файл

@@ -20,7 +20,6 @@
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';

@@ -62,4 +61,4 @@ class TestComponent extends React.PureComponent {
}
}

const TestComponentWithGuard = withIndexationGuard(TestComponent, PageContext.Issues);
const TestComponentWithGuard = withIndexationGuard(TestComponent);

+ 15
- 21
server/sonar-web/src/main/js/components/hoc/withIndexationGuard.tsx Просмотреть файл

@@ -17,29 +17,23 @@
* 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';
import PageUnavailableDueToIndexation 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 && !context?.status.hasFailures ? (
<WrappedComponent {...this.props} />
) : (
<PageUnavailableDueToIndexation pageContext={pageContext} />
)
}
</IndexationContext.Consumer>
);
}
export default function withIndexationGuard<P>(WrappedComponent: React.ComponentType<P>) {
return function WithIndexationGuard(props: React.PropsWithChildren<P>) {
return (
<IndexationContext.Consumer>
{(context) =>
context?.status.isCompleted && !context?.status.hasFailures ? (
<WrappedComponent {...props} />
) : (
<PageUnavailableDueToIndexation />
)
}
</IndexationContext.Consumer>
);
};
}

+ 2
- 3
sonar-core/src/main/resources/org/sonar/l10n/core.properties Просмотреть файл

@@ -544,7 +544,6 @@ layout.security_reports=Security Reports
layout.nav.home_logo_alt=Logo, link to homepage
layout.must_be_configured=This will be available once your project is configured and analyzed.
layout.all_project_must_be_accessible=You need access to all projects within this {0} to access it.
layout.component_must_be_reindexed=This will be available once the reindexing has completed.

sidebar.projects=Projects
sidebar.project_settings=Configuration
@@ -4734,8 +4733,8 @@ indexation.admin_link=See {link} for more information.
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.
indexation.page_unavailable.description=SonarQube is reindexing project data.
indexation.page_unavailable.description.additional_information=This page is unavailable until this process is complete. {link}


#------------------------------------------------------------------------------

Загрузка…
Отмена
Сохранить