Browse Source

SONAR-10997 display the total effort on the issues page (#615)

tags/7.5
Stas Vilchik 5 years ago
parent
commit
d4fa9a5eec

+ 1
- 4
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java View File

@@ -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) {

+ 1
- 2
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java View File

@@ -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.

+ 2
- 2
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java View File

@@ -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);
}


+ 1
- 1
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFiltersTest.java View File

@@ -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);
}
}

+ 17
- 0
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java View File

@@ -926,6 +926,23 @@ public class SearchActionTest {
.containsExactly("82fd47d4-b650-4037-80bc-7b112bd4eac3", "82fd47d4-b650-4037-80bc-7b112bd4eac1", "82fd47d4-b650-4037-80bc-7b112bd4eac2");
}

@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();

+ 2
- 2
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java View File

@@ -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();

}


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

@@ -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 }[];

+ 5
- 1
server/sonar-web/src/main/js/apps/issues/components/App.tsx View File

@@ -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}

+ 4
- 1
server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx View File

@@ -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 && (

+ 37
- 0
server/sonar-web/src/main/js/apps/issues/components/TotalEffort.tsx View File

@@ -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>
);
}

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

@@ -49,6 +49,7 @@ const PROPS = {
fetchIssues: () =>
Promise.resolve({
components: [],
effortTotal: 1,
facets,
issues,
languages: [],

+ 36
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx View File

@@ -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();
});

+ 26
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx View File

@@ -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();
});

+ 63
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap View File

@@ -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>
`;

+ 23
- 0
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap View File

@@ -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>
`;

Loading…
Cancel
Save