@@ -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) { |
@@ -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. |
@@ -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); | |||
} | |||
@@ -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); | |||
} | |||
} |
@@ -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(); |
@@ -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(); | |||
} | |||
@@ -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 }[]; |
@@ -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} |
@@ -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 && ( |
@@ -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> | |||
); | |||
} |
@@ -49,6 +49,7 @@ const PROPS = { | |||
fetchIssues: () => | |||
Promise.resolve({ | |||
components: [], | |||
effortTotal: 1, | |||
facets, | |||
issues, | |||
languages: [], |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -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> | |||
`; |