aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-08-14 16:43:17 +0200
committerSonarTech <sonartech@sonarsource.com>2018-08-21 20:21:02 +0200
commitd4fa9a5eec91b3cc36a7ac6b0b1c0ff3359162d9 (patch)
tree20aefaf5d526b5a1dda744cf9297605d6c04beec
parent63055cd49b4053c4f99949f505f6ce1214cb4135 (diff)
downloadsonarqube-d4fa9a5eec91b3cc36a7ac6b0b1c0ff3359162d9.tar.gz
sonarqube-d4fa9a5eec91b3cc36a7ac6b0b1c0ff3359162d9.zip
SONAR-10997 display the total effort on the issues page (#615)
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java5
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java3
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java4
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java2
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java17
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java4
-rw-r--r--server/sonar-web/src/main/js/api/issues.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx36
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap63
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap23
15 files changed, 220 insertions, 14 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
index d67041d3e4a..4f9c257b2e5 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
@@ -298,6 +298,7 @@ public class IssueIndex {
}
configureStickyFacets(query, options, filters, esQuery, requestBuilder);
+ requestBuilder.addAggregation(EFFORT_AGGREGATION);
requestBuilder.setFetchSource(false);
return requestBuilder.get();
}
@@ -588,10 +589,6 @@ public class IssueIndex {
}
addAssignedToMeFacetIfNeeded(esSearch, options, query, filters, esQuery);
}
-
- if (hasQueryEffortFacet(query)) {
- esSearch.addAggregation(EFFORT_AGGREGATION);
- }
}
private Optional<AggregationBuilder> getCreatedAtFacet(IssueQuery query, Map<String, QueryBuilder> filters, QueryBuilder esQuery) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
index eb2451e417b..41407f3a578 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
@@ -443,9 +443,8 @@ public class SearchAction implements IssuesWsAction {
SearchResponseLoader.Collector collector = new SearchResponseLoader.Collector(additionalFields, issueKeys);
collectLoggedInUser(collector);
collectRequestParams(collector, request);
- Facets facets = null;
+ Facets facets = new Facets(result, system2.getDefaultTimeZone());
if (!options.getFacets().isEmpty()) {
- facets = new Facets(result, system2.getDefaultTimeZone());
// add missing values to facets. For example if assignee "john" and facet on "assignees" are requested, then
// "john" should always be listed in the facet. If it is not present, then it is added with value zero.
// This is a constraint from webapp UX.
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java
index 8f519276406..38599230331 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java
@@ -506,7 +506,7 @@ public class IssueIndexFacetsTest {
private final void assertThatFacetHasExactly(IssueQuery.Builder query, String facet, Map.Entry<String, Long>... expectedEntries) {
SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet)));
Facets facets = new Facets(result, system2.getDefaultTimeZone());
- assertThat(facets.getNames()).containsOnly(facet);
+ assertThat(facets.getNames()).containsOnly(facet, "effort");
assertThat(facets.get(facet)).containsExactly(expectedEntries);
}
@@ -514,7 +514,7 @@ public class IssueIndexFacetsTest {
private final void assertThatFacetHasOnly(IssueQuery.Builder query, String facet, Map.Entry<String, Long>... expectedEntries) {
SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet)));
Facets facets = new Facets(result, system2.getDefaultTimeZone());
- assertThat(facets.getNames()).containsOnly(facet);
+ assertThat(facets.getNames()).containsOnly(facet, "effort");
assertThat(facets.get(facet)).containsOnly(expectedEntries);
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
index 3d24f2455f6..cb82e78e615 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java
@@ -794,7 +794,7 @@ public class IssueIndexFiltersTest {
private final void assertThatFacetHasOnly(IssueQuery.Builder query, String facet, Map.Entry<String, Long>... expectedEntries) {
SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet)));
Facets facets = new Facets(result, system2.getDefaultTimeZone());
- assertThat(facets.getNames()).containsOnly(facet);
+ assertThat(facets.getNames()).containsOnly(facet, "effort");
assertThat(facets.get(facet)).containsOnly(expectedEntries);
}
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
index 83292019f1b..2e96699a68b 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
@@ -927,6 +927,23 @@ public class SearchActionTest {
}
@Test
+ public void return_total_effort() {
+ UserDto john = db.users().insertUser();
+ userSession.logIn(john);
+ RuleDefinitionDto rule = db.rules().insert();
+ ComponentDto project = db.components().insertPublicProject();
+ ComponentDto file = db.components().insertComponent(newFileDto(project));
+ IssueDto issue1 = db.issues().insert(rule, project, file, i -> i.setEffort(10L));
+ IssueDto issue2 = db.issues().insert(rule, project, file, i -> i.setEffort(15L));
+ indexPermissions();
+ indexIssues();
+
+ Issues.SearchWsResponse response = ws.newRequest().executeProtobuf(Issues.SearchWsResponse.class);
+
+ assertThat(response.getEffortTotal()).isEqualTo(25L);
+ }
+
+ @Test
public void paging() {
RuleDto rule = newRule();
ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization1, "PROJECT_ID").setDbKey("PROJECT_KEY"));
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java
index 5c38990c443..07e57dfd3f3 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java
@@ -119,7 +119,7 @@ public class SearchActionTestOnSonarCloud {
JsonAssert.assertJson(input).isSimilarTo(this.getClass().getResource(this.getClass().getSimpleName() + "/no_authors_facet.json"));
JsonElement gson = new JsonParser().parse(input);
- assertThat(gson.getAsJsonObject().get("facets")).isNull();
+ assertThat(gson.getAsJsonObject().get("facets").getAsJsonArray()).isEmpty();
}
@Test
@@ -136,7 +136,7 @@ public class SearchActionTestOnSonarCloud {
JsonAssert.assertJson(input).isSimilarTo(this.getClass().getResource(this.getClass().getSimpleName() + "/no_author_and_no_authors_facet.json"));
JsonElement gson = new JsonParser().parse(input);
- assertThat(gson.getAsJsonObject().get("facets")).isNull();
+ assertThat(gson.getAsJsonObject().get("facets").getAsJsonArray()).isEmpty();
}
diff --git a/server/sonar-web/src/main/js/api/issues.ts b/server/sonar-web/src/main/js/api/issues.ts
index b49a6eddba9..202190af502 100644
--- a/server/sonar-web/src/main/js/api/issues.ts
+++ b/server/sonar-web/src/main/js/api/issues.ts
@@ -31,7 +31,7 @@ export interface IssueResponse {
interface IssuesResponse {
components?: { key: string; organization: string; name: string; uuid: string }[];
- debtTotal?: number;
+ effortTotal: number;
facets: Array<{
property: string;
values: { count: number; val: string }[];
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.tsx b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
index 067668fbd01..3d26c6b39fc 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.tsx
@@ -87,6 +87,7 @@ import DeferredSpinner from '../../../components/common/DeferredSpinner';
interface FetchIssuesPromise {
components: ReferencedComponent[];
+ effortTotal: number;
facets: RawFacet[];
issues: Issue[];
languages: ReferencedLanguage[];
@@ -111,6 +112,7 @@ interface Props {
export interface State {
bulkChange?: 'all' | 'selected';
checked: string[];
+ effortTotal?: number;
facets: { [facet: string]: Facet };
issues: Issue[];
lastChecked?: string;
@@ -461,7 +463,7 @@ export default class App extends React.PureComponent<Props, State> {
const prevQuery = this.props.location.query;
this.setState({ checked: [], loading: true });
return this.fetchIssues({}, true).then(
- ({ facets, issues, paging, ...other }) => {
+ ({ effortTotal, facets, issues, paging, ...other }) => {
if (this.mounted && areQueriesEqual(prevQuery, this.props.location.query)) {
const openIssue = this.getOpenIssue(this.props, issues);
let selected: string | undefined = undefined;
@@ -469,6 +471,7 @@ export default class App extends React.PureComponent<Props, State> {
selected = openIssue ? openIssue.key : issues[0].key;
}
this.setState(state => ({
+ effortTotal,
facets: { ...state.facets, ...parseFacets(facets) },
loading: false,
issues,
@@ -1131,6 +1134,7 @@ export default class App extends React.PureComponent<Props, State> {
!this.props.component &&
(!isSonarCloud() || this.props.myIssues)
)}
+ effortTotal={this.state.effortTotal}
onReload={this.handleReload}
paging={paging}
selectedIndex={selectedIndex}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx
index 1850b50dbda..31eb0f06472 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx
@@ -19,6 +19,7 @@
*/
import * as React from 'react';
import IssuesCounter from './IssuesCounter';
+import TotalEffort from './TotalEffort';
import { HomePageType, Paging } from '../../../app/types';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import ReloadButton from '../../../components/controls/ReloadButton';
@@ -27,6 +28,7 @@ import { isSonarCloud } from '../../../helpers/system';
interface Props {
canSetHome: boolean;
+ effortTotal: number | undefined;
onReload: () => void;
paging: Paging | undefined;
selectedIndex: number | undefined;
@@ -52,7 +54,7 @@ export default class PageActions extends React.PureComponent<Props> {
}
render() {
- const { paging, selectedIndex } = this.props;
+ const { effortTotal, paging, selectedIndex } = this.props;
return (
<div className="pull-right">
@@ -63,6 +65,7 @@ export default class PageActions extends React.PureComponent<Props> {
{paging != null && (
<IssuesCounter className="spacer-left" current={selectedIndex} total={paging.total} />
)}
+ {effortTotal !== undefined && <TotalEffort effort={effortTotal} />}
</div>
{this.props.canSetHome && (
diff --git a/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx b/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx
new file mode 100644
index 00000000000..c6f4eed009b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { translate } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
+
+export default function TotalEffort({ effort }: { effort: number }) {
+ return (
+ <div className="display-inline-block bordered-left spacer-left">
+ <div className="spacer-left">
+ <FormattedMessage
+ defaultMessage={translate('issue.x_effort')}
+ id="issue.x_effort"
+ values={{ 0: <strong>{formatMeasure(effort, 'WORK_DUR')}</strong> }}
+ />
+ </div>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
index bd16df2c7d1..2108d640fda 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/App-test.tsx
@@ -49,6 +49,7 @@ const PROPS = {
fetchIssues: () =>
Promise.resolve({
components: [],
+ effortTotal: 1,
facets,
issues,
languages: [],
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx
new file mode 100644
index 00000000000..581ef9add8a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import PageActions from '../PageActions';
+
+it('should render', () => {
+ expect(
+ shallow(
+ <PageActions
+ canSetHome={true}
+ effortTotal={125}
+ onReload={jest.fn()}
+ paging={{ pageIndex: 1, pageSize: 100, total: 12345 }}
+ selectedIndex={5}
+ />
+ )
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx
new file mode 100644
index 00000000000..2b4f8135784
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import TotalEffort from '../TotalEffort';
+
+it('should render', () => {
+ expect(shallow(<TotalEffort effort={125} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap
new file mode 100644
index 00000000000..446f460ac6d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap
@@ -0,0 +1,63 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="pull-right"
+>
+ <span
+ className="note big-spacer-right"
+ >
+ <span
+ className="big-spacer-right"
+ >
+ <span
+ className="shortcut-button little-spacer-right"
+ >
+ ↑
+ </span>
+ <span
+ className="shortcut-button little-spacer-right"
+ >
+ ↓
+ </span>
+ issues.to_select_issues
+ </span>
+ <span>
+ <span
+ className="shortcut-button little-spacer-right"
+ >
+ ←
+ </span>
+ <span
+ className="shortcut-button little-spacer-right"
+ >
+ →
+ </span>
+ issues.to_navigate
+ </span>
+ </span>
+ <div
+ className="issues-page-actions"
+ >
+ <ReloadButton
+ onClick={[MockFunction]}
+ />
+ <IssuesCounter
+ className="spacer-left"
+ current={5}
+ total={12345}
+ />
+ <TotalEffort
+ effort={125}
+ />
+ </div>
+ <Connect(HomePageSelect)
+ className="huge-spacer-left"
+ currentPage={
+ Object {
+ "type": "ISSUES",
+ }
+ }
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap
new file mode 100644
index 00000000000..791cfe582c2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="display-inline-block bordered-left spacer-left"
+>
+ <div
+ className="spacer-left"
+ >
+ <FormattedMessage
+ defaultMessage="issue.x_effort"
+ id="issue.x_effort"
+ values={
+ Object {
+ "0": <strong>
+ work_duration.x_hours.2 work_duration.x_minutes.5
+ </strong>,
+ }
+ }
+ />
+ </div>
+</div>
+`;