diff options
author | David Cho-Lerat <david.cho-lerat@sonarsource.com> | 2023-03-13 12:35:13 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-03-14 20:03:27 +0000 |
commit | c4e2a351504a92709154992ab1c2963201538d4c (patch) | |
tree | c88f89688a387b6d24cdd13230429a821d64033f /server/sonar-web/src/main/js/apps | |
parent | dd791e68ca7a5b0375e7cba70a2f8f27d4c75c26 (diff) | |
download | sonarqube-c4e2a351504a92709154992ab1c2963201538d4c.tar.gz sonarqube-c4e2a351504a92709154992ab1c2963201538d4c.zip |
Fix some code smells in MMF-3035
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
10 files changed, 181 insertions, 49 deletions
diff --git a/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx b/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx index 99ff1c64e91..34b3b2c16cc 100644 --- a/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx +++ b/server/sonar-web/src/main/js/apps/account/profile/UserExternalIdentity.tsx @@ -17,10 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { getTextColor } from 'design-system'; import * as React from 'react'; import { getIdentityProviders } from '../../../api/users'; import { colors } from '../../../app/theme'; -import { getTextColor } from '../../../helpers/colors'; import { getBaseUrl } from '../../../helpers/system'; import { IdentityProvider } from '../../../types/types'; import { LoggedInUser } from '../../../types/users'; diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx index d74f0b6bd07..7cb089ccf76 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx @@ -17,6 +17,7 @@ * 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 Link from '../../../components/common/Link'; import { ButtonLink } from '../../../components/controls/buttons'; @@ -217,7 +218,7 @@ export default class RuleDetailsMeta extends React.PureComponent<Props> { const EXTERNAL_PREFIX = 'external_'; const { ruleDetails } = this.props; const displayedKey = ruleDetails.key.startsWith(EXTERNAL_PREFIX) - ? ruleDetails.key.substr(EXTERNAL_PREFIX.length) + ? ruleDetails.key.substring(EXTERNAL_PREFIX.length) : ruleDetails.key; return <span className="note text-middle">{displayedKey}</span>; } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx b/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx index 5ba7f224b13..5a9221be36b 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/routes.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import React, { useEffect } from 'react'; import { Route, useLocation, useNavigate } from 'react-router-dom'; import { RawQuery } from '../../types/types'; @@ -44,7 +45,7 @@ function HashEditWrapper() { useEffect(() => { const { hash } = location; if (hash.length > 1) { - const query = parseHash(hash.substr(1)); + const query = parseHash(hash.substring(1)); const normalizedQuery = { ...serializeQuery(parseQuery(query)), open: query.open, diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx index 78107922aac..a1fe1c5d19e 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx @@ -17,6 +17,7 @@ * 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 Issue from '../../../components/issue/Issue'; import { BranchLike } from '../../../types/branch-like'; @@ -59,7 +60,7 @@ export default class ListItem extends React.PureComponent<Props> { const issuesReset = { issues: [] }; if (property.startsWith('tag###')) { - const tag = property.substr(6); + const tag = property.substring('tag###'.length); onFilterChange({ ...issuesReset, tags: [tag] }); } else { switch (property) { diff --git a/server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts index b6c7496acb2..230acd91907 100644 --- a/server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts @@ -17,8 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { searchProjects } from '../../../api/components'; import { mockComponent } from '../../../helpers/mocks/component'; +import { Component } from '../../../types/types'; import * as utils from '../utils'; jest.mock('../../../api/components', () => ({ @@ -55,11 +57,13 @@ describe('parseSorting', () => { }); describe('formatDuration', () => { - const ONE_MINUTE = 60000; + const ONE_SECOND = 1000; + const ONE_MINUTE = 60 * ONE_SECOND; const ONE_HOUR = 60 * ONE_MINUTE; const ONE_DAY = 24 * ONE_HOUR; const ONE_MONTH = 30 * ONE_DAY; const ONE_YEAR = 12 * ONE_MONTH; + it('render years and months only', () => { expect(utils.formatDuration(ONE_YEAR * 4 + ONE_MONTH * 2 + ONE_DAY * 10)).toEqual( 'duration.years.4 duration.months.2 ' @@ -81,13 +85,14 @@ describe('formatDuration', () => { }); it('render less than a minute', () => { - expect(utils.formatDuration(1000)).toEqual('duration.seconds'); + expect(utils.formatDuration(ONE_SECOND)).toEqual('duration.seconds'); }); }); describe('fetchProjects', () => { it('correctly converts the passed arguments to the desired query format', async () => { - await utils.fetchProjects({}, true); + await utils.fetchProjects({ isFavorite: true, query: {} }); + expect(searchProjects).toHaveBeenCalledWith({ f: 'analysisDate,leakPeriodDate', facets: utils.FACETS.join(), @@ -96,7 +101,8 @@ describe('fetchProjects', () => { ps: 50, }); - await utils.fetchProjects({ view: 'leak' }, false, 3); + await utils.fetchProjects({ isFavorite: false, pageIndex: 3, query: { view: 'leak' } }); + expect(searchProjects).toHaveBeenCalledWith({ f: 'analysisDate,leakPeriodDate', facets: utils.LEAK_FACETS.join(), @@ -107,6 +113,7 @@ describe('fetchProjects', () => { it('correctly treats result data', async () => { const components = [mockComponent({ key: 'foo' }), mockComponent({ key: 'bar' })]; + (searchProjects as jest.Mock).mockResolvedValue({ components, facets: [ @@ -121,20 +128,25 @@ describe('fetchProjects', () => { ], paging: { total: 2 }, }); - await utils.fetchProjects({}, true).then((r) => { + + await utils.fetchProjects({ isFavorite: true, query: {} }).then((r) => { expect(r).toEqual({ facets: { new_coverage: { NO_DATA: 0 }, languages: { css: 10, js: 2 }, }, - projects: components.map((component: any) => { - if (component.key === 'foo') { - component.measures = { new_coverage: '10' }; - } else { - component.measures = { languages: '20' }; + projects: components.map( + (component: Component & { measures: { languages?: string; new_coverage?: string } }) => { + // eslint-disable-next-line jest/no-conditional-in-test + if (component.key === 'foo') { + component.measures = { new_coverage: '10' }; + } else { + component.measures = { languages: '20' }; + } + + return component; } - return component; - }), + ), total: 2, }); }); @@ -148,3 +160,11 @@ describe('defineMetrics', () => { expect(utils.defineMetrics({})).toBe(utils.METRICS); }); }); + +describe('convertToSorting', () => { + it('handles asc and desc sort', () => { + expect(utils.convertToSorting({ sort: '-size' })).toStrictEqual({ asc: false, s: 'ncloc' }); + expect(utils.convertToSorting({})).toStrictEqual({ s: undefined }); + expect(utils.convertToSorting({ sort: 'search' })).toStrictEqual({ s: 'query' }); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index 55aa17d9838..536f90a44cb 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx @@ -81,7 +81,9 @@ export class AllProjects extends React.PureComponent<Props, State> { handleRequiredAuthentication(); return; } + this.handleQueryChange(); + addSideBarClass(); } @@ -97,8 +99,11 @@ export class AllProjects extends React.PureComponent<Props, State> { } fetchProjects = (query: Query) => { + const { isFavorite } = this.props; + this.setState({ loading: true, query }); - fetchProjects(query, this.props.isFavorite).then((response) => { + + fetchProjects({ isFavorite, query }).then((response) => { if (this.mounted) { this.setState({ facets: response.facets, @@ -112,10 +117,13 @@ export class AllProjects extends React.PureComponent<Props, State> { }; fetchMoreProjects = () => { + const { isFavorite } = this.props; const { pageIndex, projects, query } = this.state; - if (pageIndex && projects && query) { + + if (pageIndex && projects && Object.keys(query).length !== 0) { this.setState({ loading: true }); - fetchProjects(query, this.props.isFavorite, pageIndex + 1).then((response) => { + + fetchProjects({ isFavorite, query, pageIndex: pageIndex + 1 }).then((response) => { if (this.mounted) { this.setState({ loading: false, @@ -127,9 +135,9 @@ export class AllProjects extends React.PureComponent<Props, State> { } }; - getSort = () => this.state.query.sort || 'name'; + getSort = () => this.state.query.sort ?? 'name'; - getView = () => this.state.query.view || 'overall'; + getView = () => this.state.query.view ?? 'overall'; handleClearAll = () => { this.props.router.push({ pathname: this.props.location.pathname }); @@ -147,7 +155,7 @@ export class AllProjects extends React.PureComponent<Props, State> { }); }; - handlePerspectiveChange = ({ view }: { view: string }) => { + handlePerspectiveChange = ({ view }: { view?: string }) => { const query: { view: string | undefined; sort?: string | undefined; @@ -158,6 +166,7 @@ export class AllProjects extends React.PureComponent<Props, State> { if (this.state.query.view === 'leak' || view === 'leak') { if (this.state.query.sort) { const sort = parseSorting(this.state.query.sort); + if (SORTING_SWITCH[sort.sortValue]) { query.sort = (sort.sortDesc ? '-' : '') + SORTING_SWITCH[sort.sortValue]; } @@ -306,12 +315,15 @@ function getStorageOptions() { sort?: string; view?: string; } = {}; + if (get(LS_PROJECTS_SORT)) { options.sort = get(LS_PROJECTS_SORT) || undefined; } + if (get(LS_PROJECTS_VIEW)) { options.view = get(LS_PROJECTS_VIEW) || undefined; } + return options; } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index 9be34752c05..88c55dc28d0 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -76,8 +76,10 @@ it('renders', () => { it('fetches projects', () => { shallowRender(); - expect(fetchProjects).toHaveBeenLastCalledWith( - { + + expect(fetchProjects).toHaveBeenLastCalledWith({ + isFavorite: false, + query: { coverage: undefined, duplications: undefined, gate: undefined, @@ -97,46 +99,58 @@ it('fetches projects', () => { tags: undefined, view: undefined, }, - false - ); + }); }); it('changes sort', () => { const push = jest.fn(); - const wrapper = shallowRender({}, push); - wrapper.find('PageHeader').prop<Function>('onSortChange')('size', false); + const wrapper = shallowRender({ push }); + + wrapper.find('PageHeader').prop<(sort: string, desc: boolean) => void>('onSortChange')( + 'size', + false + ); + expect(push).toHaveBeenLastCalledWith({ pathname: '/projects', query: { sort: 'size' } }); expect(save).toHaveBeenLastCalledWith(LS_PROJECTS_SORT, 'size'); }); it('changes perspective to leak', () => { const push = jest.fn(); - const wrapper = shallowRender({}, push); - wrapper.find('PageHeader').prop<Function>('onPerspectiveChange')({ view: 'leak' }); + const wrapper = shallowRender({ push }); + + wrapper.find('PageHeader').prop<({ view }: { view?: string }) => void>('onPerspectiveChange')({ + view: 'leak', + }); + expect(push).toHaveBeenLastCalledWith({ pathname: '/projects', query: { view: 'leak' }, }); + expect(save).toHaveBeenCalledWith(LS_PROJECTS_SORT, undefined); expect(save).toHaveBeenCalledWith(LS_PROJECTS_VIEW, 'leak'); }); it('updates sorting when changing perspective from leak', () => { const push = jest.fn(); - const wrapper = shallowRender({}, push); + const wrapper = shallowRender({ push }); wrapper.setState({ query: { sort: 'new_coverage', view: 'leak' } }); - wrapper.find('PageHeader').prop<Function>('onPerspectiveChange')({ + + wrapper.find('PageHeader').prop<({ view }: { view?: string }) => void>('onPerspectiveChange')({ view: undefined, }); + expect(push).toHaveBeenLastCalledWith({ pathname: '/projects', query: { sort: 'coverage', view: undefined }, }); + expect(save).toHaveBeenCalledWith(LS_PROJECTS_SORT, 'coverage'); expect(save).toHaveBeenCalledWith(LS_PROJECTS_VIEW, undefined); }); -it('handles favorite projects', () => { +it('handles updating the favorite status of a project', () => { const wrapper = shallowRender(); expect(wrapper.state('projects')).toMatchSnapshot(); @@ -144,11 +158,28 @@ it('handles favorite projects', () => { expect(wrapper.state('projects')).toMatchSnapshot(); }); -function shallowRender( - props: Partial<AllProjects['props']> = {}, - push = jest.fn(), - replace = jest.fn() -) { +it('handles showing favorite projects on load', () => { + const wrapper = shallowRender({ + props: { currentUser: { dismissedNotices: {}, isLoggedIn: false }, isFavorite: true }, + }); + + expect(wrapper.state('projects')).toMatchSnapshot(); + + wrapper.instance().handleFavorite('foo', true); + expect(wrapper.state('projects')).toMatchSnapshot(); +}); + +const defaults = { props: {}, push: () => undefined, replace: () => undefined }; + +function shallowRender({ + props = defaults.props, + push = defaults.push, + replace = defaults.replace, +}: { + props?: Partial<AllProjects['props']>; + push?: () => void; + replace?: () => void; +} = defaults) { const wrapper = shallow<AllProjects>( <AllProjects currentUser={{ isLoggedIn: true, dismissedNotices: {} }} @@ -161,6 +192,7 @@ function shallowRender( {...props} /> ); + wrapper.setState({ loading: false, projects: [ @@ -175,5 +207,6 @@ function shallowRender( ], total: 0, }); + return wrapper; } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap index aefede3dc66..9bad4f35742 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`handles favorite projects 1`] = ` +exports[`handles showing favorite projects on load 1`] = ` [ { "key": "foo", @@ -13,7 +13,34 @@ exports[`handles favorite projects 1`] = ` ] `; -exports[`handles favorite projects 2`] = ` +exports[`handles showing favorite projects on load 2`] = ` +[ + { + "isFavorite": true, + "key": "foo", + "measures": {}, + "name": "Foo", + "qualifier": "TRK", + "tags": [], + "visibility": "public", + }, +] +`; + +exports[`handles updating the favorite status of a project 1`] = ` +[ + { + "key": "foo", + "measures": {}, + "name": "Foo", + "qualifier": "TRK", + "tags": [], + "visibility": "public", + }, +] +`; + +exports[`handles updating the favorite status of a project 2`] = ` [ { "isFavorite": true, diff --git a/server/sonar-web/src/main/js/apps/projects/utils.ts b/server/sonar-web/src/main/js/apps/projects/utils.ts index 51a7afbe3ff..235018b7a9a 100644 --- a/server/sonar-web/src/main/js/apps/projects/utils.ts +++ b/server/sonar-web/src/main/js/apps/projects/utils.ts @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { invert } from 'lodash'; import { Facet, searchProjects } from '../../api/components'; import { getMeasuresForProjects } from '../../api/measures'; @@ -150,22 +151,33 @@ export const LEAK_FACETS = [ const REVERSED_FACETS = ['coverage', 'new_coverage']; export function localizeSorting(sort?: string): string { - return translate('projects.sort', sort || 'name'); + return translate('projects.sort', sort ?? 'name'); } export function parseSorting(sort: string): { sortValue: string; sortDesc: boolean } { - const desc = sort[0] === '-'; - return { sortValue: desc ? sort.substr(1) : sort, sortDesc: desc }; + const desc = sort.startsWith('-'); + + return { sortValue: desc ? sort.substring(1) : sort, sortDesc: desc }; } -export function fetchProjects(query: Query, isFavorite: boolean, pageIndex = 1) { +export function fetchProjects({ + isFavorite, + query, + pageIndex = 1, +}: { + query: Query; + isFavorite: boolean; + pageIndex?: number; +}) { const ps = PAGE_SIZE; + const data = convertToQueryData(query, isFavorite, { p: pageIndex > 1 ? pageIndex : undefined, ps, facets: defineFacets(query).join(), f: 'analysisDate,leakPeriodDate', }); + return searchProjects(data) .then((response) => Promise.all([fetchProjectMeasures(response.components, query), Promise.resolve(response)]) @@ -183,6 +195,7 @@ export function fetchProjects(query: Query, isFavorite: boolean, pageIndex = 1) componentMeasures[measure.metric] = value; } }); + return { ...component, measures: componentMeasures }; }), total: paging.total, @@ -194,6 +207,7 @@ export function defineMetrics(query: Query): string[] { if (query.view === 'leak') { return LEAK_METRICS; } + return METRICS; } @@ -201,6 +215,7 @@ function defineFacets(query: Query): string[] { if (query.view === 'leak') { return LEAK_FACETS; } + return FACETS; } @@ -212,12 +227,15 @@ function convertToQueryData(query: Query, isFavorite: boolean, defaultData = {}) if (filter) { data.filter = filter; } + if (sort.s) { data.s = sort.s; } + if (sort.asc !== undefined) { data.asc = sort.asc; } + return data; } @@ -228,14 +246,17 @@ export function fetchProjectMeasures(projects: Array<{ key: string }>, query: Qu const projectKeys = projects.map((project) => project.key); const metrics = defineMetrics(query); + return getMeasuresForProjects(projectKeys, metrics); } function mapFacetValues(values: Array<{ val: string; count: number }>) { const map: Dict<number> = {}; + values.forEach((value) => { map[value.val] = value.count; }); + return map; } @@ -266,22 +287,27 @@ const metricToPropertyMap = invert(propertyToMetricMap); function getFacetsMap(facets: Facet[]) { const map: Dict<Dict<number>> = {}; + facets.forEach((facet) => { const property = metricToPropertyMap[facet.property]; const { values } = facet; + if (REVERSED_FACETS.includes(property)) { values.reverse(); } + map[property] = mapFacetValues(values); }); + return map; } -function convertToSorting({ sort }: Query): { s?: string; asc?: boolean } { - if (sort && sort[0] === '-') { - return { s: propertyToMetricMap[sort.substr(1)], asc: false }; +export function convertToSorting({ sort }: Query): { s?: string; asc?: boolean } { + if (sort?.startsWith('-')) { + return { s: propertyToMetricMap[sort.substring(1)], asc: false }; } - return { s: propertyToMetricMap[sort || ''] }; + + return { s: propertyToMetricMap[sort ?? ''] }; } const ONE_MINUTE = 60000; @@ -294,15 +320,18 @@ function format(periods: Array<{ value: number; label: string }>) { let result = ''; let count = 0; let lastId = -1; + for (let i = 0; i < periods.length && count < 2; i++) { if (periods[i].value > 0) { count++; + if (lastId < 0 || lastId + 1 === i) { lastId = i; result += translateWithParameters(periods[i].label, periods[i].value) + ' '; } } } + return result; } @@ -310,15 +339,21 @@ export function formatDuration(ms: number) { if (ms < ONE_MINUTE) { return translate('duration.seconds'); } + const years = Math.floor(ms / ONE_YEAR); ms -= years * ONE_YEAR; + const months = Math.floor(ms / ONE_MONTH); ms -= months * ONE_MONTH; + const days = Math.floor(ms / ONE_DAY); ms -= days * ONE_DAY; + const hours = Math.floor(ms / ONE_HOUR); ms -= hours * ONE_HOUR; + const minutes = Math.floor(ms / ONE_MINUTE); + return format([ { value: years, label: 'duration.years' }, { value: months, label: 'duration.months' }, diff --git a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx index df385e549ed..6d4d6872ce8 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserListItemIdentity.tsx @@ -17,9 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { getTextColor } from 'design-system'; import * as React from 'react'; import { colors } from '../../../app/theme'; -import { getTextColor } from '../../../helpers/colors'; import { getBaseUrl } from '../../../helpers/system'; import { IdentityProvider } from '../../../types/types'; import { User } from '../../../types/users'; |