diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-08-14 16:43:17 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-08-21 20:21:02 +0200 |
commit | d4fa9a5eec91b3cc36a7ac6b0b1c0ff3359162d9 (patch) | |
tree | 20aefaf5d526b5a1dda744cf9297605d6c04beec | |
parent | 63055cd49b4053c4f99949f505f6ce1214cb4135 (diff) | |
download | sonarqube-d4fa9a5eec91b3cc36a7ac6b0b1c0ff3359162d9.tar.gz sonarqube-d4fa9a5eec91b3cc36a7ac6b0b1c0ff3359162d9.zip |
SONAR-10997 display the total effort on the issues page (#615)
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> +`; |