Browse Source

SONAR-19728 Unlock project issues page while reindexing issues

tags/10.2.0.77647
Ambroise C 11 months ago
parent
commit
a451c69c4f

+ 5
- 1
server/sonar-web/src/main/js/api/issues.ts View File

@@ -28,7 +28,7 @@ import {
postJSON,
RequestData,
} from '../helpers/request';
import { IssueResponse, RawIssuesResponse } from '../types/issues';
import { IssueResponse, ListIssuesResponse, RawIssuesResponse } from '../types/issues';
import { Dict, FacetValue, IssueChangelog, SnippetsByComponent, SourceLine } from '../types/types';

type FacetName =
@@ -55,6 +55,10 @@ export function searchIssues(query: RequestData): Promise<RawIssuesResponse> {
return getJSON('/api/issues/search', query).catch(throwGlobalError);
}

export function listIssues(query: RequestData): Promise<ListIssuesResponse> {
return getJSON('/api/issues/list', query).catch(throwGlobalError);
}

export function getFacets(
query: RequestData,
facets: FacetName[]

+ 29
- 0
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts View File

@@ -32,6 +32,7 @@ import {
IssueStatus,
IssueTransition,
IssueType,
ListIssuesResponse,
RawFacet,
RawIssue,
RawIssuesResponse,
@@ -49,6 +50,7 @@ import {
editIssueComment,
getIssueChangelog,
getIssueFlowSnippets,
listIssues,
searchIssueAuthors,
searchIssueTags,
searchIssues,
@@ -116,6 +118,7 @@ export default class IssuesServiceMock {
jest.mocked(getIssueChangelog).mockImplementation(this.handleGetIssueChangelog);
jest.mocked(getIssueFlowSnippets).mockImplementation(this.handleGetIssueFlowSnippets);
jest.mocked(getRuleDetails).mockImplementation(this.handleGetRuleDetails);
jest.mocked(listIssues).mockImplementation(this.handleListIssues);
jest.mocked(searchIssueAuthors).mockImplementation(this.handleSearchIssueAuthors);
jest.mocked(searchIssues).mockImplementation(this.handleSearchIssues);
jest.mocked(searchIssueTags).mockImplementation(this.handleSearchIssueTags);
@@ -379,6 +382,32 @@ export default class IssuesServiceMock {
});
};

handleListIssues = (query: RequestData): Promise<ListIssuesResponse> => {
const filteredList = this.list
.filter((item) => !query.types || query.types.split(',').includes(item.issue.type))
.filter(
(item) =>
!query.inNewCodePeriod || new Date(item.issue.creationDate) > new Date('2023-01-10')
);

// Splice list items according to paging using a fixed page size
const pageIndex = query.p || 1;
const pageSize = 7;
const listItems = filteredList.slice((pageIndex - 1) * pageSize, pageIndex * pageSize);

// Generate response
return this.reply({
components: generateReferenceComponentsForIssues(filteredList),
issues: listItems.map((line) => line.issue),
paging: mockPaging({
pageIndex,
pageSize,
total: filteredList.length,
}),
rules: this.rulesList,
});
};

handleSearchIssues = (query: RequestData): Promise<RawIssuesResponse> => {
const facets = this.mockFacetDetailResponse(query);


+ 27
- 0
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx View File

@@ -957,3 +957,30 @@ describe('Activity', () => {
).toBeInTheDocument();
});
});

describe('issues app when reindexing', () => {
it('should display only some facets while reindexing is in progress', async () => {
issuesHandler.setIsAdmin(true);
renderProjectIssuesApp(undefined, { needIssueSync: true });

// Enabled facets
expect(ui.inNewCodeFilter.get()).toBeInTheDocument();
expect(ui.typeFacet.get()).toBeInTheDocument();

// Disabled facets
expect(await ui.assigneeFacet.query()).not.toBeInTheDocument();
expect(await ui.authorFacet.query()).not.toBeInTheDocument();
expect(await ui.codeVariantsFacet.query()).not.toBeInTheDocument();
expect(await ui.creationDateFacet.query()).not.toBeInTheDocument();
expect(await ui.languageFacet.query()).not.toBeInTheDocument();
expect(await ui.projectFacet.query()).not.toBeInTheDocument();
expect(await ui.resolutionFacet.query()).not.toBeInTheDocument();
expect(await ui.ruleFacet.query()).not.toBeInTheDocument();
expect(await ui.scopeFacet.query()).not.toBeInTheDocument();
expect(await ui.statusFacet.query()).not.toBeInTheDocument();
expect(await ui.tagFacet.query()).not.toBeInTheDocument();

// Indexation message
expect(screen.getByText(/indexation\.filters_unavailable/)).toBeInTheDocument();
});
});

