From d66f14e860236d0f278b26b67875432cf3ccdbc0 Mon Sep 17 00:00:00 2001 From: Revanshu Paliwal Date: Wed, 26 Jan 2022 17:59:46 +0100 Subject: [PATCH] SONAR-15857 Measure page should support ascending and descending sorting for rating and quality gate --- .../__tests__/utils-test.ts | 7 +- .../component-measures/components/App.tsx | 1 + .../components/MeasureContent.tsx | 47 +- .../__tests__/MeasureContent-test.tsx | 93 +++ .../MeasureContent-test.tsx.snap | 587 ++++++++++++++++++ .../__snapshots__/ComponentCell-test.tsx.snap | 7 + .../main/js/apps/component-measures/utils.ts | 11 +- .../__snapshots__/DrilldownLink-test.tsx.snap | 1 + .../main/js/helpers/__tests__/urls-test.ts | 21 + server/sonar-web/src/main/js/helpers/urls.ts | 5 +- 10 files changed, 758 insertions(+), 22 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts index 98a7ce7ad59..fe29210691c 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts @@ -124,10 +124,13 @@ describe('parseQuery', () => { selected: '', view: utils.DEFAULT_VIEW }); - expect(utils.parseQuery({ metric: 'foo', selected: 'bar', view: 'tree' })).toEqual({ + expect( + utils.parseQuery({ metric: 'foo', selected: 'bar', view: 'tree', asc: 'false' }) + ).toEqual({ metric: 'foo', selected: 'bar', - view: 'tree' + view: 'tree', + asc: false }); }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx index 3c5cb08289e..4078382e7f9 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.tsx @@ -284,6 +284,7 @@ export class App extends React.PureComponent { rootComponent={component} router={this.props.router} selected={query.selected} + asc={query.asc} updateQuery={this.updateQuery} view={query.view} /> diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx index 11c50d3ddb2..60b16b58b1f 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx @@ -64,6 +64,7 @@ interface Props { rootComponent: ComponentMeasure; router: InjectedRouter; selected?: string; + asc?: boolean; updateQuery: (query: Partial) => void; view: MeasurePageView; } @@ -110,13 +111,14 @@ export default class MeasureContent extends React.PureComponent { } fetchComponentTree = () => { - const { metricKeys, opts, strategy } = this.getComponentRequestParams( - this.props.view, - this.props.requestedMetric - ); - const componentKey = this.props.selected || this.props.rootComponent.key; - const baseComponentMetrics = [this.props.requestedMetric.key]; - if (this.props.requestedMetric.key === MetricKey.ncloc) { + const { asc, branchLike, metrics, requestedMetric, rootComponent, selected, view } = this.props; + // if asc is undefined we dont want to pass it inside options + const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, requestedMetric, { + ...(asc !== undefined && { asc }) + }); + const componentKey = selected || rootComponent.key; + const baseComponentMetrics = [requestedMetric.key]; + if (requestedMetric.key === MetricKey.ncloc) { baseComponentMetrics.push('ncloc_language_distribution'); } Promise.all([ @@ -124,19 +126,17 @@ export default class MeasureContent extends React.PureComponent { getMeasures({ component: componentKey, metricKeys: baseComponentMetrics.join(), - ...getBranchLikeQuery(this.props.branchLike) + ...getBranchLikeQuery(branchLike) }) ]).then(([tree, measures]) => { if (this.mounted) { - const metric = tree.metrics.find(m => m.key === this.props.requestedMetric.key); + const metric = tree.metrics.find(m => m.key === requestedMetric.key); const components = tree.components.map(component => - enhanceComponent(component, metric, this.props.metrics) + enhanceComponent(component, metric, metrics) ); - const measure = measures.find(measure => measure.metric === this.props.requestedMetric.key); - const secondaryMeasure = measures.find( - measure => measure.metric !== this.props.requestedMetric.key - ); + const measure = measures.find(measure => measure.metric === requestedMetric.key); + const secondaryMeasure = measures.find(measure => measure.metric !== requestedMetric.key); this.setState(({ selectedComponent }) => ({ baseComponent: tree.baseComponent, @@ -159,13 +159,15 @@ export default class MeasureContent extends React.PureComponent { }; fetchMoreComponents = () => { - const { metrics, view } = this.props; + const { metrics, view, asc } = this.props; const { baseComponent, metric, paging } = this.state; if (!baseComponent || !paging || !metric) { return; } + // if asc is undefined we dont want to pass it inside options const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric, { - p: paging.pageIndex + 1 + p: paging.pageIndex + 1, + ...(asc !== undefined && { asc }) }); this.setState({ loadingMoreComponents: true }); getComponentTree(strategy, baseComponent.key, metricKeys, opts).then( @@ -283,6 +285,17 @@ export default class MeasureContent extends React.PureComponent { scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth: true }); }; + getDefaultShowBestMeasures() { + const { asc, view } = this.props; + if (asc !== undefined && view === 'list') { + return true; + } else if (view === 'tree') { + return true; + } else { + return false; + } + } + renderMeasure() { const { view } = this.props; const { metric } = this.state; @@ -295,7 +308,7 @@ export default class MeasureContent extends React.PureComponent { { expect(wrapper).toMatchSnapshot(); }); +it('should render correctly when asc prop is defined', async () => { + const wrapper = shallowRender({ asc: true }); + expect(wrapper.type()).toBeNull(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); +}); + +it('should render correctly when view prop is tree', async () => { + const wrapper = shallowRender({ view: 'tree' }); + expect(wrapper.type()).toBeNull(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); +}); + it('should render correctly for a file', async () => { (getComponentTree as jest.Mock).mockResolvedValueOnce({ paging: { pageIndex: 1, pageSize: 500, total: 0 }, @@ -117,6 +131,85 @@ it('should correctly handle scrolling', () => { }); }); +it('should test fetchMoreComponents to work correctly', async () => { + (getComponentTree as jest.Mock).mockResolvedValueOnce({ + paging: { pageIndex: 12, pageSize: 500, total: 0 }, + baseComponent: mockComponentMeasure(false), + components: [], + metrics: [METRICS.bugs] + }); + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + wrapper.instance().fetchMoreComponents(); + expect(wrapper).toMatchSnapshot(); +}); + +it('should test getComponentRequestParams response for different arguments', () => { + const wrapper = shallowRender({ asc: false }); + const metric = { + direction: -1, + key: 'new_reliability_rating' + }; + const reqParamsList = { + metricKeys: ['new_reliability_rating'], + opts: { + additionalFields: 'metrics', + asc: true, + metricPeriodSort: 1, + metricSort: 'new_reliability_rating', + metricSortFilter: 'withMeasuresOnly', + ps: 500, + s: 'metricPeriod' + }, + strategy: 'leaves' + }; + expect(wrapper.instance().getComponentRequestParams('list', metric, { asc: true })).toEqual( + reqParamsList + ); + // when options.asc is not passed the opts.asc will take the default value + reqParamsList.opts.asc = false; + expect(wrapper.instance().getComponentRequestParams('list', metric, {})).toEqual(reqParamsList); + + const reqParamsTreeMap = { + metricKeys: ['new_reliability_rating', 'new_lines'], + opts: { + additionalFields: 'metrics', + asc: true, + metricPeriodSort: 1, + metricSort: 'new_lines', + metricSortFilter: 'withMeasuresOnly', + ps: 500, + s: 'metricPeriod' + }, + strategy: 'children' + }; + expect(wrapper.instance().getComponentRequestParams('treemap', metric, { asc: true })).toEqual( + reqParamsTreeMap + ); + // when options.asc is not passed the opts.asc will take the default value + reqParamsTreeMap.opts.asc = false; + expect(wrapper.instance().getComponentRequestParams('treemap', metric, {})).toEqual( + reqParamsTreeMap + ); + + const reqParamsTree = { + metricKeys: ['new_reliability_rating'], + opts: { + additionalFields: 'metrics', + asc: false, + ps: 500, + s: 'qualifier,name' + }, + strategy: 'children' + }; + expect(wrapper.instance().getComponentRequestParams('tree', metric, { asc: false })).toEqual( + reqParamsTree + ); + // when options.asc is not passed the opts.asc will take the default value + reqParamsTree.opts.asc = true; + expect(wrapper.instance().getComponentRequestParams('tree', metric, {})).toEqual(reqParamsTree); +}); + function shallowRender(props: Partial = {}) { return shallow( `; + +exports[`should render correctly when asc prop is defined 1`] = ` +
+ +
+
+
+ + } + right={ +
+ +
+ component_measures.view_as +
+ + +
+
+ } + /> +
+
+
+
+ + +
+
+`; + +exports[`should render correctly when view prop is tree 1`] = ` +
+ +
+
+
+ + } + right={ +
+ +
+ component_measures.view_as +
+ + +
+
+ } + /> +
+
+
+
+ + +
+
+`; + +exports[`should test fetchMoreComponents to work correctly 1`] = ` +
+ +
+
+
+ + } + right={ +
+ +
+ component_measures.view_as +
+ + +
+
+ } + /> +
+
+
+
+ + +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap index cc3d3f3ca07..730e6e4fb06 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/__tests__/__snapshots__/ComponentCell-test.tsx.snap @@ -16,6 +16,7 @@ exports[`should render correctly for a "APP" root component and a component with Object { "pathname": "/component_measures", "query": Object { + "asc": undefined, "branch": "develop", "id": "foo", "metric": "bugs", @@ -65,6 +66,7 @@ exports[`should render correctly for a "APP" root component and a component with Object { "pathname": "/component_measures", "query": Object { + "asc": undefined, "id": "foo", "metric": "bugs", "selected": "foo", @@ -110,6 +112,7 @@ exports[`should render correctly for a "TRK" root component and a component with Object { "pathname": "/component_measures", "query": Object { + "asc": undefined, "branch": "develop", "id": "foo", "metric": "bugs", @@ -151,6 +154,7 @@ exports[`should render correctly for a "TRK" root component and a component with Object { "pathname": "/component_measures", "query": Object { + "asc": undefined, "id": "foo", "metric": "bugs", "selected": "foo", @@ -191,6 +195,7 @@ exports[`should render correctly for a "VW" root component and a component with Object { "pathname": "/component_measures", "query": Object { + "asc": undefined, "branch": "develop", "id": "foo", "metric": "bugs", @@ -240,6 +245,7 @@ exports[`should render correctly for a "VW" root component and a component with Object { "pathname": "/component_measures", "query": Object { + "asc": undefined, "id": "foo", "metric": "bugs", "selected": "foo", @@ -285,6 +291,7 @@ exports[`should render correctly: default 1`] = ` Object { "pathname": "/component_measures", "query": Object { + "asc": undefined, "id": "foo", "metric": "bugs", "selected": "foo:src/index.tsx", diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.ts b/server/sonar-web/src/main/js/apps/component-measures/utils.ts index 3a071456b4f..34e88b9aae6 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.ts +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.ts @@ -22,7 +22,12 @@ import { enhanceMeasure } from '../../components/measure/utils'; import { isBranch, isPullRequest } from '../../helpers/branch-like'; import { getLocalizedMetricName } from '../../helpers/l10n'; import { getDisplayMetrics, isDiffMetric } from '../../helpers/measures'; -import { cleanQuery, parseAsString, serializeString } from '../../helpers/query'; +import { + cleanQuery, + parseAsOptionalBoolean, + parseAsString, + serializeString +} from '../../helpers/query'; import { BranchLike } from '../../types/branch-like'; import { ComponentQualifier } from '../../types/component'; import { MeasurePageView } from '../../types/measures'; @@ -217,6 +222,7 @@ export interface Query { metric: string; selected?: string; view: MeasurePageView; + asc?: boolean; } export const parseQuery = memoize( @@ -225,7 +231,8 @@ export const parseQuery = memoize( return { metric, selected: parseAsString(urlQuery['selected']), - view: parseView(metric, urlQuery['view']) + view: parseView(metric, urlQuery['view']), + asc: parseAsOptionalBoolean(urlQuery['asc']) }; } ); diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/DrilldownLink-test.tsx.snap b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/DrilldownLink-test.tsx.snap index 43494b4d2bf..cb653af58a1 100644 --- a/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/DrilldownLink-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/shared/__tests__/__snapshots__/DrilldownLink-test.tsx.snap @@ -8,6 +8,7 @@ exports[`should render correctly 1`] = ` Object { "pathname": "/component_measures", "query": Object { + "asc": undefined, "id": "project123", "metric": "other", "view": "list", diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts index afa9a0ecca7..63fc209c51f 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts @@ -142,6 +142,27 @@ describe('#getComponentDrilldownUrl', () => { query: { id: COMPLEX_COMPONENT_KEY, metric: METRIC } }); }); + + it('should add asc param only when its list view', () => { + expect( + getComponentDrilldownUrl({ componentKey: SIMPLE_COMPONENT_KEY, metric: METRIC, asc: false }) + ).toEqual({ + pathname: '/component_measures', + query: { id: SIMPLE_COMPONENT_KEY, metric: METRIC } + }); + + expect( + getComponentDrilldownUrl({ + componentKey: SIMPLE_COMPONENT_KEY, + metric: METRIC, + listView: true, + asc: false + }) + ).toEqual({ + pathname: '/component_measures', + query: { id: SIMPLE_COMPONENT_KEY, metric: METRIC, asc: 'false', view: 'list' } + }); + }); }); describe('#getComponentDrilldownUrlWithSelection', () => { diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 8045f920430..e7aaa61ddbb 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -27,6 +27,7 @@ import { SecurityStandard } from '../types/security'; import { Dict, HomePage } from '../types/types'; import { getBranchLikeQuery, isBranch, isMainBranch, isPullRequest } from './branch-like'; import { IS_SSR } from './browser'; +import { serializeOptionalBoolean } from './query'; import { getBaseUrl } from './system'; export interface Location { @@ -161,14 +162,16 @@ export function getComponentDrilldownUrl(options: { selectionKey?: string; treemapView?: boolean; listView?: boolean; + asc?: boolean; }): Location { - const { componentKey, metric, branchLike, selectionKey, treemapView, listView } = options; + const { componentKey, metric, branchLike, selectionKey, treemapView, listView, asc } = options; const query: Query = { id: componentKey, metric, ...getBranchLikeQuery(branchLike) }; if (treemapView) { query.view = 'treemap'; } if (listView) { query.view = 'list'; + query.asc = serializeOptionalBoolean(asc); } if (selectionKey) { query.selected = selectionKey; -- 2.39.5