]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22727 Portfolio breakdown page
authorViktor Vorona <viktor.vorona@sonarsource.com>
Thu, 8 Aug 2024 15:33:02 +0000 (17:33 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Aug 2024 20:03:05 +0000 (20:03 +0000)
18 files changed:
server/sonar-web/src/main/js/api/components.ts
server/sonar-web/src/main/js/api/mocks/ComponentsServiceMock.ts
server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
server/sonar-web/src/main/js/apps/code/__tests__/__snapshots__/utils-test.tsx.snap
server/sonar-web/src/main/js/apps/code/__tests__/buckets-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/code/__tests__/utils-test.tsx
server/sonar-web/src/main/js/apps/code/bucket.ts [deleted file]
server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
server/sonar-web/src/main/js/apps/code/components/Component.tsx
server/sonar-web/src/main/js/apps/code/utils.ts
server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCard-test.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCardMeasures-test.tsx
server/sonar-web/src/main/js/queries/common.ts
server/sonar-web/src/main/js/queries/component.ts
server/sonar-web/src/main/js/queries/settings.ts

index 4ef3576903013e4c1e828c15f63283d198bccebb..654b004e92f44c8a914af63dedeee1f0b479d7b1 100644 (file)
@@ -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[] = [],
index 0e55f044311a173057f1f62ed2c5a4d6b70af1b4..0a322db27d6f5714fdfb4b577c10b43a15545436 100644 (file)
@@ -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);
index 0f59e34a416525eb899a698c51f7fe94d5b572da..b0bdc8c17f66421e4cc7f148da091465db7753ab 100644 (file)
@@ -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 () => {
index 016ff06d553db5f50dc1dbaaf90ef92b5603f08d..5ada596b66332fc8286b4f54c2f903cd9a7c4880 100644 (file)
@@ -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 (file)
index 2357e87..0000000
+++ /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);
-});
index 094c0effa0834b7b075d35e0deb577e07d0ec882..c2c19e55ce490bfadbe8b042169a90813967078c 100644 (file)
  * 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 (file)
index 9a3827b..0000000
+++ /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<ComponentMeasure> = {};
-let childrenBucket: Dict<{
-  children: ComponentMeasure[];
-  page: number;
-  total: number;
-}> = {};
-let breadcrumbsBucket: Dict<Breadcrumb[]> = {};
-
-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 = {};
-}
index 72e81423955fa9474a565ec7dbd4a683b39c552a..46c6385554f3d171fb97ff469bbb27980bd57772 100644 (file)
 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<Props, State> {
-  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<Props>) {
+  const { component, metrics, router, location, branchLike } = props;
+  const [highlighted, setHighlighted] = React.useState<ComponentMeasure | undefined>();
+  const [newCodeSelected, setNewCodeSelected] = React.useState<boolean>(true);
+  const [searchResults, setSearchResults] = React.useState<ComponentMeasure[] | undefined>();
+
+  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 (
-      <CodeAppRenderer
-        {...this.props}
-        {...this.state}
-        handleGoToParent={this.handleGoToParent}
-        handleHighlight={this.handleHighlight}
-        handleLoadMore={this.handleLoadMore}
-        handleSearchClear={this.handleSearchClear}
-        handleSearchResults={this.handleSearchResults}
-        handleSelect={this.handleSelect}
-        handleSelectNewCode={this.handleSelectNewCode}
-      />
-    );
-  }
+  return (
+    <CodeAppRenderer
+      location={location}
+      metrics={metrics}
+      branchLike={branchLike}
+      component={component}
+      baseComponent={isFile ? undefined : baseComponent}
+      breadcrumbs={breadcrumbs ?? []}
+      components={components}
+      highlighted={highlighted}
+      loading={loading}
+      newCodeSelected={newCodeSelected}
+      searchResults={searchResults}
+      sourceViewer={isFile ? baseComponent : undefined}
+      total={total}
+      handleGoToParent={handleGoToParent}
+      handleHighlight={handleHighlight}
+      handleLoadMore={handleLoadMore}
+      handleSearchClear={handleSearchClear}
+      handleSearchResults={handleSearchResults}
+      handleSelect={handleSelect}
+      handleSelectNewCode={handleSelectNewCode}
+    />
+  );
 }
 
 function withBranchLikes<P extends { component?: Component }>(
index c85649dca926b9cf06324fb27ef832dbde46b742..96efad194340118ab5c8405611096879175fcb6a 100644 (file)
@@ -140,67 +140,67 @@ export default function CodeAppRenderer(props: Readonly<Props>) {
         </FlagMessage>
       )}
 
