From 69aa352100a5b14f0530007f9015a093e856d8a0 Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Thu, 8 Aug 2024 17:33:02 +0200 Subject: [PATCH] SONAR-22727 Portfolio breakdown page --- .../sonar-web/src/main/js/api/components.ts | 8 - .../js/api/mocks/ComponentsServiceMock.ts | 29 +- .../main/js/apps/code/__tests__/Code-it.ts | 3 + .../__snapshots__/utils-test.tsx.snap | 36 +++ .../js/apps/code/__tests__/buckets-test.tsx | 59 ---- .../js/apps/code/__tests__/utils-test.tsx | 120 +------- .../sonar-web/src/main/js/apps/code/bucket.ts | 72 ----- .../main/js/apps/code/components/CodeApp.tsx | 265 +++++++----------- .../apps/code/components/CodeAppRenderer.tsx | 144 +++++----- .../js/apps/code/components/Component.tsx | 18 +- .../sonar-web/src/main/js/apps/code/utils.ts | 218 +------------- .../js/apps/projects/__tests__/utils-test.ts | 13 - .../components/__tests__/AllProjects-test.tsx | 3 + .../__tests__/ProjectCard-test.tsx | 10 + .../__tests__/ProjectCardMeasures-test.tsx | 1 - .../sonar-web/src/main/js/queries/common.ts | 13 + .../src/main/js/queries/component.ts | 173 ++++++++++-- .../sonar-web/src/main/js/queries/settings.ts | 2 +- 18 files changed, 442 insertions(+), 745 deletions(-) delete mode 100644 server/sonar-web/src/main/js/apps/code/__tests__/buckets-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/code/bucket.ts diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index 4ef35769030..654b004e92f 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -91,14 +91,6 @@ export function getComponentTree( return getJSON(url, data).catch(throwGlobalError); } -export function getChildren( - component: string, - metrics: string[] = [], - additional: RequestData = {}, -) { - return getComponentTree('children', component, metrics, additional); -} - export function getComponentLeaves( component: string, metrics: string[] = [], diff --git a/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts index 0e55f044311..0a322db27d6 100644 --- a/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts @@ -40,7 +40,7 @@ import { changeKey, doesComponentExists, getBreadcrumbs, - getChildren, + getComponent, getComponentData, getComponentForSourceViewer, getComponentLeaves, @@ -93,8 +93,8 @@ export default class ComponentsServiceMock { this.measures = cloneDeep(this.defaultMeasures); this.projects = cloneDeep(this.defaultProjects); + jest.mocked(getComponent).mockImplementation(this.handleGetComponent); jest.mocked(getComponentTree).mockImplementation(this.handleGetComponentTree); - jest.mocked(getChildren).mockImplementation(this.handleGetChildren); jest.mocked(getTree).mockImplementation(this.handleGetTree); jest.mocked(getComponentData).mockImplementation(this.handleGetComponentData); jest @@ -244,19 +244,6 @@ export default class ComponentsServiceMock { this.measures = cloneDeep(this.defaultMeasures); }; - handleGetChildren = ( - component: string, - metrics: string[] = [], - data: RequestData = {}, - ): Promise<{ - baseComponent: ComponentMeasure; - components: ComponentMeasure[]; - metrics: Metric[]; - paging: Paging; - }> => { - return this.handleGetComponentTree('children', component, metrics, data); - }; - handleGetComponentTree = ( strategy: string, key: string, @@ -350,6 +337,18 @@ export default class ComponentsServiceMock { throw new Error(`Couldn't find component with key ${data.component}`); }; + handleGetComponent: typeof getComponent = (data: { component: string } & BranchParameters) => { + if (this.failLoadingComponentStatus !== undefined) { + return Promise.reject({ status: this.failLoadingComponentStatus }); + } + const tree = this.findComponentTree(data.component); + if (tree) { + const { component } = tree; + return this.reply({ component }); + } + throw new Error(`Couldn't find component with key ${data.component}`); + }; + handleGetComponentForSourceViewer = ({ component }: { component: string } & BranchParameters) => { const sourceFile = this.findSourceFile(component); return this.reply(sourceFile.component); diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts index 0f59e34a416..b0bdc8c17f6 100644 --- a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts +++ b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts @@ -34,6 +34,7 @@ import BranchesServiceMock from '../../../api/mocks/BranchesServiceMock'; import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock'; import { PARENT_COMPONENT_KEY, RULE_1 } from '../../../api/mocks/data/ids'; import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock'; +import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock'; import SourcesServiceMock from '../../../api/mocks/SourcesServiceMock'; import { CCT_SOFTWARE_QUALITY_METRICS } from '../../../helpers/constants'; import { isDiffMetric } from '../../../helpers/measures'; @@ -70,6 +71,7 @@ const branchesHandler = new BranchesServiceMock(); const componentsHandler = new ComponentsServiceMock(); const sourcesHandler = new SourcesServiceMock(); const issuesHandler = new IssuesServiceMock(); +const settingsHandler = new SettingsServiceMock(); const JUPYTER_ISSUE = { issue: mockRawIssue(false, { @@ -126,6 +128,7 @@ beforeEach(() => { componentsHandler.reset(); sourcesHandler.reset(); issuesHandler.reset(); + settingsHandler.reset(); }); it('should allow navigating through the tree', async () => { diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap index 016ff06d553..5ada596b663 100644 --- a/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap @@ -19,16 +19,28 @@ exports[`getCodeMetrics should return the right metrics for apps 1`] = ` exports[`getCodeMetrics should return the right metrics for portfolios 1`] = ` [ "releasability_rating", + "releasability_rating_new", "new_security_rating", + "new_security_rating_new", "new_reliability_rating", + "new_reliability_rating_new", "new_maintainability_rating", + "new_maintainability_rating_new", "new_security_review_rating", + "new_security_review_rating_new", "new_lines", "releasability_rating", + "releasability_rating_new", "security_rating", + "security_rating_new", + "security_rating", + "security_rating_new", "reliability_rating", + "reliability_rating_new", "sqale_rating", + "sqale_rating_new", "security_review_rating", + "security_review_rating_new", "ncloc", ] `; @@ -36,16 +48,28 @@ exports[`getCodeMetrics should return the right metrics for portfolios 1`] = ` exports[`getCodeMetrics should return the right metrics for portfolios 2`] = ` [ "releasability_rating", + "releasability_rating_new", "new_security_rating", + "new_security_rating_new", "new_reliability_rating", + "new_reliability_rating_new", "new_maintainability_rating", + "new_maintainability_rating_new", "new_security_review_rating", + "new_security_review_rating_new", "new_lines", "releasability_rating", + "releasability_rating_new", + "security_rating", + "security_rating_new", "security_rating", + "security_rating_new", "reliability_rating", + "reliability_rating_new", "sqale_rating", + "sqale_rating_new", "security_review_rating", + "security_review_rating_new", "ncloc", "alert_status", ] @@ -54,10 +78,15 @@ exports[`getCodeMetrics should return the right metrics for portfolios 2`] = ` exports[`getCodeMetrics should return the right metrics for portfolios 3`] = ` [ "releasability_rating", + "releasability_rating_new", "new_security_rating", + "new_security_rating_new", "new_reliability_rating", + "new_reliability_rating_new", "new_maintainability_rating", + "new_maintainability_rating_new", "new_security_review_rating", + "new_security_review_rating_new", "new_lines", "alert_status", ] @@ -66,10 +95,17 @@ exports[`getCodeMetrics should return the right metrics for portfolios 3`] = ` exports[`getCodeMetrics should return the right metrics for portfolios 4`] = ` [ "releasability_rating", + "releasability_rating_new", + "security_rating", + "security_rating_new", "security_rating", + "security_rating_new", "reliability_rating", + "reliability_rating_new", "sqale_rating", + "sqale_rating_new", "security_review_rating", + "security_review_rating_new", "ncloc", "alert_status", ] diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/buckets-test.tsx b/server/sonar-web/src/main/js/apps/code/__tests__/buckets-test.tsx deleted file mode 100644 index 2357e8707e2..00000000000 --- a/server/sonar-web/src/main/js/apps/code/__tests__/buckets-test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2024 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 { ComponentMeasure } from '../../../types/types'; -import { addComponent, addComponentChildren, getComponent, getComponentChildren } from '../bucket'; - -const component: ComponentMeasure = { key: 'frodo', name: 'frodo', qualifier: 'frodo' }; - -const componentKey: string = 'foo'; -const childrenA: ComponentMeasure[] = [ - { key: 'foo', name: 'foo', qualifier: 'foo' }, - { key: 'bar', name: 'bar', qualifier: 'bar' }, -]; -const childrenB: ComponentMeasure[] = [ - { key: 'bart', name: 'bart', qualifier: 'bart' }, - { key: 'simpson', name: 'simpson', qualifier: 'simpson' }, -]; - -it('should have empty bucket at start', () => { - expect(getComponent(component.key)).toBeUndefined(); -}); - -it('should be able to store components in a bucket', () => { - addComponent(component); - expect(getComponent(component.key)).toEqual(component); -}); - -it('should have empty children bucket at start', () => { - expect(getComponentChildren(componentKey)).toBeUndefined(); -}); - -it('should be able to store children components in a bucket', () => { - addComponentChildren(componentKey, childrenA, childrenA.length, 1); - expect(getComponentChildren(componentKey).children).toEqual(childrenA); -}); - -it('should append new children components at the end of the bucket', () => { - addComponentChildren(componentKey, childrenB, 4, 2); - const finalBucket = getComponentChildren(componentKey); - expect(finalBucket.children).toEqual([...childrenA, ...childrenB]); - expect(finalBucket.total).toBe(4); - expect(finalBucket.page).toBe(2); -}); diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx b/server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx index 094c0effa08..c2c19e55ce4 100644 --- a/server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx +++ b/server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx @@ -18,40 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { ComponentQualifier } from '~sonar-aligned/types/component'; -import { getBreadcrumbs, getChildren, getComponent } from '../../../api/components'; import { mockMainBranch, mockPullRequest } from '../../../helpers/mocks/branch-like'; -import { - addComponent, - addComponentBreadcrumbs, - addComponentChildren, - getComponentBreadcrumbs, -} from '../bucket'; -import { - getCodeMetrics, - loadMoreChildren, - mostCommonPrefix, - retrieveComponent, - retrieveComponentChildren, -} from '../utils'; - -jest.mock('../../../api/components', () => ({ - getBreadcrumbs: jest.fn().mockRejectedValue({}), - getChildren: jest.fn().mockRejectedValue({}), - getComponent: jest.fn().mockRejectedValue({}), -})); - -jest.mock('../bucket', () => ({ - addComponent: jest.fn(), - addComponentBreadcrumbs: jest.fn(), - addComponentChildren: jest.fn(), - getComponent: jest.fn(), - getComponentBreadcrumbs: jest.fn(), - getComponentChildren: jest.fn(), -})); - -beforeEach(() => { - jest.clearAllMocks(); -}); +import { getCodeMetrics, mostCommonPrefix } from '../utils'; describe('getCodeMetrics', () => { it('should return the right metrics for portfolios', () => { @@ -83,92 +51,6 @@ describe('getCodeMetrics', () => { }); }); -describe('retrieveComponentChildren', () => { - it('should retrieve children correctly', async () => { - const components = [{}, {}]; - (getChildren as jest.Mock).mockResolvedValueOnce({ - components, - paging: { total: 2, pageIndex: 0 }, - }); - - await retrieveComponentChildren( - 'key', - ComponentQualifier.Project, - { mounted: true }, - mockMainBranch(), - ); - - expect(addComponentChildren).toHaveBeenCalledWith('key', components, 2, 0); - expect(addComponent).toHaveBeenCalledTimes(2); - expect(getComponentBreadcrumbs).toHaveBeenCalledWith('key'); - }); -}); - -describe('retrieveComponent', () => { - it('should update bucket when component is mounted', async () => { - const components = [{}, {}]; - (getChildren as jest.Mock).mockResolvedValueOnce({ - components, - paging: { total: 2, pageIndex: 0 }, - }); - (getComponent as jest.Mock).mockResolvedValueOnce({ - component: {}, - }); - (getBreadcrumbs as jest.Mock).mockResolvedValueOnce([]); - - await retrieveComponent('key', ComponentQualifier.Project, { mounted: true }, mockMainBranch()); - - expect(addComponentChildren).toHaveBeenCalled(); - expect(addComponent).toHaveBeenCalledTimes(3); - expect(addComponentBreadcrumbs).toHaveBeenCalled(); - }); - - it('should not update bucket when component is not mounted', async () => { - const components = [{}, {}]; - (getChildren as jest.Mock).mockResolvedValueOnce({ - components, - paging: { total: 2, pageIndex: 0 }, - }); - (getComponent as jest.Mock).mockResolvedValueOnce({ - component: {}, - }); - (getBreadcrumbs as jest.Mock).mockResolvedValueOnce([]); - - await retrieveComponent( - 'key', - ComponentQualifier.Project, - { mounted: false }, - mockMainBranch(), - ); - - expect(addComponentChildren).not.toHaveBeenCalled(); - expect(addComponent).not.toHaveBeenCalled(); - expect(addComponentBreadcrumbs).not.toHaveBeenCalled(); - }); -}); - -describe('loadMoreChildren', () => { - it('should load more children', async () => { - const components = [{}, {}, {}]; - (getChildren as jest.Mock).mockResolvedValueOnce({ - components, - paging: { total: 6, pageIndex: 1 }, - }); - - await loadMoreChildren( - 'key', - 1, - ComponentQualifier.Project, - { mounted: true }, - mockMainBranch(), - ); - - expect(addComponentChildren).toHaveBeenCalledWith('key', components, 6, 1); - expect(addComponent).toHaveBeenCalledTimes(3); - expect(getComponentBreadcrumbs).toHaveBeenCalledWith('key'); - }); -}); - describe('#mostCommonPrefix', () => { it('should correctly find the common path prefix', () => { expect(mostCommonPrefix(['src/main/ts/tests', 'src/main/java/tests'])).toEqual('src/main/'); diff --git a/server/sonar-web/src/main/js/apps/code/bucket.ts b/server/sonar-web/src/main/js/apps/code/bucket.ts deleted file mode 100644 index 9a3827b70a7..00000000000 --- a/server/sonar-web/src/main/js/apps/code/bucket.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2024 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 { Breadcrumb } from '~sonar-aligned/types/component'; -import { ComponentMeasure, Dict } from '../../types/types'; - -let bucket: Dict = {}; -let childrenBucket: Dict<{ - children: ComponentMeasure[]; - page: number; - total: number; -}> = {}; -let breadcrumbsBucket: Dict = {}; - -export function addComponent(component: ComponentMeasure): void { - bucket[component.key] = component; -} - -export function getComponent(componentKey: string): ComponentMeasure { - return bucket[componentKey]; -} - -export function addComponentChildren( - componentKey: string, - children: ComponentMeasure[], - total: number, - page: number, -): void { - const previous = getComponentChildren(componentKey); - if (previous) { - children = [...previous.children, ...children]; - } - childrenBucket[componentKey] = { children, total, page }; -} - -export function getComponentChildren(componentKey: string): { - children: ComponentMeasure[]; - page: number; - total: number; -} { - return childrenBucket[componentKey]; -} - -export function addComponentBreadcrumbs(componentKey: string, breadcrumbs: Breadcrumb[]): void { - breadcrumbsBucket[componentKey] = breadcrumbs; -} - -export function getComponentBreadcrumbs(componentKey: string): Breadcrumb[] { - return breadcrumbsBucket[componentKey]; -} - -export function clearBucket(): void { - bucket = {}; - childrenBucket = {}; - breadcrumbsBucket = {}; -} diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx index 72e81423955..46c6385554f 100644 --- a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx @@ -20,15 +20,20 @@ import * as React from 'react'; import { withRouter } from '~sonar-aligned/components/hoc/withRouter'; import { isPortfolioLike } from '~sonar-aligned/helpers/component'; -import { Breadcrumb, ComponentQualifier } from '~sonar-aligned/types/component'; +import { ComponentQualifier } from '~sonar-aligned/types/component'; import { Location, Router } from '~sonar-aligned/types/router'; import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import withMetricsContext from '../../../app/components/metrics/withMetricsContext'; import { CodeScope, getCodeUrl, getProjectUrl } from '../../../helpers/urls'; import { WithBranchLikesProps, useBranchesQuery } from '../../../queries/branch'; +import { + useComponentBreadcrumbsQuery, + useComponentChildrenQuery, + useComponentQuery, +} from '../../../queries/component'; +import { getBranchLikeQuery } from '../../../sonar-aligned/helpers/branch-like'; import { Component, ComponentMeasure, Dict, Metric } from '../../../types/types'; -import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket'; -import { loadMoreChildren, retrieveComponent, retrieveComponentChildren } from '../utils'; +import { getCodeMetrics } from '../utils'; import CodeAppRenderer from './CodeAppRenderer'; interface Props extends WithBranchLikesProps { @@ -38,191 +43,125 @@ interface Props extends WithBranchLikesProps { router: Router; } -interface State { - baseComponent?: ComponentMeasure; - breadcrumbs: Breadcrumb[]; - components?: ComponentMeasure[]; - highlighted?: ComponentMeasure; - loading: boolean; - newCodeSelected: boolean; - page: number; - searchResults?: ComponentMeasure[]; - sourceViewer?: ComponentMeasure; - total: number; -} - -class CodeApp extends React.Component { - mounted = false; - state: State; - - constructor(props: Props) { - super(props); - this.state = { - breadcrumbs: [], - loading: true, - newCodeSelected: true, - page: 0, - total: 0, - }; - } - - componentDidMount() { - this.mounted = true; - this.handleComponentChange(); - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.location.query.selected !== this.props.location.query.selected) { - this.handleUpdate(); - } - } - - componentWillUnmount() { - clearBucket(); - this.mounted = false; - } - - loadComponent = (componentKey: string) => { - this.setState({ loading: true }); - retrieveComponent( - componentKey, - this.props.component.qualifier, - this, - this.props.branchLike, - ).then((r) => { - if ( - [ComponentQualifier.File, ComponentQualifier.TestFile].includes( - r.component.qualifier as ComponentQualifier, - ) - ) { - this.setState({ - breadcrumbs: r.breadcrumbs, - components: r.components, - loading: false, - page: 0, - searchResults: undefined, - sourceViewer: r.component, - total: 0, - }); - } else { - this.setState({ - baseComponent: r.component, - breadcrumbs: r.breadcrumbs, - components: r.components, - loading: false, - page: r.page, - searchResults: undefined, - sourceViewer: undefined, - total: r.total, - }); - } - }, this.stopLoading); - }; - - stopLoading = () => { - this.setState({ loading: false }); - }; - - handleComponentChange = () => { - const { branchLike, component } = this.props; +const PAGE_SIZE = 100; + +function CodeApp(props: Readonly) { + const { component, metrics, router, location, branchLike } = props; + const [highlighted, setHighlighted] = React.useState(); + const [newCodeSelected, setNewCodeSelected] = React.useState(true); + const [searchResults, setSearchResults] = React.useState(); + + const { data: breadcrumbs, isLoading: isBreadcrumbsLoading } = useComponentBreadcrumbsQuery({ + component: location.query.selected ?? component.key, + ...getBranchLikeQuery(branchLike), + }); + const { data: baseComponent, isLoading: isBaseComponentLoading } = useComponentQuery( + { + component: location.query.selected ?? component.key, + metricKeys: getCodeMetrics(component.qualifier, branchLike).join(), + ...getBranchLikeQuery(branchLike), + }, + { + select: (data) => data.component, + }, + ); + const { + data: componentWithChildren, + isLoading: isChildrenLoading, + fetchNextPage, + } = useComponentChildrenQuery({ + strategy: 'children', + component: location.query.selected ?? component.key, + metrics: getCodeMetrics(component.qualifier, branchLike, { + includeQGStatus: true, + }), + additionalData: { + ps: PAGE_SIZE, + s: 'qualifier,name', + ...getBranchLikeQuery(branchLike), + }, + }); + + const isFile = baseComponent + ? [ComponentQualifier.File, ComponentQualifier.TestFile].includes( + baseComponent.qualifier as ComponentQualifier, + ) + : false; + const loading = isBreadcrumbsLoading || isBaseComponentLoading || isChildrenLoading; + const total = componentWithChildren?.pages[0]?.paging.total ?? 0; + const components = componentWithChildren?.pages.flatMap((page) => page.components); - // we already know component's breadcrumbs, - addComponentBreadcrumbs(component.key, component.breadcrumbs); + React.useEffect(() => { + setSearchResults(undefined); + }, [location.query.selected]); - this.setState({ loading: true }); - retrieveComponentChildren(component.key, component.qualifier, this, branchLike).then(() => { - addComponent(component); - this.handleUpdate(); - }, this.stopLoading); - }; - - handleLoadMore = () => { - const { baseComponent, components, page } = this.state; + const handleLoadMore = () => { if (!baseComponent || !components) { return; } - loadMoreChildren( - baseComponent.key, - page + 1, - this.props.component.qualifier, - this, - this.props.branchLike, - ).then((r) => { - if (r.components.length) { - this.setState({ - components: [...components, ...r.components], - page: r.page, - total: r.total, - }); - } - }, this.stopLoading); + fetchNextPage(); }; - handleGoToParent = () => { - const { branchLike, component } = this.props; - const { breadcrumbs = [] } = this.state; - - if (breadcrumbs.length > 1) { + const handleGoToParent = () => { + if (breadcrumbs && breadcrumbs.length > 1) { const parentComponent = breadcrumbs[breadcrumbs.length - 2]; - this.props.router.push(getCodeUrl(component.key, branchLike, parentComponent.key)); - this.setState({ highlighted: breadcrumbs[breadcrumbs.length - 1] }); + router.push(getCodeUrl(component.key, branchLike, parentComponent.key)); + setHighlighted(breadcrumbs[breadcrumbs.length - 1]); } }; - handleHighlight = (highlighted: ComponentMeasure) => { - this.setState({ highlighted }); + const handleHighlight = (highlighted: ComponentMeasure) => { + setHighlighted(highlighted); }; - handleSearchClear = () => { - this.setState({ searchResults: undefined }); + const handleSearchClear = () => { + setSearchResults(undefined); }; - handleSearchResults = (searchResults: ComponentMeasure[] = []) => { - this.setState({ searchResults }); + const handleSearchResults = (searchResults: ComponentMeasure[] = []) => { + setSearchResults(searchResults); }; - handleSelect = (component: ComponentMeasure) => { - const { branchLike, component: rootComponent } = this.props; - const { newCodeSelected } = this.state; - - if (component.refKey) { + const handleSelect = (selectedComponent: ComponentMeasure) => { + if (selectedComponent.refKey) { const codeType = newCodeSelected ? CodeScope.New : CodeScope.Overall; - const url = getProjectUrl(component.refKey, component.branch, codeType); - this.props.router.push(url); + const url = getProjectUrl(selectedComponent.refKey, selectedComponent.branch, codeType); + router.push(url); } else { - this.props.router.push(getCodeUrl(rootComponent.key, branchLike, component.key)); + router.push(getCodeUrl(component.key, branchLike, selectedComponent.key)); } - this.setState({ highlighted: undefined }); - }; - - handleSelectNewCode = (newCodeSelected: boolean) => { - this.setState({ newCodeSelected }); + setHighlighted(undefined); }; - handleUpdate = () => { - const { component, location } = this.props; - const { selected } = location.query; - const finalKey = selected || component.key; - - this.loadComponent(finalKey); + const handleSelectNewCode = (newCodeSelected: boolean) => { + setNewCodeSelected(newCodeSelected); }; - render() { - return ( - - ); - } + return ( + + ); } function withBranchLikes

( diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx index c85649dca92..96efad19434 100644 --- a/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx @@ -140,67 +140,67 @@ export default function CodeAppRenderer(props: Readonly) { )} - {!allComponentsHaveSoftwareQualityMeasures && ( - - )} + + {!allComponentsHaveSoftwareQualityMeasures && ( + + )} -

-
- {hasComponents && ( - - )} +
+
+ {hasComponents && ( + + )} - {!hasComponents && sourceViewer === undefined && ( -
- - {translate( - 'code_viewer.no_source_code_displayed_due_to_empty_analysis', - component.qualifier, - )} - -
- )} + {!hasComponents && sourceViewer === undefined && ( +
+ + {translate( + 'code_viewer.no_source_code_displayed_due_to_empty_analysis', + component.qualifier, + )} + +
+ )} - {showBreadcrumbs && ( - + {showBreadcrumbs && ( + + )} +
+ + {(showComponentList || showSearch) && ( +
+ + + +
)}
{(showComponentList || showSearch) && ( -
- - - -
- )} -
- - {(showComponentList || showSearch) && ( - - + {showComponentList && ( ) { selected={highlighted} /> )} - - - )} + + )} - {showComponentList && ( - - )} + {showComponentList && ( + + )} - {sourceViewer !== undefined && !showSearch && ( -
- -
- )} + {sourceViewer !== undefined && !showSearch && ( +
+ +
+ )} + ); } diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.tsx b/server/sonar-web/src/main/js/apps/code/components/Component.tsx index 30a28ac8c4b..133f2331c77 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Component.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/Component.tsx @@ -17,11 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Spinner } from '@sonarsource/echoes-react'; import { ContentCell, NumericalCell, TableRowInteractive } from 'design-system'; import * as React from 'react'; import { ComponentQualifier } from '~sonar-aligned/types/component'; import DateFromNow from '../../../components/intl/DateFromNow'; import { WorkspaceContext } from '../../../components/workspace/context'; +import { useComponentDataQuery } from '../../../queries/component'; import { BranchLike } from '../../../types/branch-like'; import { Metric, ComponentMeasure as TypeComponentMeasure } from '../../../types/types'; import ComponentMeasure from './ComponentMeasure'; @@ -61,6 +63,17 @@ export default function Component(props: Props) { component.qualifier === ComponentQualifier.File || component.qualifier === ComponentQualifier.TestFile; + const { data: analysisDate, isLoading } = useComponentDataQuery( + { + component: component.key, + branch: component.branch, + }, + { + enabled: showAnalysisDate && !isBaseComponent, + select: (data) => data.component.analysisDate, + }, + ); + return ( {canBePinned && ( @@ -96,8 +109,9 @@ export default function Component(props: Props) { {showAnalysisDate && ( - {!isBaseComponent && - (component.analysisDate ? : '—')} + + {!isBaseComponent && (analysisDate ? : '—')} + )} diff --git a/server/sonar-web/src/main/js/apps/code/utils.ts b/server/sonar-web/src/main/js/apps/code/utils.ts index 69ca61edc07..dfbccb985d4 100644 --- a/server/sonar-web/src/main/js/apps/code/utils.ts +++ b/server/sonar-web/src/main/js/apps/code/utils.ts @@ -17,22 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getBranchLikeQuery, isPullRequest } from '~sonar-aligned/helpers/branch-like'; +import { isPullRequest } from '~sonar-aligned/helpers/branch-like'; import { isPortfolioLike } from '~sonar-aligned/helpers/component'; -import { Breadcrumb, ComponentQualifier } from '~sonar-aligned/types/component'; +import { ComponentQualifier } from '~sonar-aligned/types/component'; import { MetricKey } from '~sonar-aligned/types/metrics'; -import { getBreadcrumbs, getChildren, getComponent, getComponentData } from '../../api/components'; import { CCT_SOFTWARE_QUALITY_METRICS, OLD_TAXONOMY_METRICS } from '../../helpers/constants'; import { BranchLike } from '../../types/branch-like'; -import { ComponentMeasure } from '../../types/types'; -import { - addComponent, - addComponentBreadcrumbs, - addComponentChildren, - getComponentBreadcrumbs, - getComponentChildren, - getComponent as getComponentFromBucket, -} from './bucket'; const METRICS = [ MetricKey.ncloc, @@ -47,19 +37,31 @@ const APPLICATION_METRICS = [MetricKey.alert_status, ...METRICS]; const PORTFOLIO_METRICS = [ MetricKey.releasability_rating, + MetricKey.releasability_rating_new, MetricKey.security_rating, + MetricKey.security_rating_new, + MetricKey.security_rating, + MetricKey.security_rating_new, MetricKey.reliability_rating, + MetricKey.reliability_rating_new, MetricKey.sqale_rating, + MetricKey.sqale_rating_new, MetricKey.security_review_rating, + MetricKey.security_review_rating_new, MetricKey.ncloc, ]; const NEW_PORTFOLIO_METRICS = [ MetricKey.releasability_rating, + MetricKey.releasability_rating_new, MetricKey.new_security_rating, + MetricKey.new_security_rating_new, MetricKey.new_reliability_rating, + MetricKey.new_reliability_rating_new, MetricKey.new_maintainability_rating, + MetricKey.new_maintainability_rating_new, MetricKey.new_security_review_rating, + MetricKey.new_security_review_rating_new, MetricKey.new_lines, ]; @@ -72,42 +74,6 @@ const LEAK_METRICS = [ MetricKey.new_duplicated_lines_density, ]; -const PAGE_SIZE = 100; - -interface Children { - components: ComponentMeasure[]; - page: number; - total: number; -} - -function prepareChildren(r: any): Children { - return { - components: r.components, - total: r.paging.total, - page: r.paging.pageIndex, - }; -} - -function skipRootDir(breadcrumbs: ComponentMeasure[]) { - return breadcrumbs.filter((component) => { - return !(component.qualifier === ComponentQualifier.Directory && component.name === '/'); - }); -} - -function storeChildrenBase(children: ComponentMeasure[]) { - children.forEach(addComponent); -} - -function storeChildrenBreadcrumbs(parentComponentKey: string, children: Breadcrumb[]) { - const parentBreadcrumbs = getComponentBreadcrumbs(parentComponentKey); - if (parentBreadcrumbs) { - children.forEach((child) => { - const breadcrumbs = [...parentBreadcrumbs, child]; - addComponentBreadcrumbs(child.key, breadcrumbs); - }); - } -} - export function getCodeMetrics( qualifier: string, branchLike?: BranchLike, @@ -133,162 +99,6 @@ export function getCodeMetrics( return [...METRICS]; } -function retrieveComponentBase( - componentKey: string, - qualifier: string, - instance: { mounted: boolean }, - branchLike?: BranchLike, -) { - const existing = getComponentFromBucket(componentKey); - if (existing) { - return Promise.resolve(existing); - } - - const metrics = getCodeMetrics(qualifier, branchLike); - - // eslint-disable-next-line local-rules/no-api-imports - return getComponent({ - component: componentKey, - metricKeys: metrics.join(), - ...getBranchLikeQuery(branchLike), - }).then(({ component }) => { - if (instance.mounted) { - addComponent(component); - } - return component; - }); -} - -export async function retrieveComponentChildren( - componentKey: string, - qualifier: string, - instance: { mounted: boolean }, - branchLike?: BranchLike, -): Promise<{ components: ComponentMeasure[]; page: number; total: number }> { - const existing = getComponentChildren(componentKey); - if (existing) { - return Promise.resolve({ - components: existing.children, - total: existing.total, - page: existing.page, - }); - } - - const metrics = getCodeMetrics(qualifier, branchLike, { - includeQGStatus: true, - }); - - // eslint-disable-next-line local-rules/no-api-imports - const result = await getChildren(componentKey, metrics, { - ps: PAGE_SIZE, - s: 'qualifier,name', - ...getBranchLikeQuery(branchLike), - }).then(prepareChildren); - - if (instance.mounted && isPortfolioLike(qualifier)) { - await Promise.all( - // eslint-disable-next-line local-rules/no-api-imports - result.components.map((c) => - getComponentData({ component: c.refKey ?? c.key, branch: c.branch }), - ), - ).then( - (data) => { - data.forEach(({ component: { analysisDate } }, i) => { - result.components[i].analysisDate = analysisDate; - }); - }, - () => { - // noop - }, - ); - } - - if (instance.mounted) { - addComponentChildren(componentKey, result.components, result.total, result.page); - storeChildrenBase(result.components); - storeChildrenBreadcrumbs(componentKey, result.components); - } - - return result; -} - -function retrieveComponentBreadcrumbs( - component: string, - instance: { mounted: boolean }, - branchLike?: BranchLike, -): Promise { - const existing = getComponentBreadcrumbs(component); - if (existing) { - return Promise.resolve(existing); - } - - // eslint-disable-next-line local-rules/no-api-imports - return getBreadcrumbs({ component, ...getBranchLikeQuery(branchLike) }) - .then(skipRootDir) - .then((breadcrumbs) => { - if (instance.mounted) { - addComponentBreadcrumbs(component, breadcrumbs); - } - return breadcrumbs; - }); -} - -export function retrieveComponent( - componentKey: string, - qualifier: string, - instance: { mounted: boolean }, - branchLike?: BranchLike, -): Promise<{ - breadcrumbs: Breadcrumb[]; - component: ComponentMeasure; - components: ComponentMeasure[]; - page: number; - total: number; -}> { - return Promise.all([ - retrieveComponentBase(componentKey, qualifier, instance, branchLike), - retrieveComponentChildren(componentKey, qualifier, instance, branchLike), - retrieveComponentBreadcrumbs(componentKey, instance, branchLike), - ]).then((r) => { - return { - breadcrumbs: r[2], - component: r[0], - components: r[1].components, - page: r[1].page, - total: r[1].total, - }; - }); -} - -export function loadMoreChildren( - componentKey: string, - page: number, - qualifier: string, - instance: { mounted: boolean }, - branchLike?: BranchLike, -): Promise { - const metrics = getCodeMetrics(qualifier, branchLike, { - includeQGStatus: true, - }); - - // eslint-disable-next-line local-rules/no-api-imports - return getChildren(componentKey, metrics, { - ps: PAGE_SIZE, - p: page, - s: 'qualifier,name', - ...getBranchLikeQuery(branchLike), - }) - .then(prepareChildren) - .then((r) => { - if (instance.mounted) { - addComponentChildren(componentKey, r.components, r.total, r.page); - storeChildrenBase(r.components); - storeChildrenBreadcrumbs(componentKey, r.components); - } - return r; - }); -} - export function mostCommonPrefix(strings: string[]) { const sortedStrings = strings.slice(0).sort((a, b) => a.localeCompare(b)); const firstString = sortedStrings[0]; 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 f8857e08eda..9728540e2e8 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 @@ -30,13 +30,6 @@ jest.mock('../../../api/components', () => ({ getScannableProjects: jest.fn().mockResolvedValue({ projects: [] }), })); -jest.mock('../../../api/measures', () => ({ - getMeasuresForProjects: jest.fn().mockResolvedValue([ - { component: 'foo', metric: 'new_coverage', period: { index: 1, value: '10' } }, - { component: 'bar', metric: 'languages', value: '20' }, - ]), -})); - describe('localizeSorting', () => { it('localizes default sorting', () => { expect(utils.localizeSorting()).toBe('projects.sort.name'); @@ -152,12 +145,6 @@ describe('fetchProjects', () => { 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' }; - } component.isScannable = false; return component; }, 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 7f9ad6e44d1..df733296b25 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 @@ -25,6 +25,7 @@ import { byLabelText, byRole, byText } from '~sonar-aligned/helpers/testSelector import { ComponentQualifier } from '~sonar-aligned/types/component'; import { MetricKey } from '~sonar-aligned/types/metrics'; import { ProjectsServiceMock } from '../../../../api/mocks/ProjectsServiceMock'; +import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock'; import { save } from '../../../../helpers/storage'; import { mockAppState, mockLoggedInUser } from '../../../../helpers/testMocks'; import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; @@ -62,10 +63,12 @@ jest.mock('../../../../helpers/storage', () => { const BASE_PATH = 'projects'; const projectHandler = new ProjectsServiceMock(); +const settingsHandler = new SettingsServiceMock(); beforeEach(() => { jest.clearAllMocks(); projectHandler.reset(); + settingsHandler.reset(); }); it('renders correctly', async () => { diff --git a/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx index 44fe864f3b7..3e22f1b4f35 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx @@ -21,6 +21,8 @@ import { screen } from '@testing-library/react'; import React from 'react'; import { ComponentQualifier, Visibility } from '~sonar-aligned/types/component'; import { MetricKey } from '~sonar-aligned/types/metrics'; +import { MeasuresServiceMock } from '../../../../../api/mocks/MeasuresServiceMock'; +import SettingsServiceMock from '../../../../../api/mocks/SettingsServiceMock'; import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks'; import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; import { CurrentUser } from '../../../../../types/users'; @@ -50,6 +52,14 @@ const PROJECT: Project = { const USER_LOGGED_OUT = mockCurrentUser(); const USER_LOGGED_IN = mockLoggedInUser(); +const settingsHandler = new SettingsServiceMock(); +const measuresHandler = new MeasuresServiceMock(); + +beforeEach(() => { + settingsHandler.reset(); + measuresHandler.reset(); +}); + it('should not display the quality gate', () => { const project = { ...PROJECT, analysisDate: undefined }; renderProjectCard(project); diff --git a/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCardMeasures-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCardMeasures-test.tsx index afbe8925074..dda98e4df78 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCardMeasures-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCardMeasures-test.tsx @@ -54,7 +54,6 @@ describe('Overall measures', () => { describe('New code measures', () => { it('should be rendered properly', () => { renderProjectCardMeasures({}, { isNewCode: true }); - expect(screen.getByLabelText(MetricKey.new_security_hotspots_reviewed)).toBeInTheDocument(); expect(screen.getByTitle('metric.new_violations.description')).toBeInTheDocument(); }); }); diff --git a/server/sonar-web/src/main/js/queries/common.ts b/server/sonar-web/src/main/js/queries/common.ts index fe56e4c8c38..203f173d423 100644 --- a/server/sonar-web/src/main/js/queries/common.ts +++ b/server/sonar-web/src/main/js/queries/common.ts @@ -134,3 +134,16 @@ export function createInfiniteQueryHook( >, ) => useInfiniteQuery({ ...fn(data), ...options }); } + +export enum StaleTime { + /** Use it when the data doesn't change during the user's session or the data doesn't need to be update-to-date in the UI. */ + NEVER = Infinity, + /** Use it when the data can change at any time because of user interactions or background tasks, and it's critical to reflect it live in the UI. */ + LIVE = 0, + /** Use it when the data changes often and you want to be able to see it refreshed quickly but it's critical to see it live. */ + SHORT = 10000, + /** Use it when the data rarely changes, anything bigger than 60s doesn't change much in term of network load or UX. */ + LONG = 60000, + /** Use it for ambiguous cases where you can't decide between {@link StaleTime.SHORT} or {@link StaleTime.LONG}. It should rarely be used. */ + MEDIUM = 30000, +} diff --git a/server/sonar-web/src/main/js/queries/component.ts b/server/sonar-web/src/main/js/queries/component.ts index fca3618a211..f0b4c6e6da7 100644 --- a/server/sonar-web/src/main/js/queries/component.ts +++ b/server/sonar-web/src/main/js/queries/component.ts @@ -17,12 +17,40 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { UseQueryResult, useQuery } from '@tanstack/react-query'; +import { + UseQueryResult, + infiniteQueryOptions, + queryOptions, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; +import { groupBy, omit } from 'lodash'; import { BranchParameters } from '~sonar-aligned/types/branch-like'; import { getTasksForComponent } from '../api/ce'; +import { + getBreadcrumbs, + getComponent, + getComponentData, + getComponentTree, +} from '../api/components'; import { getMeasuresWithMetrics } from '../api/measures'; +import { getNextPageParam, getPreviousPageParam } from '../helpers/react-query'; +import { MetricKey } from '../sonar-aligned/types/metrics'; import { MeasuresAndMetaWithMetrics } from '../types/measures'; -import { Component } from '../types/types'; +import { Component, Measure } from '../types/types'; +import { StaleTime, createInfiniteQueryHook, createQueryHook } from './common'; + +const NEW_METRICS = [ + MetricKey.sqale_rating_new, + MetricKey.security_rating_new, + MetricKey.reliability_rating_new, + MetricKey.security_review_rating_new, + MetricKey.releasability_rating_new, + MetricKey.new_security_rating_new, + MetricKey.new_reliability_rating_new, + MetricKey.new_maintainability_rating_new, + MetricKey.new_security_review_rating_new, +]; const TASK_RETRY = 10_000; @@ -31,12 +59,6 @@ type QueryKeyData = { metricKeys: string[]; }; -function getComponentQueryKey(key: string, type: 'tasks'): string[]; -function getComponentQueryKey(key: string, type: 'measures', data: QueryKeyData): string[]; -function getComponentQueryKey(key: string, type: string, data?: QueryKeyData): string[] { - return ['component', key, type, JSON.stringify(data)]; -} - function extractQueryKeyData(queryKey: string[]): { data?: QueryKeyData; key: string } { const [, key, , data] = queryKey; return { key, data: JSON.parse(data ?? 'null') }; @@ -44,7 +66,7 @@ function extractQueryKeyData(queryKey: string[]): { data?: QueryKeyData; key: st export function useTaskForComponentQuery(component: Component) { return useQuery({ - queryKey: getComponentQueryKey(component.key, 'tasks'), + queryKey: ['component', component.key, 'tasks'], queryFn: ({ queryKey }) => { const { key } = extractQueryKeyData(queryKey); return getTasksForComponent(key); @@ -61,13 +83,132 @@ export function useComponentMeasuresWithMetricsQuery( ): UseQueryResult { return useQuery({ enabled, - queryKey: getComponentQueryKey(key, 'measures', { - metricKeys, - branchParameters, - }), - queryFn: ({ queryKey }) => { - const { key, data } = extractQueryKeyData(queryKey); - return data && getMeasuresWithMetrics(key, data.metricKeys, data.branchParameters); + queryKey: [ + 'component', + key, + 'measures', + 'with_metrics', + { + metricKeys, + branchParameters, + }, + ] as const, + queryFn: ({ queryKey: [, key, , , data] }) => { + return ( + data && + getMeasuresWithMetrics( + key, + data.metricKeys.filter((m) => !NEW_METRICS.includes(m as MetricKey)), + data.branchParameters, + ) + ); }, }); } + +export const useComponentQuery = createQueryHook( + ({ component, metricKeys, ...params }: Parameters[0]) => { + const queryClient = useQueryClient(); + + return queryOptions({ + queryKey: ['component', component, 'measures', { metricKeys, params }], + queryFn: async () => { + const result = await getComponent({ + component, + metricKeys: metricKeys + .split(',') + .filter((m) => !NEW_METRICS.includes(m as MetricKey)) + .join(), + ...params, + }); + const measuresMapByMetricKey = groupBy(result.component.measures, 'metric'); + metricKeys.split(',').forEach((metricKey) => { + const measure = measuresMapByMetricKey[metricKey]?.[0] ?? null; + queryClient.setQueryData( + ['measures', 'details', result.component.key, metricKey], + measure, + ); + }); + return result; + }, + staleTime: StaleTime.LONG, + }); + }, +); + +export const useComponentBreadcrumbsQuery = createQueryHook( + ({ component, ...params }: Parameters[0]) => { + return queryOptions({ + queryKey: ['component', component, 'breadcrumbs', params], + queryFn: () => getBreadcrumbs({ component, ...params }), + staleTime: StaleTime.LONG, + }); + }, +); + +export const useComponentChildrenQuery = createInfiniteQueryHook( + ({ + strategy, + component, + metrics, + additionalData, + }: { + additionalData: Parameters[3]; + component: Parameters[1]; + metrics: Parameters[2]; + strategy: 'children' | 'leaves'; + }) => { + const queryClient = useQueryClient(); + return infiniteQueryOptions({ + queryKey: ['component', component, 'tree', strategy, { metrics, additionalData }], + queryFn: async ({ pageParam }) => { + const result = await getComponentTree( + strategy, + component, + metrics?.filter((m) => !NEW_METRICS.includes(m as MetricKey)), + { ...additionalData, p: pageParam }, + ); + const measuresMapByMetricKeyForBaseComponent = groupBy( + result.baseComponent.measures, + 'metric', + ); + metrics?.forEach((metricKey) => { + const measure = measuresMapByMetricKeyForBaseComponent[metricKey]?.[0] ?? null; + queryClient.setQueryData( + ['measures', 'details', result.baseComponent.key, metricKey], + measure, + ); + }); + + result.components.forEach((childComponent) => { + const measuresMapByMetricKeyForChildComponent = groupBy( + childComponent.measures, + 'metric', + ); + metrics?.forEach((metricKey) => { + const measure = measuresMapByMetricKeyForChildComponent[metricKey]?.[0] ?? null; + queryClient.setQueryData( + ['measures', 'details', childComponent.key, metricKey], + measure, + ); + }); + }); + return result; + }, + getNextPageParam: (data) => getNextPageParam({ page: data.paging }), + getPreviousPageParam: (data) => getPreviousPageParam({ page: data.paging }), + initialPageParam: 1, + staleTime: 60_000, + }); + }, +); + +export const useComponentDataQuery = createQueryHook( + (data: Parameters[0]) => { + return queryOptions({ + queryKey: ['component', data.component, 'component_data', omit(data, 'component')], + queryFn: () => getComponentData(data), + staleTime: StaleTime.LONG, + }); + }, +); diff --git a/server/sonar-web/src/main/js/queries/settings.ts b/server/sonar-web/src/main/js/queries/settings.ts index 6d3b2c02d4f..2afb4624194 100644 --- a/server/sonar-web/src/main/js/queries/settings.ts +++ b/server/sonar-web/src/main/js/queries/settings.ts @@ -48,7 +48,7 @@ export const useGetValueQuery = createQueryHook( export const useIsLegacyCCTMode = () => { return useGetValueQuery( - { key: 'sonar.old_world' }, + { key: 'sonar.legacy.ratings.mode.enabled' }, { staleTime: Infinity, select: (data) => !!data }, ); }; -- 2.39.5