+ 187
- 153
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx View File

@@ -18,10 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { BasicSeparator } from 'design-system';
import { BasicSeparator, FlagMessage, Link } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import { isBranch, isPullRequest } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { AppState } from '../../../types/appstate';
import { BranchLike } from '../../../types/branch-like';
import {
@@ -167,6 +169,8 @@ export class SidebarClass extends React.PureComponent<Props> {
const displayPeriodFilter = component !== undefined && !isPortfolioLike(component.qualifier);
const displayProjectsFacet = !component || isView(component.qualifier);

const needIssueSync = component?.needIssueSync;

return (
<>
{displayPeriodFilter && (
@@ -178,6 +182,7 @@ export class SidebarClass extends React.PureComponent<Props> {

<TypeFacet
fetching={this.props.loadingFacets.types === true}
needIssueSync={needIssueSync}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.types}
@@ -185,191 +190,220 @@ export class SidebarClass extends React.PureComponent<Props> {
types={query.types}
/>

<BasicSeparator className="sw-my-4" />

<SeverityFacet
fetching={this.props.loadingFacets.severities === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.severities}
severities={query.severities}
stats={facets.severities}
/>

<BasicSeparator className="sw-my-4" />

<ScopeFacet
fetching={this.props.loadingFacets.scopes === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.scopes}
stats={facets.scopes}
scopes={query.scopes}
/>

<BasicSeparator className="sw-my-4" />

<ResolutionFacet
fetching={this.props.loadingFacets.resolutions === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.resolutions}
resolutions={query.resolutions}
resolved={query.resolved}
stats={facets.resolutions}
/>

<BasicSeparator className="sw-my-4" />
{!needIssueSync && (
<>
<BasicSeparator className="sw-my-4" />

<StatusFacet
fetching={this.props.loadingFacets.statuses === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.statuses}
stats={facets.statuses}
statuses={query.statuses}
/>
<SeverityFacet
fetching={this.props.loadingFacets.severities === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.severities}
severities={query.severities}
stats={facets.severities}
/>

<BasicSeparator className="sw-my-4" />
<BasicSeparator className="sw-my-4" />

<StandardFacet
cwe={query.cwe}
cweOpen={!!openFacets.cwe}
cweStats={facets.cwe}
fetchingCwe={this.props.loadingFacets.cwe === true}
fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true}
fetchingOwaspTop10-2021={this.props.loadingFacets['owaspTop10-2021'] === true}
fetchingSonarSourceSecurity={this.props.loadingFacets.sonarsourceSecurity === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.standards}
owaspTop10={query.owaspTop10}
owaspTop10Open={!!openFacets.owaspTop10}
owaspTop10Stats={facets.owaspTop10}
owaspTop10-2021={query['owaspTop10-2021']}
owaspTop10-2021Open={!!openFacets['owaspTop10-2021']}
owaspTop10-2021Stats={facets['owaspTop10-2021']}
query={query}
sonarsourceSecurity={query.sonarsourceSecurity}
sonarsourceSecurityOpen={!!openFacets.sonarsourceSecurity}
sonarsourceSecurityStats={facets.sonarsourceSecurity}
/>
<ScopeFacet
fetching={this.props.loadingFacets.scopes === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.scopes}
stats={facets.scopes}
scopes={query.scopes}
/>

<BasicSeparator className="sw-my-4" />
<BasicSeparator className="sw-my-4" />

<CreationDateFacet
component={component}
createdAfter={query.createdAfter}
createdAfterIncludesTime={createdAfterIncludesTime}
createdAt={query.createdAt}
createdBefore={query.createdBefore}
createdInLast={query.createdInLast}
fetching={this.props.loadingFacets.createdAt === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.createdAt}
inNewCodePeriod={query.inNewCodePeriod}
stats={facets.createdAt}
/>
<ResolutionFacet
fetching={this.props.loadingFacets.resolutions === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.resolutions}
resolutions={query.resolutions}
resolved={query.resolved}
stats={facets.resolutions}
/>

<BasicSeparator className="sw-my-4" />
<BasicSeparator className="sw-my-4" />

<LanguageFacet
fetching={this.props.loadingFacets.languages === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.languages}
query={query}
referencedLanguages={this.props.referencedLanguages}
selectedLanguages={query.languages}
stats={facets.languages}
/>
<StatusFacet
fetching={this.props.loadingFacets.statuses === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.statuses}
stats={facets.statuses}
statuses={query.statuses}
/>

<BasicSeparator className="sw-my-4" />
<BasicSeparator className="sw-my-4" />

<RuleFacet
fetching={this.props.loadingFacets.rules === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.rules}
query={query}
referencedRules={this.props.referencedRules}
stats={facets.rules}
/>
<StandardFacet
cwe={query.cwe}
cweOpen={!!openFacets.cwe}
cweStats={facets.cwe}
fetchingCwe={this.props.loadingFacets.cwe === true}
fetchingOwaspTop10={this.props.loadingFacets.owaspTop10 === true}
fetchingOwaspTop10-2021={this.props.loadingFacets['owaspTop10-2021'] === true}
fetchingSonarSourceSecurity={this.props.loadingFacets.sonarsourceSecurity === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.standards}
owaspTop10={query.owaspTop10}
owaspTop10Open={!!openFacets.owaspTop10}
owaspTop10Stats={facets.owaspTop10}
owaspTop10-2021={query['owaspTop10-2021']}
owaspTop10-2021Open={!!openFacets['owaspTop10-2021']}
owaspTop10-2021Stats={facets['owaspTop10-2021']}
query={query}
sonarsourceSecurity={query.sonarsourceSecurity}
sonarsourceSecurityOpen={!!openFacets.sonarsourceSecurity}
sonarsourceSecurityStats={facets.sonarsourceSecurity}
/>

<BasicSeparator className="sw-my-4" />
<BasicSeparator className="sw-my-4" />

<TagFacet
component={component}
branch={branch}
fetching={this.props.loadingFacets.tags === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.tags}
query={query}
stats={facets.tags}
tags={query.tags}
/>
<CreationDateFacet
component={component}
createdAfter={query.createdAfter}
createdAfterIncludesTime={createdAfterIncludesTime}
createdAt={query.createdAt}
createdBefore={query.createdBefore}
createdInLast={query.createdInLast}
fetching={this.props.loadingFacets.createdAt === true}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.createdAt}
inNewCodePeriod={query.inNewCodePeriod}
stats={facets.createdAt}
/>

{displayProjectsFacet && (
<>
<BasicSeparator className="sw-my-4" />

<ProjectFacet
component={component}
fetching={this.props.loadingFacets.projects === true}
<LanguageFacet
fetching={this.props.loadingFacets.languages === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.projects}
projects={query.projects}
open={!!openFacets.languages}
query={query}
referencedComponents={this.props.referencedComponentsByKey}
stats={facets.projects}
referencedLanguages={this.props.referencedLanguages}
selectedLanguages={query.languages}
stats={facets.languages}
/>
</>
)}

{this.renderComponentFacets()}

{!this.props.myIssues && !disableDeveloperAggregatedInfo && (
<>
<BasicSeparator className="sw-my-4" />

<AssigneeFacet
assigned={query.assigned}
assignees={query.assignees}
fetching={this.props.loadingFacets.assignees === true}
<RuleFacet
fetching={this.props.loadingFacets.rules === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.assignees}
open={!!openFacets.rules}
query={query}
referencedUsers={this.props.referencedUsers}
stats={facets.assignees}
referencedRules={this.props.referencedRules}
stats={facets.rules}
/>

{!disableDeveloperAggregatedInfo && (
<>
<BasicSeparator className="sw-my-4" />

<TagFacet
component={component}
branch={branch}
fetching={this.props.loadingFacets.tags === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.tags}
query={query}
stats={facets.tags}
tags={query.tags}
/>

{displayProjectsFacet && (
<>
<BasicSeparator className="sw-my-4" />

<ProjectFacet
component={component}
fetching={this.props.loadingFacets.projects === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.projects}
projects={query.projects}
query={query}
referencedComponents={this.props.referencedComponentsByKey}
stats={facets.projects}
/>
</>
)}

{this.renderComponentFacets()}

{!this.props.myIssues && (
<>
<BasicSeparator className="sw-my-4" />

<AssigneeFacet
assigned={query.assigned}
assignees={query.assignees}
fetching={this.props.loadingFacets.assignees === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.assignees}
query={query}
referencedUsers={this.props.referencedUsers}
stats={facets.assignees}
/>
</>
)}

<BasicSeparator className="sw-my-4" />

<AuthorFacet
author={query.author}
component={component}
fetching={this.props.loadingFacets.author === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.author}
query={query}
stats={facets.author}
/>
</>
)}
</>
)}

{!disableDeveloperAggregatedInfo && (
{needIssueSync && (
<>
<BasicSeparator className="sw-my-4" />

<AuthorFacet
author={query.author}
component={component}
fetching={this.props.loadingFacets.author === true}
loadSearchResultCount={this.props.loadSearchResultCount}
onChange={this.props.onFilterChange}
onToggle={this.props.onFacetToggle}
open={!!openFacets.author}
query={query}
stats={facets.author}
/>
<FlagMessage className="sw-my-6" variant="info">
<div>
{translate('indexation.page_unavailable.description')}
<span className="sw-ml-1">
<FormattedMessage
defaultMessage={translate('indexation.filters_unavailable')}
id="indexation.filters_unavailable"
values={{
link: (
<Link to="https://docs.sonarqube.org/latest/instance-administration/reindexing/">
{translate('learn_more')}
</Link>
),
}}
/>
</span>
</div>
</FlagMessage>
</>
)}
</>

+ 3
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx View File

@@ -30,6 +30,7 @@ import { MultipleSelectionHint } from './MultipleSelectionHint';

interface Props {
fetching: boolean;
needIssueSync?: boolean;
onChange: (changes: Partial<Query>) => void;
onToggle: (property: string) => void;
open: boolean;
@@ -79,6 +80,7 @@ export class TypeFacet extends React.PureComponent<Props> {
}

renderItem = (type: string) => {
const { needIssueSync } = this.props;
const active = this.isFacetItemActive(type);
const stat = this.getStat(type);

@@ -94,7 +96,7 @@ export class TypeFacet extends React.PureComponent<Props> {
key={type}
name={translate('issue.type', type)}
onClick={this.handleItemClick}
stat={formatFacetStat(stat) ?? 0}
stat={(!needIssueSync && formatFacetStat(stat)) ?? 0}
value={type}
/>
);

+ 1
- 0
server/sonar-web/src/main/js/apps/issues/test-utils.tsx View File

@@ -67,6 +67,7 @@ export const ui = {
scopeFacet: byRole('button', { name: 'issues.facet.scopes' }),
statusFacet: byRole('button', { name: 'issues.facet.statuses' }),
tagFacet: byRole('button', { name: 'issues.facet.tags' }),
typeFacet: byRole('button', { name: 'issues.facet.types' }),

clearAssigneeFacet: byTestId('clear-issues.facet.assignees'),
clearAuthorFacet: byTestId('clear-issues.facet.authors'),

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/utils.ts View File

@@ -169,7 +169,7 @@ export function serializeQuery(query: Query): RawQuery {
export const areQueriesEqual = (a: RawQuery, b: RawQuery) =>
queriesEqual(parseQuery(a), parseQuery(b));

export function parseFacets(facets: RawFacet[]): Dict<Facet> {
export function parseFacets(facets?: RawFacet[]): Dict<Facet> {
if (!facets) {
return {};
}

+ 5
- 2
server/sonar-web/src/main/js/helpers/branch-like.ts View File

@@ -113,8 +113,11 @@ export function getBrancheLikesAsTree(branchLikes: BranchLike[]): BranchLikeTree
}
}

export function getBranchLikeQuery(branchLike?: BranchLike): BranchParameters {
if (isBranch(branchLike) && !isMainBranch(branchLike)) {
export function getBranchLikeQuery(
branchLike?: BranchLike,
includeMainBranch = false
): BranchParameters {
if (isBranch(branchLike) && (includeMainBranch || !isMainBranch(branchLike))) {
return { branch: branchLike.name };
} else if (isPullRequest(branchLike)) {
return { pullRequest: branchLike.key };

+ 18
- 5
server/sonar-web/src/main/js/types/issues.ts View File

@@ -153,15 +153,28 @@ export interface RawIssuesResponse {
users?: UserBase[];
}

export interface FetchIssuesPromise {
export interface ListIssuesResponse {
components: ReferencedComponent[];
effortTotal: number;
facets: RawFacet[];
issues: RawIssue[];
paging: Paging;
rules?: Array<{}>;
}

export interface FetchIssuesPromise {
components?: ReferencedComponent[];
effortTotal?: number;
facets?: RawFacet[];
issues: Issue[];
languages?: ReferencedLanguage[];
paging: Paging;
rules: ReferencedRule[];
users?: UserBase[];
}

export interface ListIssuesPromise {
issues: Issue[];
languages: ReferencedLanguage[];
paging: Paging;
rules: ReferencedRule[];
users: UserBase[];
}

export interface ReferencedComponent {

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -4741,6 +4741,8 @@ indexation.page_unavailable.description.additional_information=This page is unav
indexation.filter_unavailable.description=This filter is unavailable until this process is complete.
indexation.learn_more=Learn more:
indexation.reindexing=Reindexing
indexation.filters_unavailable=Some filters are unavailable until this process is complete. {link}


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

Loading…
Cancel
Save