-      {!allComponentsHaveSoftwareQualityMeasures && (
-        <AnalysisMissingInfoMessage
-          qualifier={component.qualifier}
-          hide={isPortfolio}
-          className="sw-mb-4"
-        />
-      )}
+      <Spinner isLoading={loading}>
+        {!allComponentsHaveSoftwareQualityMeasures && (
+          <AnalysisMissingInfoMessage
+            qualifier={component.qualifier}
+            hide={isPortfolio}
+            className="sw-mb-4"
+          />
+        )}
 
-      <div className="sw-flex sw-justify-between">
-        <div>
-          {hasComponents && (
-            <Search
-              branchLike={branchLike}
-              className="sw-mb-4"
-              component={component}
-              newCodeSelected={newCodeSelected}
-              onNewCodeToggle={props.handleSelectNewCode}
-              onSearchClear={props.handleSearchClear}
-              onSearchResults={props.handleSearchResults}
-            />
-          )}
+        <div className="sw-flex sw-justify-between">
+          <div>
+            {hasComponents && (
+              <Search
+                branchLike={branchLike}
+                className="sw-mb-4"
+                component={component}
+                newCodeSelected={newCodeSelected}
+                onNewCodeToggle={props.handleSelectNewCode}
+                onSearchClear={props.handleSearchClear}
+                onSearchResults={props.handleSearchResults}
+              />
+            )}
 
-          {!hasComponents && sourceViewer === undefined && (
-            <div className="sw-flex sw-align-center sw-flex-col sw-fixed sw-top-1/2">
-              <LightLabel>
-                {translate(
-                  'code_viewer.no_source_code_displayed_due_to_empty_analysis',
-                  component.qualifier,
-                )}
-              </LightLabel>
-            </div>
-          )}
+            {!hasComponents && sourceViewer === undefined && (
+              <div className="sw-flex sw-align-center sw-flex-col sw-fixed sw-top-1/2">
+                <LightLabel>
+                  {translate(
+                    'code_viewer.no_source_code_displayed_due_to_empty_analysis',
+                    component.qualifier,
+                  )}
+                </LightLabel>
+              </div>
+            )}
 
-          {showBreadcrumbs && (
-            <CodeBreadcrumbs
-              branchLike={branchLike}
-              breadcrumbs={breadcrumbs}
-              rootComponent={component}
-            />
+            {showBreadcrumbs && (
+              <CodeBreadcrumbs
+                branchLike={branchLike}
+                breadcrumbs={breadcrumbs}
+                rootComponent={component}
+              />
+            )}
+          </div>
+
+          {(showComponentList || showSearch) && (
+            <div className="sw-flex sw-items-end sw-body-sm">
+              <KeyboardHint
+                className="sw-mr-4 sw-ml-6"
+                command={`${KeyboardKeys.DownArrow} ${KeyboardKeys.UpArrow}`}
+                title={translate('component_measures.select_files')}
+              />
+
+              <KeyboardHint
+                command={`${KeyboardKeys.LeftArrow} ${KeyboardKeys.RightArrow}`}
+                title={translate('component_measures.navigate')}
+              />
+            </div>
           )}
         </div>
 
         {(showComponentList || showSearch) && (
-          <div className="sw-flex sw-items-end sw-body-sm">
-            <KeyboardHint
-              className="sw-mr-4 sw-ml-6"
-              command={`${KeyboardKeys.DownArrow} ${KeyboardKeys.UpArrow}`}
-              title={translate('component_measures.select_files')}
-            />
-
-            <KeyboardHint
-              command={`${KeyboardKeys.LeftArrow} ${KeyboardKeys.RightArrow}`}
-              title={translate('component_measures.navigate')}
-            />
-          </div>
-        )}
-      </div>
-
-      {(showComponentList || showSearch) && (
-        <Card className="sw-mt-2 sw-overflow-auto">
-          <Spinner isLoading={loading}>
+          <Card className="sw-mt-2 sw-overflow-auto">
             {showComponentList && (
               <Components
                 baseComponent={baseComponent}
@@ -230,26 +230,26 @@ export default function CodeAppRenderer(props: Readonly<Props>) {
                 selected={highlighted}
               />
             )}
-          </Spinner>
-        </Card>
-      )}
+          </Card>
+        )}
 
-      {showComponentList && (
-        <ListFooter count={components.length} loadMore={props.handleLoadMore} total={total} />
-      )}
+        {showComponentList && (
+          <ListFooter count={components.length} loadMore={props.handleLoadMore} total={total} />
+        )}
 
-      {sourceViewer !== undefined && !showSearch && (
-        <div className="sw-mt-2">
-          <SourceViewerWrapper
-            branchLike={branchLike}
-            component={sourceViewer.key}
-            componentMeasures={sourceViewer.measures}
-            isFile
-            location={location}
-            onGoToParent={props.handleGoToParent}
-          />
-        </div>
-      )}
+        {sourceViewer !== undefined && !showSearch && (
+          <div className="sw-mt-2">
+            <SourceViewerWrapper
+              branchLike={branchLike}
+              component={sourceViewer.key}
+              componentMeasures={sourceViewer.measures}
+              isFile
+              location={location}
+              onGoToParent={props.handleGoToParent}
+            />
+          </div>
+        )}
+      </Spinner>
     </LargeCenteredLayout>
   );
 }
