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[] = [],
changeKey,
doesComponentExists,
getBreadcrumbs,
- getChildren,
+ getComponent,
getComponentData,
getComponentForSourceViewer,
getComponentLeaves,
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
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,
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);
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';
const componentsHandler = new ComponentsServiceMock();
const sourcesHandler = new SourcesServiceMock();
const issuesHandler = new IssuesServiceMock();
+const settingsHandler = new SettingsServiceMock();
const JUPYTER_ISSUE = {
issue: mockRawIssue(false, {
componentsHandler.reset();
sourcesHandler.reset();
issuesHandler.reset();
+ settingsHandler.reset();
});
it('should allow navigating through the tree', async () => {
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",
]
`;
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",
]
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",
]
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",
]
+++ /dev/null
-/*
- * 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);
-});
* 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', () => {
});
});
-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/');
+++ /dev/null
-/*
- * 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 = {};
-}
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 {
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 }>(
</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}
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>
);
}
* 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';
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 && (
{showAnalysisDate && (
<NumericalCell className="sw-whitespace-nowrap">
- {!isBaseComponent &&
- (component.analysisDate ? <DateFromNow date={component.analysisDate} /> : '—')}
+ <Spinner isLoading={isLoading}>
+ {!isBaseComponent && (analysisDate ? <DateFromNow date={analysisDate} /> : '—')}
+ </Spinner>
</NumericalCell>
)}
</TableRowInteractive>
* 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,
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,
];
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,
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];
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');
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;
},
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';
const BASE_PATH = 'projects';
const projectHandler = new ProjectsServiceMock();
+const settingsHandler = new SettingsServiceMock();
beforeEach(() => {
jest.clearAllMocks();
projectHandler.reset();
+ settingsHandler.reset();
});
it('renders correctly', async () => {
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';
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);
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();
});
});
>,
) => 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,
+}
* 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;
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') };
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);
): 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,
+ });
+ },
+);
export const useIsLegacyCCTMode = () => {
return useGetValueQuery(
- { key: 'sonar.old_world' },
+ { key: 'sonar.legacy.ratings.mode.enabled' },
{ staleTime: Infinity, select: (data) => !!data },
);
};