@@ -75,6 +75,8 @@ export function getFacet( | |||
export function searchIssueTags(data: { | |||
project?: string; | |||
branch?: string; | |||
all?: boolean; | |||
ps?: number; | |||
q?: string; | |||
}): Promise<string[]> { |
@@ -21,6 +21,7 @@ import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { getGlobalSettingValue, Store } from '../../../store/rootReducer'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { isBranch, isPullRequest } from '../../../helpers/branch-like'; | |||
import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../types/component'; | |||
import { | |||
Facet, | |||
@@ -105,7 +106,19 @@ export class Sidebar extends React.PureComponent<Props> { | |||
} | |||
render() { | |||
const { component, createdAfterIncludesTime, facets, openFacets, query } = this.props; | |||
const { | |||
component, | |||
createdAfterIncludesTime, | |||
facets, | |||
openFacets, | |||
query, | |||
branchLike | |||
} = this.props; | |||
const branch = | |||
(isBranch(branchLike) && branchLike.name) || | |||
(isPullRequest(branchLike) && branchLike.branch) || | |||
undefined; | |||
const displayProjectsFacet = | |||
!component || !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier); | |||
@@ -216,6 +229,7 @@ export class Sidebar extends React.PureComponent<Props> { | |||
/> | |||
<TagFacet | |||
component={component} | |||
branch={branch} | |||
fetching={this.props.loadingFacets.tags === true} | |||
loadSearchResultCount={this.props.loadSearchResultCount} | |||
onChange={this.props.onFilterChange} |
@@ -30,6 +30,7 @@ import { Query } from '../utils'; | |||
interface Props { | |||
component: T.Component | undefined; | |||
branch?: string; | |||
fetching: boolean; | |||
loadSearchResultCount: (property: string, changes: Partial<Query>) => Promise<Facet>; | |||
onChange: (changes: Partial<Query>) => void; | |||
@@ -44,11 +45,12 @@ const SEARCH_SIZE = 100; | |||
export default class TagFacet extends React.PureComponent<Props> { | |||
handleSearch = (query: string) => { | |||
const { component } = this.props; | |||
const { component, branch } = this.props; | |||
const project = | |||
component && ['TRK', 'VW', 'APP'].includes(component.qualifier) ? component.key : undefined; | |||
return searchIssueTags({ | |||
project, | |||
branch, | |||
ps: SEARCH_SIZE, | |||
q: query | |||
}).then(tags => ({ maxResults: tags.length === SEARCH_SIZE, results: tags })); |
@@ -50,6 +50,7 @@ export default class SetIssueTagsPopup extends React.PureComponent<Props, State> | |||
onSearch = (query: string) => { | |||
return searchIssueTags({ | |||
all: true, | |||
q: query, | |||
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, MAX_LIST_SIZE) | |||
}).then( |
@@ -516,9 +516,11 @@ public class IssueIndex { | |||
if (Boolean.TRUE.equals(query.onComponentOnly())) { | |||
return; | |||
} | |||
allFilters.addFilter( | |||
"__is_main_branch", new SimpleFieldFilterScope(FIELD_ISSUE_IS_MAIN_BRANCH), | |||
createTermFilter(FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(query.isMainBranch()))); | |||
if (query.isMainBranch() != null) { | |||
allFilters.addFilter( | |||
"__is_main_branch", new SimpleFieldFilterScope(FIELD_ISSUE_IS_MAIN_BRANCH), | |||
createTermFilter(FIELD_ISSUE_IS_MAIN_BRANCH, query.isMainBranch().toString())); | |||
} | |||
allFilters.addFilter( | |||
FIELD_ISSUE_BRANCH_UUID, new SimpleFieldFilterScope(FIELD_ISSUE_BRANCH_UUID), | |||
createTermFilter(FIELD_ISSUE_BRANCH_UUID, query.branchUuid())); |
@@ -96,7 +96,7 @@ public class IssueQuery { | |||
private final Boolean asc; | |||
private final String facetMode; | |||
private final String branchUuid; | |||
private final boolean mainBranch; | |||
private final Boolean mainBranch; | |||
private final ZoneId timeZone; | |||
private IssueQuery(Builder builder) { | |||
@@ -279,7 +279,7 @@ public class IssueQuery { | |||
return branchUuid; | |||
} | |||
public boolean isMainBranch() { | |||
public Boolean isMainBranch() { | |||
return mainBranch; | |||
} | |||
@@ -336,7 +336,7 @@ public class IssueQuery { | |||
private Boolean asc = false; | |||
private String facetMode; | |||
private String branchUuid; | |||
private boolean mainBranch = true; | |||
private Boolean mainBranch = true; | |||
private ZoneId timeZone; | |||
private Builder() { | |||
@@ -540,7 +540,7 @@ public class IssueQuery { | |||
return this; | |||
} | |||
public Builder mainBranch(boolean mainBranch) { | |||
public Builder mainBranch(@Nullable Boolean mainBranch) { | |||
this.mainBranch = mainBranch; | |||
return this; | |||
} |
@@ -276,6 +276,7 @@ public class IssueIndexFiltersTest { | |||
assertThatSearchReturnsOnly( | |||
IssueQuery.builder().componentUuids(singletonList(branch.uuid())).projectUuids(singletonList(project.uuid())).branchUuid(branch.uuid()).mainBranch(false), | |||
issueOnBranch.key()); | |||
assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())).mainBranch(null), issueOnProject.key(), issueOnBranch.key(), issueOnAnotherBranch.key()); | |||
assertThatSearchReturnsEmpty(IssueQuery.builder().branchUuid("unknown")); | |||
} | |||
@@ -19,10 +19,10 @@ | |||
*/ | |||
package org.sonar.server.issue.ws; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.google.common.io.Resources; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.resources.Qualifiers; | |||
import org.sonar.api.resources.Scopes; | |||
@@ -33,6 +33,7 @@ import org.sonar.api.server.ws.WebService; | |||
import org.sonar.api.server.ws.WebService.NewAction; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.BranchDto; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.server.component.ComponentFinder; | |||
import org.sonar.server.issue.index.IssueIndex; | |||
@@ -41,19 +42,22 @@ import org.sonar.server.issue.index.IssueQuery; | |||
import org.sonarqube.ws.Issues; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static java.util.Optional.ofNullable; | |||
import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE; | |||
import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY; | |||
import static org.sonar.server.issue.index.IssueQueryFactory.ISSUE_TYPE_NAMES; | |||
import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001; | |||
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; | |||
import static org.sonar.server.ws.WsUtils.writeProtobuf; | |||
/** | |||
* List issue tags matching a given query. | |||
* | |||
* @since 5.1 | |||
*/ | |||
public class TagsAction implements IssuesWsAction { | |||
private static final String PARAM_PROJECT = "project"; | |||
private static final String PARAM_BRANCH = "branch"; | |||
private static final String PARAM_ALL = "all"; | |||
private final IssueIndex issueIndex; | |||
private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker; | |||
@@ -61,8 +65,8 @@ public class TagsAction implements IssuesWsAction { | |||
private final ComponentFinder componentFinder; | |||
public TagsAction(IssueIndex issueIndex, | |||
IssueIndexSyncProgressChecker issueIndexSyncProgressChecker, DbClient dbClient, | |||
ComponentFinder componentFinder) { | |||
IssueIndexSyncProgressChecker issueIndexSyncProgressChecker, DbClient dbClient, | |||
ComponentFinder componentFinder) { | |||
this.issueIndex = issueIndex; | |||
this.issueIndexSyncProgressChecker = issueIndexSyncProgressChecker; | |||
this.dbClient = dbClient; | |||
@@ -84,15 +88,31 @@ public class TagsAction implements IssuesWsAction { | |||
.setRequired(false) | |||
.setExampleValue(KEY_PROJECT_EXAMPLE_001) | |||
.setSince("7.4"); | |||
action.createParam(PARAM_BRANCH) | |||
.setDescription("Branch key") | |||
.setRequired(false) | |||
.setExampleValue(KEY_BRANCH_EXAMPLE_001) | |||
.setSince("9.2"); | |||
action.createParam(PARAM_ALL) | |||
.setDescription("Indicator to search for all tags or only for tags in the main branch of a project") | |||
.setRequired(false) | |||
.setDefaultValue(Boolean.FALSE) | |||
.setPossibleValues(Boolean.TRUE, Boolean.FALSE) | |||
.setSince("9.2"); | |||
} | |||
@Override | |||
public void handle(Request request, Response response) throws Exception { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
String projectKey = request.param(PARAM_PROJECT); | |||
String branchKey = request.param(PARAM_BRANCH); | |||
boolean all = request.mandatoryParamAsBoolean(PARAM_ALL); | |||
checkIfAnyComponentsNeedIssueSync(dbSession, projectKey); | |||
Optional<ComponentDto> project = getProject(dbSession, projectKey); | |||
List<String> tags = searchTags(project.orElse(null), request); | |||
Optional<BranchDto> branch = project.flatMap(p -> dbClient.branchDao().selectByBranchKey(dbSession, p.uuid(), branchKey)); | |||
List<String> tags = searchTags(project.orElse(null), branch.orElse(null), request, all); | |||
Issues.TagsResponse.Builder tagsResponseBuilder = Issues.TagsResponse.newBuilder(); | |||
tags.forEach(tagsResponseBuilder::addTags); | |||
writeProtobuf(tagsResponseBuilder.build(), request, response); | |||
@@ -116,22 +136,31 @@ public class TagsAction implements IssuesWsAction { | |||
} | |||
} | |||
private List<String> searchTags(@Nullable ComponentDto project, Request request) { | |||
private List<String> searchTags(@Nullable ComponentDto project, @Nullable BranchDto branch, Request request, boolean all) { | |||
IssueQuery.Builder issueQueryBuilder = IssueQuery.builder() | |||
.types(ISSUE_TYPE_NAMES); | |||
ofNullable(project).ifPresent(p -> { | |||
switch (p.qualifier()) { | |||
if (project != null) { | |||
switch (project.qualifier()) { | |||
case Qualifiers.PROJECT: | |||
issueQueryBuilder.projectUuids(ImmutableSet.of(p.uuid())); | |||
return; | |||
issueQueryBuilder.projectUuids(Set.of(project.uuid())); | |||
break; | |||
case Qualifiers.APP: | |||
case Qualifiers.VIEW: | |||
issueQueryBuilder.viewUuids(ImmutableSet.of(p.uuid())); | |||
return; | |||
issueQueryBuilder.viewUuids(Set.of(project.uuid())); | |||
break; | |||
default: | |||
throw new IllegalArgumentException(String.format("Component of type '%s' is not supported", p.qualifier())); | |||
throw new IllegalArgumentException(String.format("Component of type '%s' is not supported", project.qualifier())); | |||
} | |||
}); | |||
if (branch != null && !project.uuid().equals(branch.getUuid())) { | |||
issueQueryBuilder.branchUuid(branch.getUuid()); | |||
issueQueryBuilder.mainBranch(false); | |||
} | |||
} | |||
if (all) { | |||
issueQueryBuilder.mainBranch(null); | |||
} | |||
return issueIndex.searchTags( | |||
issueQueryBuilder.build(), | |||
request.param(TEXT_QUERY), |
@@ -156,6 +156,66 @@ public class TagsActionTest { | |||
verify(issueIndexSyncProgressChecker).checkIfComponentNeedIssueSync(any(), eq(project1.getKey())); | |||
} | |||
@Test | |||
public void search_tags_by_branch_equals_main_branch() { | |||
RuleDefinitionDto rule = db.rules().insertIssueRule(); | |||
ComponentDto project = db.components().insertPrivateProject(); | |||
ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch")); | |||
db.issues().insertIssue(rule, project, project, issue -> issue.setTags(asList("tag1", "tag2"))); | |||
db.issues().insertIssue(rule, branch, branch, issue -> issue.setTags(asList("tag12", "tag4", "tag5"))); | |||
indexIssues(); | |||
permissionIndexer.allowOnlyAnyone(project, branch); | |||
assertThat(tagListOf(ws.newRequest() | |||
.setParam("project", project.getKey()) | |||
.setParam("branch", project.uuid()))).containsExactly("tag1", "tag2"); | |||
} | |||
@Test | |||
public void search_tags_by_branch() { | |||
RuleDefinitionDto rule = db.rules().insertIssueRule(); | |||
ComponentDto project = db.components().insertPrivateProject(); | |||
ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch")); | |||
db.issues().insertIssue(rule, project, project, issue -> issue.setTags(asList("tag1", "tag2"))); | |||
db.issues().insertIssue(rule, branch, branch, issue -> issue.setTags(asList("tag12", "tag4", "tag5"))); | |||
indexIssues(); | |||
permissionIndexer.allowOnlyAnyone(project, branch); | |||
assertThat(tagListOf(ws.newRequest() | |||
.setParam("project", project.getKey()) | |||
.setParam("branch", "my_branch"))).containsExactly("tag12", "tag4", "tag5"); | |||
} | |||
@Test | |||
public void search_tags_by_branch_not_exist_fall_back_to_main_branch() { | |||
RuleDefinitionDto rule = db.rules().insertIssueRule(); | |||
ComponentDto project = db.components().insertPrivateProject(); | |||
ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch")); | |||
db.issues().insertIssue(rule, project, project, issue -> issue.setTags(asList("tag1", "tag2"))); | |||
db.issues().insertIssue(rule, branch, branch, issue -> issue.setTags(asList("tag12", "tag4", "tag5"))); | |||
indexIssues(); | |||
permissionIndexer.allowOnlyAnyone(project, branch); | |||
assertThat(tagListOf(ws.newRequest() | |||
.setParam("project", project.getKey()) | |||
.setParam("branch", "not_exist"))).containsExactly("tag1", "tag2"); | |||
} | |||
@Test | |||
public void search_all_tags_by_query() { | |||
RuleDefinitionDto rule = db.rules().insertIssueRule(); | |||
ComponentDto project = db.components().insertPrivateProject(); | |||
ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch")); | |||
db.issues().insertIssue(rule, project, project, issue -> issue.setTags(asList("tag1", "tag2"))); | |||
db.issues().insertIssue(rule, branch, branch, issue -> issue.setTags(asList("tag12", "tag4", "tag5"))); | |||
indexIssues(); | |||
permissionIndexer.allowOnlyAnyone(project, branch); | |||
assertThat(tagListOf(ws.newRequest() | |||
.setParam("q", "tag1") | |||
.setParam("all", "true"))).containsExactly("tag1", "tag12"); | |||
} | |||
@Test | |||
public void search_tags_by_project_ignores_hotspots() { | |||
RuleDefinitionDto issueRule = db.rules().insertIssueRule(); | |||
@@ -319,7 +379,9 @@ public class TagsActionTest { | |||
.containsExactlyInAnyOrder( | |||
tuple("q", null, null, false, false), | |||
tuple("ps", "10", null, false, false), | |||
tuple("project", null, "7.4", false, false)); | |||
tuple("project", null, "7.4", false, false), | |||
tuple("branch", null, "9.2", false, false), | |||
tuple("all", "false", "9.2", false, false)); | |||
} | |||
private static ProtocolStringList tagListOf(TestRequest testRequest) { |