index 30a28ac8c4b73213609a9dc8970c4fe8cc396d93..133f2331c77b8a7745e9a6f1cb486006bffaa067 100644 (file)
  * 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 (
     <TableRowInteractive selected={selected} aria-label={component.name}>
       {canBePinned && (
@@ -96,8 +109,9 @@ export default function Component(props: Props) {
 
       {showAnalysisDate && (
         <NumericalCell className="sw-whitespace-nowrap">
-          {!isBaseComponent &&
-            (component.analysisDate ? <DateFromNow date={component.analysisDate} /> : '—')}
+          <Spinner isLoading={isLoading}>
+            {!isBaseComponent && (analysisDate ? <DateFromNow date={analysisDate} /> : '—')}
+          </Spinner>
         </NumericalCell>
       )}
     </TableRowInteractive>
index 69ca61edc07ee37f723e26fb6fd983a33d7351ed..dfbccb985d4d8aa24dc9c5e61e7c30bb0681cb3f 100644 (file)
  * 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<Breadcrumb[]> {
-  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<Children> {
-  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];
index f8857e08eda412755ffa63188bf9c190f95d9c39..9728540e2e892f3122cd5452ae6a311496537a31 100644 (file)
@@ -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;
           },
index 7f9ad6e44d1e02aaf333547026b5c38f197936ed..df733296b250f5e21ae52bcc343e7d0fa6439682 100644 (file)
@@ -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 () => {
index 44fe864f3b72b153b8ae18e92e15e028867a68a1..3e22f1b4f3524c5980783473d694296d74400cc1 100644 (file)
@@ -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);
index afbe892507480d522269568d20fe77f687dc4eb3..dda98e4df780eff0b9343b1160a87bd3837d9129 100644 (file)
@@ -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();
   });
 });
index fe56e4c8c3822ff1889a913b21aaf7d565f07082..203f173d4231c4460a4aeb6f4d7b2eedcbda3ff0 100644 (file)
@@ -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,
+}
index fca3618a211f3693be792c0ad8a0e11da2b0eac6..f0b4c6e6da78faf233aa7c23b9d343db744a49cf 100644 (file)
  * 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<MeasuresAndMetaWithMetrics> {
   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<typeof getComponent>[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<Measure>(
+            ['measures', 'details', result.component.key, metricKey],
+            measure,
+          );
+        });
+        return result;
+      },
+      staleTime: StaleTime.LONG,
+    });
+  },
+);
+
+export const useComponentBreadcrumbsQuery = createQueryHook(
+  ({ component, ...params }: Parameters<typeof getBreadcrumbs>[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<typeof getComponentTree>[3];
+    component: Parameters<typeof getComponentTree>[1];
+    metrics: Parameters<typeof getComponentTree>[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<Measure>(
+            ['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<Measure>(
+              ['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<typeof getComponentData>[0]) => {
+    return queryOptions({
+      queryKey: ['component', data.component, 'component_data', omit(data, 'component')],
+      queryFn: () => getComponentData(data),
+      staleTime: StaleTime.LONG,
+    });
+  },
+);
index 6d3b2c02d4f753b9c38297981414f62cebce8ea0..2afb4624194081614faf9fb78520a603a9802708 100644 (file)
@@ -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 },
   );
 };