* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import classNames from 'classnames';
import { max, min } from 'd3-array';
import { themeColor, themeContrast } from '../helpers';
import { ButtonSecondary } from '../sonar-aligned/components/buttons';
import { Note } from '../sonar-aligned/components/typography';
-import { BubbleColorVal } from '../types/charts';
import { Tooltip } from './Tooltip';
const TICKS_COUNT = 5;
interface BubbleItem<T> {
- color?: BubbleColorVal;
+ backgroundColor?: string;
+ borderColor?: string;
data?: T;
key?: string;
size: number;
const bubbles = sortBy(items, (b) => -b.size).map((item, index) => {
return (
<Bubble
- color={item.color}
+ backgroundColor={item.backgroundColor}
+ borderColor={item.borderColor}
data={item.data}
key={item.key ?? index}
onClick={props.onBubbleClick}
}
interface BubbleProps<T> {
- color?: BubbleColorVal;
+ backgroundColor?: string;
+ borderColor?: string;
data?: T;
onClick?: (ref?: T) => void;
r: number;
}
function Bubble<T>(props: BubbleProps<T>) {
- const theme = useTheme();
- const { color, data, onClick, r, scale, tooltip, x, y } = props;
+ const { backgroundColor, borderColor, data, onClick, r, scale, tooltip, x, y } = props;
const handleClick = React.useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
event.stopPropagation();
<BubbleStyled
r={r}
style={{
- fill: color ? themeColor(`bubble.${color}`)({ theme }) : '',
- stroke: color ? themeContrast(`bubble.${color}`)({ theme }) : '',
+ fill: backgroundColor ?? '',
+ stroke: borderColor ?? '',
}}
transform={`translate(${x}, ${y}) scale(${scale})`}
/>
import styled from '@emotion/styled';
import tw from 'twin.macro';
import { themeBorder, themeColor, themeContrast } from '../helpers';
-import { BubbleColorVal } from '../types/charts';
+import { BubbleColorVal } from '../types';
import { Tooltip } from './Tooltip';
import { Checkbox } from './input/Checkbox';
color.selected
? {
backgroundColor:
- color.borderColor ??
- themeColor(`bubble.${(idx + 1) as BubbleColorVal}`)({ theme }),
- borderColor:
color.backgroundColor ??
- themeContrast(`bubble.${(idx + 1) as BubbleColorVal}`)({ theme }),
+ themeColor(`bubble.legacy.${(idx + 1) as BubbleColorVal}`)({
+ theme,
+ }),
+ borderColor:
+ color.borderColor ??
+ themeContrast(`bubble.legacy.${(idx + 1) as BubbleColorVal}`)({
+ theme,
+ }),
}
: {}
}
// bubble charts
bubbleChartLine: COLORS.grey[50],
bubbleDefault: [...COLORS.blue[500], 0.3],
+ 'bubble.legacy.1': [...COLORS.green[500], 0.3],
+ 'bubble.legacy.2': [...COLORS.yellowGreen[500], 0.3],
+ 'bubble.legacy.3': [...COLORS.yellow[500], 0.3],
+ 'bubble.legacy.4': [...COLORS.orange[500], 0.3],
+ 'bubble.legacy.5': [...COLORS.red[500], 0.3],
+
'bubble.1': [...COLORS.green[500], 0.3],
'bubble.2': [...COLORS.yellowGreen[500], 0.3],
'bubble.3': [...COLORS.yellow[500], 0.3],
- 'bubble.4': [...COLORS.orange[500], 0.3],
+ 'bubble.4': [...COLORS.red[500], 0.3],
'bubble.5': [...COLORS.red[500], 0.3],
// TreeMap Colors
+ 'treeMap.legacy.A': COLORS.green[500],
+ 'treeMap.legacy.B': COLORS.yellowGreen[500],
+ 'treeMap.legacy.C': COLORS.yellow[500],
+ 'treeMap.legacy.D': COLORS.orange[500],
+ 'treeMap.legacy.E': COLORS.red[500],
+
'treeMap.A': COLORS.green[500],
'treeMap.B': COLORS.yellowGreen[500],
'treeMap.C': COLORS.yellow[500],
- 'treeMap.D': COLORS.orange[500],
+ 'treeMap.D': COLORS.red[500],
'treeMap.E': COLORS.red[500],
+
'treeMap.NA1': COLORS.blueGrey[300],
'treeMap.NA2': COLORS.blueGrey[200],
treeMapCellTextColor: COLORS.blueGrey[900],
// bubble charts
bubbleDefault: COLORS.blue[500],
+ 'bubble.legacy.1': COLORS.green[500],
+ 'bubble.legacy.2': COLORS.yellowGreen[500],
+ 'bubble.legacy.3': COLORS.yellow[500],
+ 'bubble.legacy.4': COLORS.orange[500],
+ 'bubble.legacy.5': COLORS.red[500],
+
'bubble.1': COLORS.green[500],
'bubble.2': COLORS.yellowGreen[500],
'bubble.3': COLORS.yellow[500],
- 'bubble.4': COLORS.orange[500],
+ 'bubble.4': COLORS.red[500],
'bubble.5': COLORS.red[500],
// news bar
return getJSON(url, data).catch(throwGlobalError);
}
-export function getComponentLeaves(
- component: string,
- metrics: string[] = [],
- additional: RequestData = {},
-) {
- return getComponentTree('leaves', component, metrics, additional);
-}
-
export function getComponent(
data: { component: string; metricKeys: string } & BranchParameters,
): Promise<{ component: ComponentMeasure }> {
return getJSON(COMPONENT_URL, data).then((r) => r.component.measures, throwGlobalError);
}
-export function getMeasuresWithMetrics(
- component: string,
- metrics: string[],
- branchParameters?: BranchParameters,
-): Promise<MeasuresAndMetaWithMetrics> {
- return getJSON(COMPONENT_URL, {
- additionalFields: 'metrics',
- component,
- metricKeys: metrics.join(','),
- ...branchParameters,
- }).catch(throwGlobalError);
-}
-
-export function getMeasuresWithPeriod(
- component: string,
- metrics: string[],
- branchParameters?: BranchParameters,
-): Promise<MeasuresAndMetaWithPeriod> {
- return getJSON(COMPONENT_URL, {
- additionalFields: 'period',
- component,
- metricKeys: metrics.join(','),
- ...branchParameters,
- }).catch(throwGlobalError);
-}
-
export function getMeasuresWithPeriodAndMetrics(
component: string,
metrics: string[],
getComponent,
getComponentData,
getComponentForSourceViewer,
- getComponentLeaves,
getComponentTree,
getDuplications,
getSources,
jest.mocked(getDuplications).mockImplementation(this.handleGetDuplications);
jest.mocked(getSources).mockImplementation(this.handleGetSources);
jest.mocked(changeKey).mockImplementation(this.handleChangeKey);
- jest.mocked(getComponentLeaves).mockImplementation(this.handleGetComponentLeaves);
jest.mocked(getBreadcrumbs).mockImplementation(this.handleGetBreadcrumbs);
jest.mocked(setProjectTags).mockImplementation(this.handleSetProjectTags);
jest.mocked(setApplicationTags).mockImplementation(this.handleSetApplicationTags);
return Promise.reject({ status: 404, message: 'Component not found' });
};
- handleGetComponentLeaves = (
- component: string,
- metrics: string[] = [],
- data: RequestData = {},
- ): Promise<{
- baseComponent: ComponentMeasure;
- components: ComponentMeasure[];
- metrics: Metric[];
- paging: Paging;
- }> => {
- return this.handleGetComponentTree('leaves', component, metrics, data);
- };
-
handleGetBreadcrumbs = ({ component: key }: { component: string } & BranchParameters) => {
const base = this.findComponentTree(key);
if (base === undefined) {
import { MetricKey } from '~sonar-aligned/types/metrics';
import { mockMetric, mockPeriod } from '../../helpers/testMocks';
import { Metric, Period } from '../../types/types';
-import { getMeasures, getMeasuresWithPeriod, getMeasuresWithPeriodAndMetrics } from '../measures';
+import { getMeasures, getMeasuresWithPeriodAndMetrics } from '../measures';
import { ComponentTree, mockFullComponentTree } from './data/components';
import { mockIssuesList } from './data/issues';
import { MeasureRecords, getMetricTypeFromKey, mockFullMeasureData } from './data/measures';
};
jest.mocked(getMeasures).mockImplementation(this.handleGetMeasures);
- jest.mocked(getMeasuresWithPeriod).mockImplementation(this.handleGetMeasuresWithPeriod);
jest
.mocked(getMeasuresWithPeriodAndMetrics)
.mockImplementation(this.handleGetMeasuresWithPeriodAndMetrics);
return this.reply(measures);
};
- handleGetMeasuresWithPeriod = (
- component: string,
- metrics: string[],
- _branchParameters?: BranchParameters,
- ) => {
- const entry = this.findComponentTree(component);
- const measures = this.filterMeasures(entry.component.key, metrics);
-
- return this.reply({
- component: {
- ...entry.component,
- measures,
- },
- period: this.#period,
- });
- };
-
handleGetMeasuresWithPeriodAndMetrics = (componentKey: string, metricKeys: string[]) => {
const { component } = this.findComponentTree(componentKey);
const measures = this.filterMeasures(component.key, metricKeys);
key: SettingsKey.QPAdminCanDisableInheritedRules,
value: 'true',
},
+ {
+ key: 'sonar.old_world',
+ value: 'false',
+ },
];
#settingValues: SettingValue[] = cloneDeep(this.#defaultValues);
import { isDiffMetric } from '../../../helpers/measures';
import { useMeasureQuery } from '../../../queries/measures';
import { useIsLegacyCCTMode } from '../../../queries/settings';
+import { BranchLike } from '../../../types/branch-like';
interface Props {
+ branchLike?: BranchLike;
className?: string;
componentKey: string;
getLabel?: (rating: RatingEnum) => string;
| MetricKey.security_review_rating
| MetricKey.releasability_rating;
+function isNewRatingMetric(metricKey: MetricKey) {
+ return metricKey.includes('_new');
+}
+
const useGetMetricKeyForRating = (ratingMetric: RatingMetricKeys): MetricKey | null => {
const { data: isLegacy, isLoading } = useIsLegacyCCTMode();
+
+ if (isNewRatingMetric(ratingMetric)) {
+ return ratingMetric;
+ }
+
if (isLoading) {
return null;
}
};
export default function RatingComponent(props: Readonly<Props>) {
- const { componentKey, ratingMetric, size, className, getLabel, getTooltip } = props;
+ const { componentKey, ratingMetric, size, className, getLabel, branchLike, getTooltip } = props;
+
const metricKey = useGetMetricKeyForRating(ratingMetric as RatingMetricKeys);
const { data: isLegacy } = useIsLegacyCCTMode();
const { data: targetMeasure, isLoading: isLoadingTargetMeasure } = useMeasureQuery(
- { componentKey, metricKey: metricKey ?? '' },
+ { componentKey, metricKey: metricKey ?? '', branchLike },
{ enabled: !!metricKey },
);
+
const { data: oldMeasure, isLoading: isLoadingOldMeasure } = useMeasureQuery(
- { componentKey, metricKey: ratingMetric },
- { enabled: !isLegacy && targetMeasure === null },
+ { componentKey, metricKey: ratingMetric, branchLike },
+ { enabled: !isLegacy && !isNewRatingMetric(ratingMetric) && targetMeasure === null },
);
const isLoading = isLoadingTargetMeasure || isLoadingOldMeasure;
</ContentCell>
{metrics.map((metric) => (
- <ComponentMeasure component={component} key={metric.key} metric={metric} />
+ <ComponentMeasure
+ component={component}
+ branchLike={branchLike}
+ key={metric.key}
+ metric={metric}
+ />
))}
{showAnalysisDate && (
areCCTMeasuresComputed as areCCTMeasuresComputedFn,
isDiffMetric,
} from '../../../helpers/measures';
+import { BranchLike } from '../../../types/branch-like';
import { isApplication, isProject } from '../../../types/component';
import { Metric, ComponentMeasure as TypeComponentMeasure } from '../../../types/types';
interface Props {
+ branchLike?: BranchLike;
component: TypeComponentMeasure;
metric: Metric;
}
export default function ComponentMeasure(props: Props) {
- const { component, metric } = props;
+ const { component, metric, branchLike } = props;
const isProjectLike = isProject(component.qualifier) || isApplication(component.qualifier);
const isReleasability = metric.key === MetricKey.releasability_rating;
case MetricType.Rating:
return (
<RatingCell className="sw-whitespace-nowrap">
- <RatingComponent componentKey={component.key} ratingMetric={metric.key as MetricKey} />
+ <RatingComponent
+ branchLike={branchLike}
+ componentKey={component.key}
+ ratingMetric={metric.key as MetricKey}
+ />
</RatingCell>
);
default:
return (
<NumericalCell className="sw-whitespace-nowrap">
<Measure
+ branchLike={branchLike}
componentKey={component.key}
metricKey={finalMetricKey}
metricType={finalMetricType}
import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
import { MeasuresServiceMock } from '../../../api/mocks/MeasuresServiceMock';
+import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import { mockComponent } from '../../../helpers/mocks/component';
import { mockMeasure, mockMetric } from '../../../helpers/testMocks';
import { renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
const measuresHandler = new MeasuresServiceMock();
const issuesHandler = new IssuesServiceMock();
const branchHandler = new BranchesServiceMock();
+const settingsHandler = new SettingsServiceMock();
afterEach(() => {
componentsHandler.reset();
measuresHandler.reset();
issuesHandler.reset();
branchHandler.reset();
+ settingsHandler.reset();
});
describe('rendering', () => {
it('should correctly render the default overview and navigation', async () => {
const { ui, user } = getPageObject();
renderMeasuresApp();
- await ui.appLoaded();
// Overview.
- expect(ui.seeDataAsListLink.get()).toBeInTheDocument();
+ expect(await ui.seeDataAsListLink.find()).toBeInTheDocument();
expect(ui.overviewDomainLink.get()).toHaveAttribute('aria-current', 'true');
expect(ui.bubbleChart.get()).toBeInTheDocument();
expect(within(ui.bubbleChart.get()).getAllByRole('link')).toHaveLength(8);
'component_measures.metric.new_maintainability_issues.name 5',
'Added Technical Debt work_duration.x_minutes.1',
'Technical Debt Ratio on New Code 1.0%',
- 'Maintainability Rating on New Code metric.has_rating_X.E',
+ 'Maintainability Rating on New Code metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
'component_measures.metric.maintainability_issues.name 2',
'Technical Debt work_duration.x_minutes.1',
'Technical Debt Ratio 1.0%',
- 'Maintainability Rating metric.has_rating_X.E',
+ 'Maintainability Rating metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
'Effort to Reach Maintainability Rating A work_duration.x_minutes.1',
].forEach((measure) => {
expect(ui.measureLink(measure).get()).toBeInTheDocument();
'component_measures.metric.new_code_smells.name 9',
'Added Technical Debt work_duration.x_minutes.1',
'Technical Debt Ratio on New Code 1.0%',
- 'Maintainability Rating on New Code metric.has_rating_X.E',
+ 'Maintainability Rating on New Code metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
'component_measures.metric.code_smells.name 9',
'Technical Debt work_duration.x_minutes.1',
'Technical Debt Ratio 1.0%',
- 'Maintainability Rating metric.has_rating_X.E',
+ 'Maintainability Rating metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
'Effort to Reach Maintainability Rating A work_duration.x_minutes.1',
].forEach((measure) => {
expect(ui.measureLink(measure).get()).toBeInTheDocument();
it('should correctly render a list view', async () => {
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=code_smells&view=list');
- await ui.appLoaded();
- expect(ui.measuresTable.get()).toBeInTheDocument();
+ expect(await ui.measuresTable.find()).toBeInTheDocument();
expect(ui.measuresRows.getAll()).toHaveLength(8);
});
it('should correctly render a tree view', async () => {
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=code_smells&view=tree');
- await ui.appLoaded();
- expect(ui.measuresTable.get()).toBeInTheDocument();
+ expect(await ui.measuresTable.find()).toBeInTheDocument();
expect(ui.measuresRows.getAll()).toHaveLength(7);
});
it('should correctly render a rating treemap view', async () => {
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=sqale_rating&view=treemap');
- await ui.appLoaded();
- expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7);
+ await waitFor(() => {
+ expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7);
+ });
expect(ui.treeMapCell(/folderA .+ Maintainability Rating: C/).get()).toBeInTheDocument();
expect(ui.treeMapCell(/test1\.js .+ Maintainability Rating: B/).get()).toBeInTheDocument();
expect(ui.treeMapCell(/index\.tsx .+ Maintainability Rating: A/).get()).toBeInTheDocument();
const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=coverage&view=treemap');
- await ui.appLoaded();
- expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7);
+ await waitFor(() => {
+ expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7);
+ });
expect(ui.treeMapCell(/folderA .+ Coverage: 74.2%/).get()).toBeInTheDocument();
expect(ui.treeMapCell(/test1\.js .+ Coverage: —/).get()).toBeInTheDocument();
});
it('should correctly render the language distribution', async () => {
- const { ui } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=ncloc');
- await ui.appLoaded();
- expect(screen.getByText('10short_number_suffix.k')).toBeInTheDocument();
+ expect(await screen.findByText('10short_number_suffix.k')).toBeInTheDocument();
expect(screen.getByText('java')).toBeInTheDocument();
expect(screen.getByText('5short_number_suffix.k')).toBeInTheDocument();
expect(screen.getByText('javascript')).toBeInTheDocument();
const { ui, user } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=sqale_rating&view=list');
- await ui.appLoaded();
- expect(ui.notShowingAllComponentsTxt.get()).toBeInTheDocument();
+ expect(await ui.notShowingAllComponentsTxt.find()).toBeInTheDocument();
await user.click(ui.showAllBtn.get());
expect(ui.notShowingAllComponentsTxt.query()).not.toBeInTheDocument();
});
await ui.appLoaded();
await user.click(ui.maintainabilityDomainBtn.get());
- await user.click(ui.measureLink('Maintainability Rating metric.has_rating_X.E').get());
+ await user.click(
+ ui
+ .measureLink(
+ 'Maintainability Rating metric.has_rating_X.E metric.sqale_rating.tooltip.E.0.0%',
+ )
+ .get(),
+ );
// Click treemap option in view select
await user.click(ui.viewSelect.get());
const { ui, user } = getPageObject();
renderMeasuresApp('component_measures?id=foo&metric=code_smells&view=list');
- await ui.appLoaded();
- await user.click(ui.showAllBtn.get());
+ await user.click(await ui.showAllBtn.find());
expect(ui.showingOutOfTxt('500', '1,008').get()).toBeInTheDocument();
await ui.clickLoadMore();
themeBorder,
themeColor,
} from 'design-system';
-import { keyBy } from 'lodash';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
-import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
+import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
import { getBranchLikeQuery, isPullRequest } from '~sonar-aligned/helpers/branch-like';
import { isPortfolioLike } from '~sonar-aligned/helpers/component';
import { ComponentQualifier } from '~sonar-aligned/types/component';
import { MetricKey } from '~sonar-aligned/types/metrics';
-import { Location, Router } from '~sonar-aligned/types/router';
-import { getMeasuresWithPeriod } from '../../../api/measures';
-import { getAllMetrics } from '../../../api/metrics';
import { ComponentContext } from '../../../app/components/componentContext/ComponentContext';
+import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { enhanceMeasure } from '../../../components/measure/utils';
import '../../../components/search-navigator.css';
import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
-import { isSameBranchLike } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
-import { areCCTMeasuresComputed } from '../../../helpers/measures';
-import { WithBranchLikesProps, useBranchesQuery } from '../../../queries/branch';
+import {
+ areCCTMeasuresComputed,
+ areSoftwareQualityRatingsComputed,
+} from '../../../helpers/measures';
+import { useBranchesQuery } from '../../../queries/branch';
+import { useMeasuresComponentQuery } from '../../../queries/measures';
import { MeasurePageView } from '../../../types/measures';
-import { ComponentMeasure, Dict, MeasureEnhanced, Metric, Period } from '../../../types/types';
+import { useBubbleChartMetrics } from '../hooks';
import Sidebar from '../sidebar/Sidebar';
import {
Query,
sortMeasures,
} from '../utils';
import MeasureContent from './MeasureContent';
-import MeasureOverviewContainer from './MeasureOverviewContainer';
+import MeasureOverview from './MeasureOverview';
import MeasuresEmpty from './MeasuresEmpty';
-interface Props extends WithBranchLikesProps {
- component: ComponentMeasure;
- location: Location;
- router: Router;
-}
-
-interface State {
- leakPeriod?: Period;
- loading: boolean;
- measures: MeasureEnhanced[];
- metrics: Dict<Metric>;
-}
-
-class ComponentMeasuresApp extends React.PureComponent<Props, State> {
- mounted = false;
- state: State;
-
- constructor(props: Props) {
- super(props);
-
- this.state = {
- loading: true,
- measures: [],
- metrics: {},
- };
- }
-
- componentDidMount() {
- this.mounted = true;
-
- getAllMetrics().then(
- (metrics) => {
- const byKey = keyBy(metrics, 'key');
- this.setState({ metrics: byKey });
- },
- () => {},
+export default function ComponentMeasuresApp() {
+ const { component } = React.useContext(ComponentContext);
+ const { data: { branchLike } = {} } = useBranchesQuery(component);
+ const { query: rawQuery, pathname } = useLocation();
+ const query = parseQuery(rawQuery);
+ const router = useRouter();
+ const metrics = useMetrics();
+ const filteredMetrics = getMeasuresPageMetricKeys(metrics, branchLike);
+ const componentKey =
+ query.selected !== undefined && query.selected !== '' ? query.selected : component?.key ?? '';
+
+ const { data: { component: componentWithMeasures, period } = {}, isLoading } =
+ useMeasuresComponentQuery(
+ { componentKey, metricKeys: filteredMetrics, branchLike },
+ { enabled: Boolean(componentKey) },
);
- }
-
- componentDidUpdate(prevProps: Props, prevState: State) {
- const prevQuery = parseQuery(prevProps.location.query);
- const query = parseQuery(this.props.location.query);
-
- const hasSelectedQueryChanged = prevQuery.selected !== query.selected;
-
- const hasBranchChanged = !isSameBranchLike(prevProps.branchLike, this.props.branchLike);
-
- const isBranchReady =
- isPortfolioLike(this.props.component.qualifier) || this.props.branchLike !== undefined;
- const haveMetricsChanged =
- Object.keys(this.state.metrics).length !== Object.keys(prevState.metrics).length;
-
- const areMetricsReady = Object.keys(this.state.metrics).length > 0;
-
- if (
- areMetricsReady &&
- isBranchReady &&
- (haveMetricsChanged || hasBranchChanged || hasSelectedQueryChanged)
- ) {
- this.fetchMeasures(this.state.metrics);
- }
+ const measures = (
+ componentWithMeasures
+ ? filterMeasures(
+ banQualityGateMeasure(componentWithMeasures).map((measure) =>
+ enhanceMeasure(measure, metrics),
+ ),
+ )
+ : []
+ ).filter((measure) => measure.value !== undefined || measure.leak !== undefined);
+ const bubblesByDomain = useBubbleChartMetrics(measures);
+
+ const leakPeriod =
+ componentWithMeasures?.qualifier === ComponentQualifier.Project ? period : undefined;
+ const displayOverview = hasBubbleChart(bubblesByDomain, query.metric);
+
+ if (!component) {
+ return null;
}
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchMeasures(metrics: State['metrics']) {
- const { branchLike } = this.props;
- const query = parseQuery(this.props.location.query);
- const componentKey =
- query.selected !== undefined && query.selected !== ''
- ? query.selected
- : this.props.component.key;
-
- const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike);
-
- getMeasuresWithPeriod(componentKey, filteredKeys, getBranchLikeQuery(branchLike)).then(
- ({ component, period }) => {
- if (this.mounted) {
- const measures = filterMeasures(
- banQualityGateMeasure(component).map((measure) => enhanceMeasure(measure, metrics)),
- );
- const leakPeriod =
- component.qualifier === ComponentQualifier.Project ? period : undefined;
-
- this.setState({
- loading: false,
- leakPeriod,
- measures: measures.filter(
- (measure) => measure.value !== undefined || measure.leak !== undefined,
- ),
- });
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- },
- );
- }
-
- getSelectedMetric = (query: Query, displayOverview: boolean) => {
+ const getSelectedMetric = (query: Query, displayOverview: boolean) => {
if (displayOverview) {
return undefined;
}
- const metric = this.state.metrics[query.metric];
+ const metric = metrics[query.metric];
if (!metric) {
- const domainMeasures = groupByDomains(this.state.measures);
-
+ const domainMeasures = groupByDomains(measures);
const firstMeasure =
domainMeasures[0] && sortMeasures(domainMeasures[0].name, domainMeasures[0].measures)[0];
return metric;
};
- updateQuery = (newQuery: Partial<Query>) => {
- const query: Query = { ...parseQuery(this.props.location.query), ...newQuery };
+ const metric = getSelectedMetric(query, displayOverview);
- const metric = this.getSelectedMetric(query, false);
+ const updateQuery = (newQuery: Partial<Query>) => {
+ const nextQuery: Query = { ...parseQuery(query), ...newQuery };
+ const metric = getSelectedMetric(nextQuery, false);
if (metric) {
if (query.view === MeasurePageView.treemap && !hasTreemap(metric.key, metric.type)) {
}
}
- this.props.router.push({
- pathname: this.props.location.pathname,
+ router.push({
+ pathname,
query: {
- ...serializeQuery(query),
- ...getBranchLikeQuery(this.props.branchLike),
- id: this.props.component.key,
+ ...serializeQuery(nextQuery),
+ ...getBranchLikeQuery(branchLike),
+ id: component?.key,
},
});
};
- renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => {
- const { branchLike, component } = this.props;
- const { leakPeriod } = this.state;
+ const showFullMeasures = hasFullMeasures(branchLike);
+ const renderContent = () => {
if (displayOverview) {
return (
<StyledMain className="sw-rounded-1 sw-mb-4">
- <MeasureOverviewContainer
- branchLike={branchLike}
- domain={query.metric}
+ <MeasureOverview
+ bubblesByDomain={bubblesByDomain}
leakPeriod={leakPeriod}
- metrics={this.state.metrics}
rootComponent={component}
- router={this.props.router}
- selected={query.selected}
- updateQuery={this.updateQuery}
+ updateQuery={updateQuery}
/>
</StyledMain>
);
return (
<StyledMain className="sw-rounded-1 sw-mb-4">
<MeasureContent
- asc={query.asc}
- branchLike={branchLike}
leakPeriod={leakPeriod}
- metrics={this.state.metrics}
requestedMetric={metric}
rootComponent={component}
- router={this.props.router}
- selected={query.selected}
- updateQuery={this.updateQuery}
- view={query.view}
+ updateQuery={updateQuery}
/>
</StyledMain>
);
};
- render() {
- const { branchLike } = this.props;
- const { measures } = this.state;
- const { canBrowseAllChildProjects, qualifier, key } = this.props.component;
- const query = parseQuery(this.props.location.query);
- const showFullMeasures = hasFullMeasures(branchLike);
- const displayOverview = hasBubbleChart(query.metric);
- const metric = this.getSelectedMetric(query, displayOverview);
-
- return (
- <LargeCenteredLayout id="component-measures" className="sw-pt-8">
- <Suggestions suggestionGroup="component_measures" />
- <Helmet defer={false} title={translate('layout.measures')} />
- <PageContentFontWrapper className="sw-body-sm">
- <Spinner isLoading={this.state.loading} />
-
- {measures.length > 0 ? (
- <div className="sw-grid sw-grid-cols-12 sw-w-full">
- <Sidebar
- componentKey={key}
- measures={measures}
- selectedMetric={metric ? metric.key : query.metric}
- showFullMeasures={showFullMeasures}
- updateQuery={this.updateQuery}
- />
-
- <div className="sw-col-span-9 sw-ml-12">
- {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
- <FlagMessage className="sw-mb-4 it__portfolio_warning" variant="warning">
- {translate('component_measures.not_all_measures_are_shown')}
- <HelpTooltip
- className="sw-ml-2"
- overlay={translate('component_measures.not_all_measures_are_shown.help')}
- />
- </FlagMessage>
- )}
- {!areCCTMeasuresComputed(measures) && (
- <AnalysisMissingInfoMessage className="sw-mb-4" qualifier={qualifier} />
- )}
- {this.renderContent(displayOverview, query, metric)}
- </div>
+ return (
+ <LargeCenteredLayout id="component-measures" className="sw-pt-8">
+ <Suggestions suggestionGroup="component_measures" />
+ <Helmet defer={false} title={translate('layout.measures')} />
+ <PageContentFontWrapper className="sw-body-sm">
+ <Spinner isLoading={isLoading} />
+
+ {measures.length > 0 ? (
+ <div className="sw-grid sw-grid-cols-12 sw-w-full">
+ <Sidebar
+ componentKey={componentKey}
+ measures={measures}
+ selectedMetric={metric ? metric.key : query.metric}
+ showFullMeasures={showFullMeasures}
+ updateQuery={updateQuery}
+ />
+
+ <div className="sw-col-span-9 sw-ml-12">
+ {!component?.canBrowseAllChildProjects && isPortfolioLike(component?.qualifier) && (
+ <FlagMessage className="sw-mb-4 it__portfolio_warning" variant="warning">
+ {translate('component_measures.not_all_measures_are_shown')}
+ <HelpTooltip
+ className="sw-ml-2"
+ overlay={translate('component_measures.not_all_measures_are_shown.help')}
+ />
+ </FlagMessage>
+ )}
+ {(!areCCTMeasuresComputed(measures) ||
+ !areSoftwareQualityRatingsComputed(measures)) && (
+ <AnalysisMissingInfoMessage
+ className="sw-mb-4"
+ qualifier={component?.qualifier as ComponentQualifier}
+ />
+ )}
+ {renderContent()}
</div>
- ) : (
- <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
- <MeasuresEmpty />
- </StyledMain>
- )}
- </PageContentFontWrapper>
- </LargeCenteredLayout>
- );
- }
+ </div>
+ ) : (
+ <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
+ <MeasuresEmpty />
+ </StyledMain>
+ )}
+ </PageContentFontWrapper>
+ </LargeCenteredLayout>
+ );
}
-/*
- * This needs to be refactored: the issue
- * is that we can't use the usual withComponentContext HOC, because the type
- * of `component` isn't the same. It probably used to work because of the lazy loading
- */
-const WrappedApp = withRouter(ComponentMeasuresApp);
-
-function AppWithComponentContext() {
- const { component } = React.useContext(ComponentContext);
- const { data: { branchLike } = {} } = useBranchesQuery(component);
-
- return <WrappedApp branchLike={branchLike} component={component as ComponentMeasure} />;
-}
-
-export default AppWithComponentContext;
-
const StyledMain = withTheme(styled.main`
background-color: ${themeColor('pageBlock')};
border: ${themeBorder('default', 'pageBlockBorder')};
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { keepPreviousData } from '@tanstack/react-query';
import { Highlight, KeyboardHint } from 'design-system';
import * as React from 'react';
import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { MetricKey } from '~sonar-aligned/types/metrics';
-import { Router } from '~sonar-aligned/types/router';
-import { getComponentTree } from '../../../api/components';
-import { getMeasures } from '../../../api/measures';
+import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import FilesCounter from '../../../components/ui/FilesCounter';
-import { isSameBranchLike } from '../../../helpers/branch-like';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
+import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from '../../../helpers/constants';
import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate } from '../../../helpers/l10n';
import { getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
import { RequestData } from '../../../helpers/request';
import { isDefined } from '../../../helpers/types';
import { getProjectUrl } from '../../../helpers/urls';
+import { useBranchesQuery } from '../../../queries/branch';
+import { useComponentTreeQuery, useMeasuresComponentQuery } from '../../../queries/measures';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
+import { useLocation, useRouter } from '../../../sonar-aligned/components/hoc/withRouter';
import { BranchLike } from '../../../types/branch-like';
import { isApplication, isFile, isView } from '../../../types/component';
import { MeasurePageView } from '../../../types/measures';
import {
- ComponentMeasure,
+ Component,
ComponentMeasureEnhanced,
ComponentMeasureIntern,
- Dict,
- Measure,
Metric,
- Paging,
Period,
} from '../../../types/types';
import { complementary } from '../config/complementary';
import FilesView from '../drilldown/FilesView';
import TreeMapView from '../drilldown/TreeMapView';
-import { Query, enhanceComponent } from '../utils';
+import { Query, enhanceComponent, parseQuery } from '../utils';
import MeasureContentHeader from './MeasureContentHeader';
import MeasureHeader from './MeasureHeader';
import MeasureViewSelect from './MeasureViewSelect';
import MeasuresBreadcrumbs from './MeasuresBreadcrumbs';
interface Props {
- asc?: boolean;
- branchLike?: BranchLike;
leakPeriod?: Period;
- metrics: Dict<Metric>;
requestedMetric: Pick<Metric, 'key' | 'direction'>;
- rootComponent: ComponentMeasure;
- router: Router;
- selected?: string;
+ rootComponent: Component;
updateQuery: (query: Partial<Query>) => void;
- view: MeasurePageView;
}
-interface State {
- baseComponent?: ComponentMeasure;
- components: ComponentMeasureEnhanced[];
- loadingMoreComponents: boolean;
- measure?: Measure;
- metric?: Metric;
- paging?: Paging;
- secondaryMeasure?: Measure;
- selectedComponent?: ComponentMeasureIntern;
-}
-
-export default class MeasureContent extends React.PureComponent<Props, State> {
- container?: HTMLElement | null;
- mounted = false;
- state: State = {
- components: [],
- loadingMoreComponents: false,
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchComponentTree();
+export default function MeasureContent(props: Readonly<Props>) {
+ const { leakPeriod, requestedMetric, rootComponent, updateQuery } = props;
+ const metrics = useMetrics();
+ const { query: rawQuery } = useLocation();
+ const { data: { branchLike } = {} } = useBranchesQuery();
+ const router = useRouter();
+ const query = parseQuery(rawQuery);
+ const { data: isLegacy } = useIsLegacyCCTMode();
+ const { selected, asc, view } = query;
+
+ const containerRef = React.useRef<HTMLDivElement>(null);
+ // if asc is undefined we dont want to pass it inside options
+ const { metricKeys, opts, strategy } = getComponentRequestParams(
+ view,
+ requestedMetric,
+ branchLike,
+ {
+ ...(asc !== undefined && { asc }),
+ },
+ );
+ const componentKey = selected !== undefined && selected !== '' ? selected : rootComponent.key;
+ const {
+ data: treeData,
+ isFetchingNextPage: fetchingMoreComponents,
+ fetchNextPage,
+ } = useComponentTreeQuery(
+ {
+ strategy,
+ component: componentKey,
+ metrics: metricKeys,
+ additionalData: opts,
+ },
+ {
+ placeholderData: keepPreviousData,
+ },
+ );
+
+ const baseComponentMetrics = [requestedMetric.key];
+
+ if (requestedMetric.key === MetricKey.ncloc) {
+ baseComponentMetrics.push(MetricKey.ncloc_language_distribution);
}
-
- componentDidUpdate(prevProps: Props) {
- const prevComponentKey =
- prevProps.selected !== undefined && prevProps.selected !== ''
- ? prevProps.selected
- : prevProps.rootComponent.key;
- const componentKey =
- this.props.selected !== undefined && this.props.selected !== ''
- ? this.props.selected
- : this.props.rootComponent.key;
- if (
- prevComponentKey !== componentKey ||
- !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
- prevProps.requestedMetric !== this.props.requestedMetric ||
- prevProps.view !== this.props.view
- ) {
- this.fetchComponentTree();
- }
+ if (SOFTWARE_QUALITY_RATING_METRICS_MAP[requestedMetric.key]) {
+ baseComponentMetrics.push(SOFTWARE_QUALITY_RATING_METRICS_MAP[requestedMetric.key]);
}
- componentWillUnmount() {
- this.mounted = false;
- }
+ const { data: measuresData } = useMeasuresComponentQuery(
+ { componentKey, metricKeys: baseComponentMetrics, branchLike },
+ { enabled: Boolean(componentKey) },
+ );
- fetchComponentTree = () => {
- const { asc, branchLike, metrics, requestedMetric, rootComponent, selected, view } = this.props;
- // if asc is undefined we dont want to pass it inside options
- const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, requestedMetric, {
- ...(asc !== undefined && { asc }),
- });
- const componentKey = selected !== undefined && selected !== '' ? selected : rootComponent.key;
- const baseComponentMetrics = [requestedMetric.key];
- if (requestedMetric.key === MetricKey.ncloc) {
- baseComponentMetrics.push(MetricKey.ncloc_language_distribution);
- }
- Promise.all([
- getComponentTree(strategy, componentKey, metricKeys, opts),
- getMeasures({
- component: componentKey,
- metricKeys: baseComponentMetrics.join(),
- ...getBranchLikeQuery(branchLike),
- }),
- ]).then(
- ([tree, measures]) => {
- if (this.mounted) {
- const metric = tree.metrics.find((m) => m.key === requestedMetric.key);
- if (metric !== undefined) {
- metric.direction = requestedMetric.direction;
- }
-
- const components = tree.components.map((component) =>
- enhanceComponent(component, metric, metrics),
- );
+ const [selectedComponent, setSelectedComponent] = React.useState<ComponentMeasureEnhanced>();
- const measure = measures.find((m) => m.metric === requestedMetric.key);
- const secondaryMeasure = measures.find((m) => m.metric !== requestedMetric.key);
+ const metric = metrics[requestedMetric.key];
+ metric.direction = requestedMetric.direction;
- this.setState(({ selectedComponent }) => ({
- baseComponent: tree.baseComponent,
- components,
- measure,
- metric,
- paging: tree.paging,
- secondaryMeasure,
- selectedComponent:
- components.length > 0 &&
- components.find(
- (c) =>
- getComponentMeasureUniqueKey(c) ===
- getComponentMeasureUniqueKey(selectedComponent),
- )
- ? selectedComponent
- : undefined,
- }));
- }
- },
- () => {
- /* noop */
- },
- );
- };
+ const baseComponent = treeData?.pages[0].baseComponent;
+ if (!baseComponent) {
+ return null;
+ }
- fetchMoreComponents = () => {
- const { metrics, view, asc } = this.props;
- const { baseComponent, metric, paging } = this.state;
- if (!baseComponent || !paging || !metric) {
- return;
- }
- // if asc is undefined we dont want to pass it inside options
- const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric, {
- p: paging.pageIndex + 1,
- ...(asc !== undefined && { asc }),
- });
- this.setState({ loadingMoreComponents: true });
- getComponentTree(strategy, baseComponent.key, metricKeys, opts).then(
- (r) => {
- if (this.mounted && metric.key === this.props.requestedMetric.key) {
- this.setState((state) => ({
- components: [
- ...state.components,
- ...r.components.map((component) => enhanceComponent(component, metric, metrics)),
- ],
- loadingMoreComponents: false,
- paging: r.paging,
- }));
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loadingMoreComponents: false });
- }
- },
+ const components =
+ treeData?.pages
+ .flatMap((p) => p.components)
+ .map((component) => enhanceComponent(component, metric, metrics)) ?? [];
+
+ const measures = measuresData?.component.measures ?? [];
+ const measure = measures.find((m) => m.metric === requestedMetric.key);
+ const secondaryMeasure = measures.find((m) => m.metric !== requestedMetric.key);
+ const rawMeasureValue =
+ measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value);
+ const measureValue = getCCTMeasureValue(metric.key, rawMeasureValue);
+ const isFileComponent = isFile(baseComponent.qualifier);
+
+ const paging = treeData?.pages[treeData?.pages.length - 1].paging;
+ const totalComponents = treeData?.pages[0].paging.total;
+
+ const getSelectedIndex = () => {
+ const componentKey = isFile(baseComponent?.qualifier)
+ ? getComponentMeasureUniqueKey(baseComponent)
+ : getComponentMeasureUniqueKey(selectedComponent);
+ const index = components.findIndex(
+ (component) => getComponentMeasureUniqueKey(component) === componentKey,
);
+ return index !== -1 ? index : undefined;
};
+ const selectedIdx = getSelectedIndex();
- getComponentRequestParams(
- view: MeasurePageView,
- metric: Pick<Metric, 'key' | 'direction'>,
- options: Object = {},
- ) {
- const strategy = view === MeasurePageView.list ? 'leaves' : 'children';
- const metricKeys = [metric.key];
- const opts: RequestData = {
- ...getBranchLikeQuery(this.props.branchLike),
- additionalFields: 'metrics',
- ps: 500,
- };
-
- const setMetricSort = () => {
- const isDiff = isDiffMetric(metric.key);
- opts.s = isDiff ? 'metricPeriod' : 'metric';
- opts.metricSortFilter = 'withMeasuresOnly';
- if (isDiff) {
- opts.metricPeriodSort = 1;
- }
- };
-
- const isDiff = isDiffMetric(metric.key);
- if (view === MeasurePageView.tree) {
- metricKeys.push(...(complementary[metric.key] || []));
- opts.asc = true;
- opts.s = 'qualifier,name';
- } else if (view === MeasurePageView.list) {
- metricKeys.push(...(complementary[metric.key] || []));
- opts.asc = metric.direction === 1;
- opts.metricSort = metric.key;
- setMetricSort();
- } else if (view === MeasurePageView.treemap) {
- const sizeMetric = isDiff ? MetricKey.new_lines : MetricKey.ncloc;
- metricKeys.push(sizeMetric);
- opts.asc = false;
- opts.metricSort = sizeMetric;
- setMetricSort();
- }
-
- return { metricKeys, opts: { ...opts, ...options }, strategy };
- }
-
- updateSelected = (component: string) => {
- this.props.updateQuery({
- selected: component !== this.props.rootComponent.key ? component : undefined,
+ const updateSelected = (component: string) => {
+ updateQuery({
+ selected: component !== rootComponent.key ? component : undefined,
});
};
- updateView = (view: MeasurePageView) => {
- this.props.updateQuery({ view });
- };
-
- onOpenComponent = (component: ComponentMeasureIntern) => {
- if (isView(this.props.rootComponent.qualifier)) {
- const comp = this.state.components.find(
+ const onOpenComponent = (component: ComponentMeasureIntern) => {
+ if (isView(rootComponent.qualifier)) {
+ const comp = components.find(
(c) =>
c.refKey === component.key ||
getComponentMeasureUniqueKey(c) === getComponentMeasureUniqueKey(component),
);
if (comp) {
- this.props.router.push(getProjectUrl(comp.refKey || comp.key, component.branch));
+ router.push(getProjectUrl(comp.refKey ?? comp.key, component.branch));
}
return;
}
- this.setState((state) => ({ selectedComponent: state.baseComponent }));
- this.updateSelected(component.key);
- if (this.container) {
- this.container.focus();
+ updateSelected(component.key);
+ if (containerRef.current) {
+ containerRef.current.focus();
}
};
- onSelectComponent = (component: ComponentMeasureIntern) => {
- this.setState({ selectedComponent: component });
- };
-
- getSelectedIndex = () => {
- const componentKey = isFile(this.state.baseComponent?.qualifier)
- ? getComponentMeasureUniqueKey(this.state.baseComponent)
- : getComponentMeasureUniqueKey(this.state.selectedComponent);
- const index = this.state.components.findIndex(
- (component) => getComponentMeasureUniqueKey(component) === componentKey,
- );
- return index !== -1 ? index : undefined;
+ const handleSelectRow = (component: ComponentMeasureEnhanced) => {
+ setSelectedComponent(component);
};
- getDefaultShowBestMeasures() {
- const { asc, view } = this.props;
- if ((asc !== undefined && view === MeasurePageView.list) || view === MeasurePageView.tree) {
- return true;
- }
- return false;
- }
-
- renderMeasure() {
- const { view } = this.props;
- const { metric } = this.state;
- if (!metric) {
- return null;
- }
+ const renderMeasure = () => {
if (view === MeasurePageView.list || view === MeasurePageView.tree) {
- const selectedIdx = this.getSelectedIndex();
return (
<FilesView
- branchLike={this.props.branchLike}
- components={this.state.components}
- defaultShowBestMeasures={this.getDefaultShowBestMeasures()}
- fetchMore={this.fetchMoreComponents}
- handleOpen={this.onOpenComponent}
- handleSelect={this.onSelectComponent}
- loadingMore={this.state.loadingMoreComponents}
+ branchLike={branchLike}
+ components={components}
+ defaultShowBestMeasures={
+ (asc !== undefined && view === MeasurePageView.list) || view === MeasurePageView.tree
+ }
+ fetchMore={fetchNextPage}
+ handleOpen={onOpenComponent}
+ handleSelect={handleSelectRow}
+ loadingMore={fetchingMoreComponents}
metric={metric}
- metrics={this.props.metrics}
- paging={this.state.paging}
- rootComponent={this.props.rootComponent}
+ metrics={metrics}
+ paging={paging}
+ rootComponent={rootComponent}
selectedIdx={selectedIdx}
- selectedComponent={
- selectedIdx !== undefined
- ? (this.state.selectedComponent as ComponentMeasureEnhanced)
- : undefined
- }
+ selectedComponent={selectedComponent}
view={view}
/>
);
return (
<TreeMapView
- components={this.state.components}
- handleSelect={this.onOpenComponent}
+ isLegacyMode={Boolean(isLegacy)}
+ components={components}
+ handleSelect={onOpenComponent}
metric={metric}
/>
);
- }
-
- render() {
- const { branchLike, rootComponent, view } = this.props;
- const { baseComponent, measure, metric, paging, secondaryMeasure } = this.state;
-
- if (!baseComponent || !metric) {
- return null;
- }
+ };
- const rawMeasureValue =
- measure && (isDiffMetric(measure.metric) ? measure.period?.value : measure.value);
- const measureValue = getCCTMeasureValue(metric.key, rawMeasureValue);
+ return (
+ <div ref={containerRef}>
+ <A11ySkipTarget anchor="measures_main" />
- const isFileComponent = isFile(baseComponent.qualifier);
- const selectedIdx = this.getSelectedIndex();
+ <MeasureContentHeader
+ left={
+ <MeasuresBreadcrumbs
+ backToFirst={view === MeasurePageView.list}
+ branchLike={branchLike}
+ className="sw-flex-1"
+ component={baseComponent}
+ handleSelect={onOpenComponent}
+ rootComponent={rootComponent}
+ />
+ }
+ right={
+ <div className="sw-flex sw-items-center">
+ {!isFileComponent && metric && (
+ <>
+ {!isApplication(baseComponent.qualifier) && (
+ <>
+ <Highlight className="sw-whitespace-nowrap" id="measures-view-selection-label">
+ {translate('component_measures.view_as')}
+ </Highlight>
+ <MeasureViewSelect
+ className="measure-view-select sw-ml-2 sw-mr-4"
+ handleViewChange={(view) => updateQuery({ view })}
+ metric={metric}
+ view={view}
+ />
+ </>
+ )}
+
+ {view !== MeasurePageView.treemap && (
+ <>
+ <KeyboardHint
+ className="sw-mr-4 sw-ml-6"
+ command={`${KeyboardKeys.DownArrow} ${KeyboardKeys.UpArrow}`}
+ title={translate('component_measures.select_files')}
+ />
- return (
- <div ref={(container) => (this.container = container)}>
- <A11ySkipTarget anchor="measures_main" />
+ <KeyboardHint
+ command={`${KeyboardKeys.LeftArrow} ${KeyboardKeys.RightArrow}`}
+ title={translate('component_measures.navigate')}
+ />
+ </>
+ )}
+
+ {isDefined(totalComponents) && totalComponents > 0 && (
+ <FilesCounter
+ className="sw-min-w-24 sw-text-right"
+ current={
+ isDefined(selectedIdx) && view !== MeasurePageView.treemap
+ ? selectedIdx + 1
+ : undefined
+ }
+ total={totalComponents}
+ />
+ )}
+ </>
+ )}
+ </div>
+ }
+ />
- <MeasureContentHeader
- left={
- <MeasuresBreadcrumbs
- backToFirst={view === MeasurePageView.list}
+ <div className="sw-p-6">
+ <MeasureHeader
+ branchLike={branchLike}
+ component={baseComponent}
+ leakPeriod={leakPeriod}
+ measureValue={measureValue}
+ metric={metric}
+ secondaryMeasure={secondaryMeasure}
+ />
+ {isFileComponent ? (
+ <div>
+ <SourceViewer
+ hideHeader
branchLike={branchLike}
- className="sw-flex-1"
- component={baseComponent}
- handleSelect={this.onOpenComponent}
- rootComponent={rootComponent}
+ component={baseComponent.key}
+ metricKey={metric.key}
/>
- }
- right={
- <div className="sw-flex sw-items-center">
- {!isFileComponent && metric && (
- <>
- {!isApplication(baseComponent.qualifier) && (
- <>
- <Highlight
- className="sw-whitespace-nowrap"
- id="measures-view-selection-label"
- >
- {translate('component_measures.view_as')}
- </Highlight>
- <MeasureViewSelect
- className="measure-view-select sw-ml-2 sw-mr-4"
- handleViewChange={this.updateView}
- metric={metric}
- view={view}
- />
- </>
- )}
-
- {view !== MeasurePageView.treemap && (
- <>
- <KeyboardHint
- className="sw-mr-4 sw-ml-6"
- command={`${KeyboardKeys.DownArrow} ${KeyboardKeys.UpArrow}`}
- title={translate('component_measures.select_files')}
- />
+ </div>
+ ) : (
+ renderMeasure()
+ )}
+ </div>
+ </div>
+ );
+}
- <KeyboardHint
- command={`${KeyboardKeys.LeftArrow} ${KeyboardKeys.RightArrow}`}
- title={translate('component_measures.navigate')}
- />
- </>
- )}
+function getComponentRequestParams(
+ view: MeasurePageView,
+ metric: Pick<Metric, 'key' | 'direction'>,
+ branchLike?: BranchLike,
+ options: Object = {},
+) {
+ const strategy: 'leaves' | 'children' = view === MeasurePageView.list ? 'leaves' : 'children';
+ const metricKeys = [metric.key];
+ const softwareQualityRatingMetric = SOFTWARE_QUALITY_RATING_METRICS_MAP[metric.key];
+ if (softwareQualityRatingMetric) {
+ metricKeys.push(softwareQualityRatingMetric);
+ }
+ const opts: RequestData = {
+ ...getBranchLikeQuery(branchLike),
+ additionalFields: 'metrics',
+ ps: 500,
+ };
- {paging && paging.total > 0 && (
- <FilesCounter
- className="sw-min-w-24 sw-text-right"
- current={
- isDefined(selectedIdx) && view !== MeasurePageView.treemap
- ? selectedIdx + 1
- : undefined
- }
- total={paging.total}
- />
- )}
- </>
- )}
- </div>
- }
- />
+ const setMetricSort = () => {
+ const isDiff = isDiffMetric(metric.key);
+ opts.s = isDiff ? 'metricPeriod' : 'metric';
+ opts.metricSortFilter = 'withMeasuresOnly';
+ if (isDiff) {
+ opts.metricPeriodSort = 1;
+ }
+ };
- <div className="sw-p-6">
- <MeasureHeader
- branchLike={branchLike}
- component={baseComponent}
- leakPeriod={this.props.leakPeriod}
- measureValue={measureValue}
- metric={metric}
- secondaryMeasure={secondaryMeasure}
- />
- {isFileComponent ? (
- <div>
- <SourceViewer
- hideHeader
- branchLike={branchLike}
- component={baseComponent.key}
- metricKey={this.state.metric?.key}
- />
- </div>
- ) : (
- this.renderMeasure()
- )}
- </div>
- </div>
- );
+ const isDiff = isDiffMetric(metric.key);
+ if (view === MeasurePageView.tree) {
+ metricKeys.push(...(complementary[metric.key] || []));
+ opts.asc = true;
+ opts.s = 'qualifier,name';
+ } else if (view === MeasurePageView.list) {
+ metricKeys.push(...(complementary[metric.key] || []));
+ opts.asc = metric.direction === 1;
+ opts.metricSort = metric.key;
+ setMetricSort();
+ } else if (view === MeasurePageView.treemap) {
+ const sizeMetric = isDiff ? MetricKey.new_lines : MetricKey.ncloc;
+ metricKeys.push(...(complementary[metric.key] || []));
+ metricKeys.push(sizeMetric);
+ opts.asc = false;
+ opts.metricSort = sizeMetric;
+ setMetricSort();
}
+
+ return { metricKeys, opts: { ...opts, ...options }, strategy };
}
<div className="sw-flex sw-items-center sw-ml-2">
<Measure
+ branchLike={branchLike}
componentKey={component.key}
className={classNames('it__measure-details-value sw-body-md')}
metricKey={metric.key}
* 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 'design-system';
+import { Spinner } from '@sonarsource/echoes-react';
import * as React from 'react';
-import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
-import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
-import { getComponentLeaves } from '../../../api/components';
+import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
-import { isSameBranchLike } from '../../../helpers/branch-like';
-import { BranchLike } from '../../../types/branch-like';
-import { isFile } from '../../../types/component';
-import {
- ComponentMeasure,
- ComponentMeasureEnhanced,
- ComponentMeasureIntern,
- Dict,
- Metric,
- Paging,
- Period,
-} from '../../../types/types';
+import { getProjectUrl } from '../../../helpers/urls';
+import { useBranchesQuery } from '../../../queries/branch';
+import { useComponentDataQuery } from '../../../queries/component';
+import { useComponentTreeQuery } from '../../../queries/measures';
+import A11ySkipTarget from '../../../sonar-aligned/components/a11y/A11ySkipTarget';
+import { useLocation, useRouter } from '../../../sonar-aligned/components/hoc/withRouter';
+import { getBranchLikeQuery } from '../../../sonar-aligned/helpers/branch-like';
+import { isFile, isView } from '../../../types/component';
+import { Component, ComponentMeasureIntern, Period } from '../../../types/types';
+import { BubblesByDomain } from '../config/bubbles';
import BubbleChartView from '../drilldown/BubbleChartView';
-import { BUBBLES_FETCH_LIMIT, enhanceComponent, getBubbleMetrics, hasFullMeasures } from '../utils';
+import {
+ BUBBLES_FETCH_LIMIT,
+ enhanceComponent,
+ getBubbleMetrics,
+ hasFullMeasures,
+ parseQuery,
+ Query,
+} from '../utils';
import LeakPeriodLegend from './LeakPeriodLegend';
import MeasureContentHeader from './MeasureContentHeader';
import MeasuresBreadcrumbs from './MeasuresBreadcrumbs';
interface Props {
- branchLike?: BranchLike;
- className?: string;
- component: ComponentMeasure;
- domain: string;
+ bubblesByDomain: BubblesByDomain;
leakPeriod?: Period;
- loading: boolean;
- metrics: Dict<Metric>;
- rootComponent: ComponentMeasure;
- updateLoading: (param: Dict<boolean>) => void;
- updateSelected: (component: ComponentMeasureIntern) => void;
-}
-
-interface State {
- components: ComponentMeasureEnhanced[];
- paging?: Paging;
+ rootComponent: Component;
+ updateQuery: (query: Partial<Query>) => void;
}
-export default class MeasureOverview extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = { components: [] };
+export default function MeasureOverview(props: Readonly<Props>) {
+ const { leakPeriod, updateQuery, rootComponent, bubblesByDomain } = props;
+ const metrics = useMetrics();
+ const { data: { branchLike } = {} } = useBranchesQuery();
+ const router = useRouter();
+ const { query } = useLocation();
+ const { selected, metric: domain } = parseQuery(query);
+ // eslint-disable-next-line local-rules/no-implicit-coercion
+ const componentKey = selected || rootComponent.key;
+ const { data: componentData, isLoading: loadingComponent } = useComponentDataQuery(
+ {
+ ...getBranchLikeQuery(branchLike),
+ component: componentKey,
+ },
+ { enabled: Boolean(componentKey) },
+ );
- componentDidMount() {
- this.mounted = true;
- this.fetchComponents();
+ const component = componentData?.component;
+ const { x, y, size, colors } = getBubbleMetrics(bubblesByDomain, domain, metrics);
+ const metricsKey = [x.key, y.key, size.key];
+ if (colors) {
+ metricsKey.push(...colors.map((metric) => metric.key));
}
+ const { data: bubblesData, isLoading: loadingBubbles } = useComponentTreeQuery(
+ {
+ strategy: 'leaves',
+ metrics: metricsKey,
+ component: component?.key ?? '',
+ additionalData: {
+ ...getBranchLikeQuery(branchLike),
+ s: 'metric',
+ metricSort: size.key,
+ asc: false,
+ ps: BUBBLES_FETCH_LIMIT,
+ },
+ },
+ {
+ enabled: Boolean(component),
+ },
+ );
- componentDidUpdate(prevProps: Props) {
- if (
- prevProps.component !== this.props.component ||
- !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
- prevProps.metrics !== this.props.metrics ||
- prevProps.domain !== this.props.domain
- ) {
- this.fetchComponents();
- }
- }
+ const components = (bubblesData?.pages?.[0]?.components ?? []).map((c) =>
+ enhanceComponent(c, undefined, metrics),
+ );
+ const paging = bubblesData?.pages?.[0]?.paging;
- componentWillUnmount() {
- this.mounted = false;
+ if (!component) {
+ return null;
}
- fetchComponents = () => {
- const { branchLike, component, domain, metrics } = this.props;
- if (isFile(component.qualifier)) {
- this.setState({ components: [], paging: undefined });
- return;
- }
- const { x, y, size, colors } = getBubbleMetrics(domain, metrics);
- const metricsKey = [x.key, y.key, size.key];
- if (colors) {
- metricsKey.push(...colors.map((metric) => metric.key));
- }
- const options = {
- ...getBranchLikeQuery(branchLike),
- s: 'metric',
- metricSort: size.key,
- asc: false,
- ps: BUBBLES_FETCH_LIMIT,
- };
+ const loading = loadingComponent || loadingBubbles;
- this.props.updateLoading({ bubbles: true });
- getComponentLeaves(component.key, metricsKey, options).then(
- (r) => {
- if (domain === this.props.domain) {
- if (this.mounted) {
- this.setState({
- components: r.components.map((c) => enhanceComponent(c, undefined, metrics)),
- paging: r.paging,
- });
- }
- this.props.updateLoading({ bubbles: false });
- }
- },
- () => this.props.updateLoading({ bubbles: false }),
- );
+ const updateSelected = (component: ComponentMeasureIntern) => {
+ if (component && isView(component.qualifier)) {
+ router.push(getProjectUrl(component.refKey || component.key, component.branch));
+ } else {
+ updateQuery({
+ selected: component.key !== rootComponent.key ? component.key : undefined,
+ });
+ }
};
+ const displayLeak = hasFullMeasures(branchLike);
+ const isFileComponent = isFile(component.qualifier);
- renderContent(isFile: boolean) {
- const { branchLike, component, domain, metrics } = this.props;
- const { paging } = this.state;
-
- if (isFile) {
- return (
- <div className="measure-details-viewer">
- <SourceViewer hideHeader branchLike={branchLike} component={component.key} />
- </div>
- );
- }
+ return (
+ <div>
+ <A11ySkipTarget anchor="measures_main" />
- return (
- <BubbleChartView
- component={component}
- branchLike={branchLike}
- components={this.state.components}
- domain={domain}
- metrics={metrics}
- paging={paging}
- updateSelected={this.props.updateSelected}
+ <MeasureContentHeader
+ left={
+ <MeasuresBreadcrumbs
+ backToFirst
+ branchLike={branchLike}
+ component={component}
+ handleSelect={updateSelected}
+ rootComponent={rootComponent}
+ />
+ }
+ right={
+ leakPeriod &&
+ displayLeak && <LeakPeriodLegend component={component} period={leakPeriod} />
+ }
/>
- );
- }
- render() {
- const { branchLike, className, component, leakPeriod, loading, rootComponent } = this.props;
- const displayLeak = hasFullMeasures(branchLike);
- const isFileComponent = isFile(component.qualifier);
-
- return (
- <div className={className}>
- <A11ySkipTarget anchor="measures_main" />
-
- <MeasureContentHeader
- left={
- <MeasuresBreadcrumbs
- backToFirst
- branchLike={branchLike}
+ <div className="sw-p-6">
+ <Spinner isLoading={loading}>
+ {isFileComponent && (
+ <div className="measure-details-viewer">
+ <SourceViewer hideHeader branchLike={branchLike} component={component.key} />
+ </div>
+ )}
+ {!isFileComponent && (
+ <BubbleChartView
+ bubblesByDomain={bubblesByDomain}
component={component}
- handleSelect={this.props.updateSelected}
- rootComponent={rootComponent}
+ branchLike={branchLike}
+ components={components}
+ domain={domain}
+ metrics={metrics}
+ paging={paging}
+ updateSelected={updateSelected}
/>
- }
- right={
- leakPeriod &&
- displayLeak && <LeakPeriodLegend component={component} period={leakPeriod} />
- }
- />
-
- <div className="sw-p-6">
- <Spinner loading={loading} />
- {!loading && this.renderContent(isFileComponent)}
- </div>
+ )}
+ </Spinner>
</div>
- );
- }
+ </div>
+ );
}
+++ /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 * as React from 'react';
-import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
-import { Router } from '~sonar-aligned/types/router';
-import { getComponentShow } from '../../../api/components';
-import { isSameBranchLike } from '../../../helpers/branch-like';
-import { getProjectUrl } from '../../../helpers/urls';
-import { BranchLike } from '../../../types/branch-like';
-import { isView } from '../../../types/component';
-import {
- ComponentMeasure,
- ComponentMeasureIntern,
- Dict,
- Metric,
- Period,
-} from '../../../types/types';
-import { Query } from '../utils';
-import MeasureOverview from './MeasureOverview';
-
-interface Props {
- branchLike?: BranchLike;
- className?: string;
- domain: string;
- leakPeriod?: Period;
- metrics: Dict<Metric>;
- rootComponent: ComponentMeasure;
- router: Router;
- selected?: string;
- updateQuery: (query: Partial<Query>) => void;
-}
-
-interface LoadingState {
- bubbles: boolean;
- component: boolean;
-}
-
-interface State {
- component?: ComponentMeasure;
- loading: LoadingState;
-}
-
-export default class MeasureOverviewContainer extends React.PureComponent<Props, State> {
- mounted = false;
-
- state: State = {
- loading: { bubbles: false, component: false },
- };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchComponent();
- }
-
- componentDidUpdate(prevProps: Props) {
- const prevComponentKey = prevProps.selected || prevProps.rootComponent.key;
- const componentKey = this.props.selected || this.props.rootComponent.key;
- if (
- prevComponentKey !== componentKey ||
- !isSameBranchLike(prevProps.branchLike, this.props.branchLike) ||
- prevProps.domain !== this.props.domain
- ) {
- this.fetchComponent();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchComponent = () => {
- const { branchLike, rootComponent, selected } = this.props;
- if (!selected || rootComponent.key === selected) {
- this.setState({ component: rootComponent });
- this.updateLoading({ component: false });
- return;
- }
- this.updateLoading({ component: true });
- getComponentShow({ component: selected, ...getBranchLikeQuery(branchLike) }).then(
- ({ component }) => {
- if (this.mounted) {
- this.setState({ component });
- this.updateLoading({ component: false });
- }
- },
- () => this.updateLoading({ component: false }),
- );
- };
-
- updateLoading = (loading: Partial<LoadingState>) => {
- if (this.mounted) {
- this.setState((state) => ({ loading: { ...state.loading, ...loading } }));
- }
- };
-
- updateSelected = (component: ComponentMeasureIntern) => {
- if (this.state.component && isView(this.state.component.qualifier)) {
- this.props.router.push(getProjectUrl(component.refKey || component.key, component.branch));
- } else {
- this.props.updateQuery({
- selected: component.key !== this.props.rootComponent.key ? component.key : undefined,
- });
- }
- };
-
- render() {
- if (!this.state.component) {
- return null;
- }
-
- return (
- <MeasureOverview
- branchLike={this.props.branchLike}
- className={this.props.className}
- component={this.state.component}
- domain={this.props.domain}
- leakPeriod={this.props.leakPeriod}
- loading={this.state.loading.component || this.state.loading.bubbles}
- metrics={this.props.metrics}
- rootComponent={this.props.rootComponent}
- updateLoading={this.updateLoading}
- updateSelected={this.updateSelected}
- />
- );
- }
-}
import { collapsePath, limitComponentName } from '../../../helpers/path';
import { BranchLike } from '../../../types/branch-like';
import { isProject } from '../../../types/component';
-import { ComponentMeasure, ComponentMeasureIntern } from '../../../types/types';
+import { Component, ComponentMeasure, ComponentMeasureIntern } from '../../../types/types';
interface Props {
backToFirst: boolean;
className?: string;
component: ComponentMeasure;
handleSelect: (component: ComponentMeasureIntern) => void;
- rootComponent: ComponentMeasure;
+ rootComponent: Component;
}
interface State {
*/
import { MetricKey } from '~sonar-aligned/types/metrics';
-export const bubbles: {
- [domain: string]: {
+export type BubblesByDomain = Record<
+ string,
+ {
colors?: string[];
size: string;
x: string;
y: string;
yDomain?: [number, number];
- };
-} = {
+ }
+>;
+
+export const newTaxonomyBubbles: BubblesByDomain = {
+ Reliability: {
+ x: MetricKey.ncloc,
+ y: MetricKey.reliability_remediation_effort,
+ size: MetricKey.reliability_issues,
+ colors: [MetricKey.reliability_rating_new],
+ },
+ Security: {
+ x: MetricKey.ncloc,
+ y: MetricKey.security_remediation_effort,
+ size: MetricKey.security_issues,
+ colors: [MetricKey.security_rating_new],
+ },
+ Maintainability: {
+ x: MetricKey.ncloc,
+ y: MetricKey.sqale_index,
+ size: MetricKey.maintainability_issues,
+ colors: [MetricKey.sqale_rating_new],
+ },
+ Coverage: {
+ x: MetricKey.complexity,
+ y: MetricKey.coverage,
+ size: MetricKey.uncovered_lines,
+ yDomain: [100, 0],
+ },
+ Duplications: {
+ x: MetricKey.ncloc,
+ y: MetricKey.duplicated_lines,
+ size: MetricKey.duplicated_blocks,
+ },
+ project_overview: {
+ x: MetricKey.sqale_index,
+ y: MetricKey.coverage,
+ size: MetricKey.ncloc,
+ colors: [MetricKey.reliability_rating_new, MetricKey.security_rating_new],
+ yDomain: [100, 0],
+ },
+};
+
+export const newTaxonomyWithoutRatingsBubbles: BubblesByDomain = {
+ Reliability: {
+ x: MetricKey.ncloc,
+ y: MetricKey.reliability_remediation_effort,
+ size: MetricKey.reliability_issues,
+ colors: [MetricKey.reliability_rating],
+ },
+ Security: {
+ x: MetricKey.ncloc,
+ y: MetricKey.security_remediation_effort,
+ size: MetricKey.security_issues,
+ colors: [MetricKey.security_rating],
+ },
+ Maintainability: {
+ x: MetricKey.ncloc,
+ y: MetricKey.sqale_index,
+ size: MetricKey.maintainability_issues,
+ colors: [MetricKey.sqale_rating],
+ },
+ Coverage: {
+ x: MetricKey.complexity,
+ y: MetricKey.coverage,
+ size: MetricKey.uncovered_lines,
+ yDomain: [100, 0],
+ },
+ Duplications: {
+ x: MetricKey.ncloc,
+ y: MetricKey.duplicated_lines,
+ size: MetricKey.duplicated_blocks,
+ },
+ project_overview: {
+ x: MetricKey.sqale_index,
+ y: MetricKey.coverage,
+ size: MetricKey.ncloc,
+ colors: [MetricKey.reliability_rating, MetricKey.security_rating],
+ yDomain: [100, 0],
+ },
+};
+
+export const legacyBubbles: BubblesByDomain = {
Reliability: {
x: MetricKey.ncloc,
y: MetricKey.reliability_remediation_effort,
MetricKey.new_reliability_issues,
MetricKey.new_bugs,
MetricKey.new_reliability_rating,
+ MetricKey.new_reliability_rating_new,
MetricKey.new_reliability_remediation_effort,
+ MetricKey.new_reliability_remediation_effort_new,
OVERALL_CATEGORY,
MetricKey.reliability_issues,
MetricKey.bugs,
MetricKey.reliability_rating,
+ MetricKey.reliability_rating_new,
MetricKey.reliability_remediation_effort,
+ MetricKey.reliability_remediation_effort_new,
],
},
MetricKey.new_security_issues,
MetricKey.new_vulnerabilities,
MetricKey.new_security_rating,
+ MetricKey.new_security_rating_new,
MetricKey.new_security_remediation_effort,
+ MetricKey.new_security_remediation_effort_new,
OVERALL_CATEGORY,
MetricKey.security_issues,
MetricKey.vulnerabilities,
MetricKey.security_rating,
+ MetricKey.security_rating_new,
MetricKey.security_remediation_effort,
+ MetricKey.security_remediation_effort_new,
],
},
NEW_CODE_CATEGORY,
MetricKey.new_security_hotspots,
MetricKey.new_security_review_rating,
+ MetricKey.new_security_review_rating_new,
MetricKey.new_security_hotspots_reviewed,
OVERALL_CATEGORY,
MetricKey.security_hotspots,
MetricKey.security_review_rating,
+ MetricKey.security_review_rating_new,
MetricKey.security_hotspots_reviewed,
],
},
MetricKey.new_technical_debt,
MetricKey.new_sqale_debt_ratio,
MetricKey.new_maintainability_rating,
+ MetricKey.new_maintainability_rating_new,
OVERALL_CATEGORY,
MetricKey.maintainability_issues,
MetricKey.sqale_index,
MetricKey.sqale_debt_ratio,
MetricKey.sqale_rating,
+ MetricKey.sqale_rating_new,
MetricKey.effort_to_reach_maintainability_rating_a,
+ MetricKey.effort_to_reach_maintainability_rating_a_new,
],
},
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
BubbleColorVal,
Link,
BubbleChart as OriginalBubbleChart,
themeColor,
+ themeContrast,
} from 'design-system';
import * as React from 'react';
import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip';
translate,
translateWithParameters,
} from '../../../helpers/l10n';
-import { isDiffMetric } from '../../../helpers/measures';
+import { getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { getComponentDrilldownUrl } from '../../../helpers/urls';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
import { BranchLike } from '../../../types/branch-like';
import { isProject, isView } from '../../../types/component';
import {
Metric,
Paging,
} from '../../../types/types';
+import { BubblesByDomain } from '../config/bubbles';
import {
BUBBLES_FETCH_LIMIT,
getBubbleMetrics,
interface Props {
branchLike?: BranchLike;
+ bubblesByDomain: BubblesByDomain;
component: ComponentMeasureI;
components: ComponentMeasureEnhanced[];
domain: string;
updateSelected: (component: ComponentMeasureIntern) => void;
}
-interface State {
- ratingFilters: { [rating: number]: boolean };
-}
-
-export default class BubbleChartView extends React.PureComponent<Props, State> {
- state: State = {
- ratingFilters: {},
- };
+export default function BubbleChartView(props: Readonly<Props>) {
+ const {
+ metrics,
+ domain,
+ components,
+ updateSelected,
+ paging,
+ component,
+ branchLike,
+ bubblesByDomain,
+ } = props;
+ const theme = useTheme();
+ const { data: isLegacy } = useIsLegacyCCTMode();
+ const bubbleMetrics = getBubbleMetrics(bubblesByDomain, domain, metrics);
+ const [ratingFilters, setRatingFilters] = React.useState<{ [rating: number]: boolean }>({});
- getMeasureVal = (component: ComponentMeasureEnhanced, metric: Metric) => {
- const measure = component.measures.find((measure) => measure.metric.key === metric.key);
- if (!measure) {
- return undefined;
- }
- return Number(isDiffMetric(metric.key) ? measure.leak : measure.value);
- };
-
- getTooltip(
- component: ComponentMeasureEnhanced,
- values: { colors?: Array<number | undefined>; size: number; x: number; y: number },
- metrics: { colors?: Metric[]; size: Metric; x: Metric; y: Metric },
- ) {
- const inner = [
- [component.name, isProject(component.qualifier) ? component.branch : undefined]
- .filter((s) => !!s)
- .join(' / '),
- `${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`,
- `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
- `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`,
- ].filter((s) => !!s);
- const { colors: valuesColors } = values;
- const { colors: metricColors } = metrics;
- if (valuesColors && metricColors) {
- metricColors.forEach((metric, idx) => {
- const colorValue = valuesColors[idx];
- if (colorValue || colorValue === 0) {
- inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`);
- }
- });
- }
- return (
- <div className="sw-text-left">
- {inner.map((line, index) => (
- <React.Fragment key={index}>
- {line}
- {index < inner.length - 1 && <br />}
- </React.Fragment>
- ))}
- </div>
- );
- }
-
- handleRatingFilterClick = (selection: number) => {
- this.setState(({ ratingFilters }) => {
- return { ratingFilters: { ...ratingFilters, [selection]: !ratingFilters[selection] } };
+ const handleRatingFilterClick = (selection: number) => {
+ setRatingFilters((ratingFilters) => {
+ return { ...ratingFilters, [selection]: !ratingFilters[selection] };
});
};
- handleBubbleClick = (component: ComponentMeasureEnhanced) => this.props.updateSelected(component);
-
- getDescription(domain: string) {
- const description = `component_measures.overview.${domain}.description`;
- const translatedDescription = translate(description);
- if (description === translatedDescription) {
- return null;
- }
- return translatedDescription;
- }
-
- renderBubbleChart(metrics: { colors?: Metric[]; size: Metric; x: Metric; y: Metric }) {
- const { ratingFilters } = this.state;
-
- const items = this.props.components
+ const renderBubbleChart = () => {
+ const items = components
.map((component) => {
- const x = this.getMeasureVal(component, metrics.x);
- const y = this.getMeasureVal(component, metrics.y);
- const size = this.getMeasureVal(component, metrics.size);
- const colors = metrics.colors?.map((metric) => this.getMeasureVal(component, metric));
+ const x = getMeasureVal(component, bubbleMetrics.x);
+ const y = getMeasureVal(component, bubbleMetrics.y);
+ const size = getMeasureVal(component, bubbleMetrics.size);
+ const colors = bubbleMetrics.colors?.map((metric) => getMeasureVal(component, metric));
if ((!x && x !== 0) || (!y && y !== 0) || (!size && size !== 0)) {
return undefined;
}
x,
y,
size,
- color: (colorRating as BubbleColorVal) ?? 0,
+ backgroundColor: themeColor(
+ `bubble.${isLegacy ? 'legacy.' : ''}${colorRating as BubbleColorVal}`,
+ )({
+ theme,
+ }),
+ borderColor: themeContrast(
+ `bubble.${isLegacy ? 'legacy.' : ''}${colorRating as BubbleColorVal}`,
+ )({
+ theme,
+ }),
data: component,
- tooltip: this.getTooltip(component, { x, y, size, colors }, metrics),
+ tooltip: getTooltip(component, { x, y, size, colors }, bubbleMetrics),
};
})
.filter(isDefined);
- const formatXTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.x.type);
- const formatYTick = (tick: string | number | undefined) => formatMeasure(tick, metrics.y.type);
+ const formatXTick = (tick: string | number | undefined) =>
+ formatMeasure(tick, bubbleMetrics.x.type);
+ const formatYTick = (tick: string | number | undefined) =>
+ formatMeasure(tick, bubbleMetrics.y.type);
let xDomain: [number, number] | undefined;
if (items.reduce((acc, item) => acc + item.x, 0) === 0) {
formatYTick={formatYTick}
height={HEIGHT}
items={items}
- onBubbleClick={this.handleBubbleClick}
+ onBubbleClick={(component: ComponentMeasureEnhanced) => updateSelected(component)}
padding={[0, 4, 50, 100]}
- yDomain={getBubbleYDomain(this.props.domain)}
+ yDomain={getBubbleYDomain(bubblesByDomain, domain)}
xDomain={xDomain}
/>
);
- }
-
- renderChartHeader(domain: string, sizeMetric: Metric, colorsMetric?: Metric[]) {
- const { ratingFilters } = this.state;
- const { paging, component, branchLike, metrics: propsMetrics } = this.props;
- const metrics = getBubbleMetrics(domain, propsMetrics);
+ };
+ const renderChartHeader = () => {
const title = isProjectOverview(domain)
? translate('component_measures.overview', domain, 'title')
: translateWithParameters(
<div>
<div className="sw-flex sw-items-center sw-whitespace-nowrap">
<Highlight className="it__measure-overview-bubble-chart-title">{title}</Highlight>
- <HelpTooltip className="sw-ml-2" overlay={this.getDescription(domain)}>
+ <HelpTooltip className="sw-ml-2" overlay={getDescription(domain)}>
<HelperHintIcon />
</HelpTooltip>
</div>
to={getComponentDrilldownUrl({
componentKey: component.key,
branchLike,
- metric: isProjectOverview(domain) ? MetricKey.violations : metrics.size.key,
+ metric: isProjectOverview(domain) ? MetricKey.violations : bubbleMetrics.size.key,
listView: true,
})}
>
<div className="sw-flex sw-flex-col sw-items-end">
<div className="sw-text-right">
- {colorsMetric && (
+ {bubbleMetrics.colors && (
<span className="sw-mr-3">
<strong className="sw-body-sm-highlight">
{translate('component_measures.legend.color')}
</strong>{' '}
- {colorsMetric.length > 1
+ {bubbleMetrics.colors.length > 1
? translateWithParameters(
'component_measures.legend.worse_of_x_y',
- ...colorsMetric.map((metric) => getLocalizedMetricName(metric)),
+ ...bubbleMetrics.colors.map((metric) => getLocalizedMetricName(metric)),
)
- : getLocalizedMetricName(colorsMetric[0])}
+ : getLocalizedMetricName(bubbleMetrics.colors[0])}
</span>
)}
<strong className="sw-body-sm-highlight">
{translate('component_measures.legend.size')}
</strong>{' '}
- {getLocalizedMetricName(sizeMetric)}
+ {getLocalizedMetricName(bubbleMetrics.size)}
</div>
- {colorsMetric && (
+ {bubbleMetrics.colors && (
<ColorRatingsLegend
className="sw-mt-2"
filters={ratingFilters}
- onRatingClick={this.handleRatingFilterClick}
+ onRatingClick={handleRatingFilterClick}
/>
)}
</div>
</div>
);
+ };
+
+ if (components.length <= 0) {
+ return <EmptyResult />;
}
- render() {
- if (this.props.components.length <= 0) {
- return <EmptyResult />;
- }
- const { domain } = this.props;
- const metrics = getBubbleMetrics(domain, this.props.metrics);
+ return (
+ <BubbleChartWrapper className="sw-relative sw-body-sm">
+ {renderChartHeader()}
+ {renderBubbleChart()}
+ <div className="sw-text-center">{getLocalizedMetricName(bubbleMetrics.x)}</div>
+ <YAxis className="sw-absolute sw-top-1/2 sw-left-3">
+ {getLocalizedMetricName(bubbleMetrics.y)}
+ </YAxis>
+ </BubbleChartWrapper>
+ );
+}
- return (
- <BubbleChartWrapper className="sw-relative sw-body-sm">
- {this.renderChartHeader(domain, metrics.size, metrics.colors)}
- {this.renderBubbleChart(metrics)}
- <div className="sw-text-center">{getLocalizedMetricName(metrics.x)}</div>
- <YAxis className="sw-absolute sw-top-1/2 sw-left-3">
- {getLocalizedMetricName(metrics.y)}
- </YAxis>
- </BubbleChartWrapper>
- );
+const getDescription = (domain: string) => {
+ const description = `component_measures.overview.${domain}.description`;
+ const translatedDescription = translate(description);
+ if (description === translatedDescription) {
+ return null;
}
-}
+ return translatedDescription;
+};
+
+const getMeasureVal = (component: ComponentMeasureEnhanced, metric: Metric) => {
+ const measure = component.measures.find((measure) => measure.metric.key === metric.key);
+ if (!measure) {
+ return undefined;
+ }
+ return Number(
+ getCCTMeasureValue(metric.key, isDiffMetric(metric.key) ? measure.leak : measure.value),
+ );
+};
+
+const getTooltip = (
+ component: ComponentMeasureEnhanced,
+ values: { colors?: Array<number | undefined>; size: number; x: number; y: number },
+ metrics: { colors?: Metric[]; size: Metric; x: Metric; y: Metric },
+) => {
+ const inner = [
+ [component.name, isProject(component.qualifier) ? component.branch : undefined]
+ .filter((s) => !!s)
+ .join(' / '),
+ `${metrics.x.name}: ${formatMeasure(values.x, metrics.x.type)}`,
+ `${metrics.y.name}: ${formatMeasure(values.y, metrics.y.type)}`,
+ `${metrics.size.name}: ${formatMeasure(values.size, metrics.size.type)}`,
+ ].filter((s) => !!s);
+ const { colors: valuesColors } = values;
+ const { colors: metricColors } = metrics;
+ if (valuesColors && metricColors) {
+ metricColors.forEach((metric, idx) => {
+ const colorValue = valuesColors[idx];
+ if (colorValue || colorValue === 0) {
+ inner.push(`${metric.name}: ${formatMeasure(colorValue, metric.type)}`);
+ }
+ });
+ }
+ return (
+ <div className="sw-text-left">
+ {inner.map((line, index) => (
+ <React.Fragment key={index}>
+ {line}
+ {index < inner.length - 1 && <br />}
+ </React.Fragment>
+ ))}
+ </div>
+ );
+};
const BubbleChartWrapper = styled.div`
color: ${themeColor('pageContentLight')};
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ColorFilterOption, ColorsLegend } from 'design-system';
+import { useTheme } from '@emotion/react';
+import {
+ BubbleColorVal,
+ ColorFilterOption,
+ ColorsLegend,
+ themeColor,
+ themeContrast,
+} from 'design-system';
import * as React from 'react';
import { formatMeasure } from '~sonar-aligned/helpers/measures';
import { MetricType } from '~sonar-aligned/types/metrics';
import { translateWithParameters } from '../../../helpers/l10n';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
export interface ColorRatingsLegendProps {
className?: string;
onRatingClick: (selection: number) => void;
}
-const RATINGS = [1, 2, 3, 4, 5];
-
export default function ColorRatingsLegend(props: ColorRatingsLegendProps) {
+ const { data: isLegacy } = useIsLegacyCCTMode();
+ const theme = useTheme();
+ const RATINGS = isLegacy ? [1, 2, 3, 4, 5] : [1, 2, 3, 4];
+
const { className, filters } = props;
- const ratingsColors = RATINGS.map((rating) => {
+ const ratingsColors = RATINGS.map((rating: BubbleColorVal) => {
const formattedMeasure = formatMeasure(rating, MetricType.Rating);
return {
overlay: translateWithParameters('component_measures.legend.help_x', formattedMeasure),
label: formattedMeasure,
value: rating,
selected: !filters[rating],
+ backgroundColor: themeColor(isLegacy ? `bubble.legacy.${rating}` : `bubble.${rating}`)({
+ theme,
+ }),
+ borderColor: themeContrast(isLegacy ? `bubble.legacy.${rating}` : `bubble.${rating}`)({
+ theme,
+ }),
};
});
view={props.view}
/>
- <MeasureCell component={component} metric={metric} />
+ <MeasureCell branchLike={branchLike} component={component} metric={metric} />
{otherMetrics.map((metric) => (
<MeasureCell
+ branchLike={branchLike}
key={metric.key}
component={component}
measure={component.measures.find((measure) => measure.metric.key === metric.key)}
import { isDiffMetric, isPeriodBestValue } from '../../../helpers/measures';
import { BranchLike } from '../../../types/branch-like';
import { MeasurePageView } from '../../../types/measures';
-import {
- ComponentMeasure,
- ComponentMeasureEnhanced,
- Dict,
- Metric,
- Paging,
-} from '../../../types/types';
+import { Component, ComponentMeasureEnhanced, Dict, Metric, Paging } from '../../../types/types';
import ComponentsList from './ComponentsList';
interface Props {
metric: Metric;
metrics: Dict<Metric>;
paging?: Paging;
- rootComponent: ComponentMeasure;
+ rootComponent: Component;
selectedComponent?: ComponentMeasureEnhanced;
selectedIdx?: number;
view: MeasurePageView;
import * as React from 'react';
import Measure from '~sonar-aligned/components/measure/Measure';
import { getCCTMeasureValue, isDiffMetric } from '../../../helpers/measures';
+import { BranchLike } from '../../../types/branch-like';
import { ComponentMeasureEnhanced, MeasureEnhanced, Metric } from '../../../types/types';
interface Props {
+ branchLike?: BranchLike;
component: ComponentMeasureEnhanced;
measure?: MeasureEnhanced;
metric: Metric;
}
-export default function MeasureCell({ component, measure, metric }: Readonly<Props>) {
+export default function MeasureCell({ component, measure, metric, branchLike }: Readonly<Props>) {
const getValue = (item: { leak?: string; value?: string }) =>
isDiffMetric(metric.key) ? item.leak : item.value;
return (
<NumericalCell className="sw-py-3">
<Measure
+ branchLike={branchLike}
componentKey={component.key}
metricKey={metric.key}
metricType={metric.type}
interface TreeMapViewProps {
components: ComponentMeasureEnhanced[];
handleSelect: (component: ComponentMeasureIntern) => void;
+ isLegacyMode: boolean;
metric: Metric;
}
}
const PERCENT_SCALE_DOMAIN = [0, 25, 50, 75, 100];
-const RATING_SCALE_DOMAIN = [1, 2, 3, 4, 5];
+const RATING_SCALE_DOMAIN = [1, 2, 3, 4];
+const LEGACY_RATING_SCALE_DOMAIN = [1, 2, 3, 4, 5];
const HEIGHT = 500;
const NA_COLORS: [ThemeColors, ThemeColors] = ['treeMap.NA1', 'treeMap.NA2'];
'treeMap.D',
'treeMap.E',
];
+const TREEMAP_LEGACY_COLORS: ThemeColors[] = [
+ 'treeMap.legacy.A',
+ 'treeMap.legacy.B',
+ 'treeMap.legacy.C',
+ 'treeMap.legacy.D',
+ 'treeMap.legacy.E',
+];
export class TreeMapView extends React.PureComponent<Props, State> {
state: State;
};
getMappedThemeColors = (): string[] => {
- const { theme } = this.props;
- return TREEMAP_COLORS.map((c) => themeColor(c)({ theme }));
+ const { theme, isLegacyMode } = this.props;
+ return (isLegacyMode ? TREEMAP_LEGACY_COLORS : TREEMAP_COLORS).map((c) =>
+ themeColor(c)({ theme }),
+ );
};
getLevelColorScale = () =>
return color;
};
- getRatingColorScale = () =>
- scaleLinear<string, string>().domain(RATING_SCALE_DOMAIN).range(this.getMappedThemeColors());
+ getRatingColorScale = () => {
+ const { isLegacyMode } = this.props;
+ return scaleLinear<string, string>()
+ .domain(isLegacyMode ? LEGACY_RATING_SCALE_DOMAIN : RATING_SCALE_DOMAIN)
+ .range(this.getMappedThemeColors());
+ };
getColorScale = (metric: Metric) => {
if (metric.type === MetricType.Level) {
--- /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 { areCCTMeasuresComputed, areSoftwareQualityRatingsComputed } from '../../helpers/measures';
+import { useIsLegacyCCTMode } from '../../queries/settings';
+import { MeasureEnhanced } from '../../types/types';
+import {
+ legacyBubbles,
+ newTaxonomyBubbles,
+ newTaxonomyWithoutRatingsBubbles,
+} from './config/bubbles';
+
+export function useBubbleChartMetrics(measures: MeasureEnhanced[]) {
+ const { data: isLegacyFlag } = useIsLegacyCCTMode();
+
+ if (isLegacyFlag || !areCCTMeasuresComputed(measures)) {
+ return legacyBubbles;
+ }
+
+ if (!areSoftwareQualityRatingsComputed(measures)) {
+ return newTaxonomyWithoutRatingsBubbles;
+ }
+
+ return newTaxonomyBubbles;
+}
translate,
} from '../../../helpers/l10n';
import { MeasureEnhanced } from '../../../types/types';
+import { useBubbleChartMetrics } from '../hooks';
import {
addMeasureCategories,
getMetricSubnavigationName,
interface Props {
componentKey: string;
domain: { measures: MeasureEnhanced[]; name: string };
+ measures: MeasureEnhanced[];
onChange: (metric: string) => void;
open: boolean;
selected: string;
}
export default function DomainSubnavigation(props: Readonly<Props>) {
- const { componentKey, domain, onChange, open, selected, showFullMeasures } = props;
+ const { componentKey, domain, onChange, open, selected, showFullMeasures, measures } = props;
const helperMessageKey = `component_measures.domain_subnavigation.${domain.name}.help`;
const helper = hasMessage(helperMessageKey) ? translate(helperMessageKey) : undefined;
const items = addMeasureCategories(domain.name, domain.measures);
+ const bubbles = useBubbleChartMetrics(measures);
const hasCategories = items.some((item) => typeof item === 'string');
const translateMetric = hasCategories ? getLocalizedCategoryMetricName : getLocalizedMetricName;
let sortedItems = sortMeasures(domain.name, items);
const hasOverview = (domain: string) => {
- return showFullMeasures && hasBubbleChart(domain);
+ return showFullMeasures && hasBubbleChart(bubbles, domain);
};
// sortedItems contains both measures (type object) and categories (type string)
domain={domain}
key={domain.name}
onChange={handleChangeMetric}
+ measures={measures}
open={isDomainSelected(selectedMetric, domain)}
selected={selectedMetric}
showFullMeasures={showFullMeasures}
import React from 'react';
import Measure from '~sonar-aligned/components/measure/Measure';
import { isDiffMetric } from '../../../helpers/measures';
+import { useBranchesQuery } from '../../../queries/branch';
import { MeasureEnhanced } from '../../../types/types';
interface Props {
export default function SubnavigationMeasureValue({ measure, componentKey }: Readonly<Props>) {
const isDiff = isDiffMetric(measure.metric.key);
+ const { data: { branchLike } = {} } = useBranchesQuery();
const value = isDiff ? measure.leak : measure.value;
return (
id={`measure-${measure.metric.key}-${isDiff ? 'leak' : 'value'}`}
>
<Measure
+ branchLike={branchLike}
componentKey={componentKey}
badgeSize="xs"
metricKey={measure.metric.key}
HIDDEN_METRICS,
LEAK_CCT_SOFTWARE_QUALITY_METRICS,
LEAK_OLD_TAXONOMY_METRICS,
+ LEAK_OLD_TAXONOMY_RATINGS,
OLD_TAXONOMY_METRICS,
+ OLD_TAXONOMY_RATINGS,
+ SOFTWARE_QUALITY_RATING_METRICS,
} from '../../helpers/constants';
import { getLocalizedMetricName, translate } from '../../helpers/l10n';
import {
- MEASURES_REDIRECTION,
areCCTMeasuresComputed,
areLeakCCTMeasuresComputed,
+ areSoftwareQualityRatingsComputed,
getCCTMeasureValue,
getDisplayMetrics,
isDiffMetric,
+ MEASURES_REDIRECTION,
} from '../../helpers/measures';
import {
cleanQuery,
MeasureEnhanced,
Metric,
} from '../../types/types';
-import { bubbles } from './config/bubbles';
+import { BubblesByDomain } from './config/bubbles';
import { domains } from './config/domains';
export const BUBBLES_FETCH_LIMIT = 500;
(measure) => !LEAK_OLD_TAXONOMY_METRICS.includes(measure.metric.key as MetricKey),
);
}
+
+ // Both new and overall code will exist after next analysis
+ if (areSoftwareQualityRatingsComputed(measures)) {
+ populatedMeasures = populatedMeasures.filter(
+ (measure) =>
+ !OLD_TAXONOMY_RATINGS.includes(measure.metric.key as MetricKey) &&
+ !LEAK_OLD_TAXONOMY_RATINGS.includes(measure.metric.key as MetricKey),
+ );
+ }
+
if (areCCTMeasuresComputed(measures)) {
populatedMeasures = populatedMeasures.filter(
(measure) => !OLD_TAXONOMY_METRICS.includes(measure.metric.key as MetricKey),
);
}
-export function hasBubbleChart(domainName: string): boolean {
- return bubbles[domainName] !== undefined;
+export function hasBubbleChart(bubblesByDomain: BubblesByDomain, domainName: string): boolean {
+ return bubblesByDomain[domainName] !== undefined;
}
export function hasFacetStat(metric: string): boolean {
}
export function getMeasuresPageMetricKeys(metrics: Dict<Metric>, branch?: BranchLike) {
- const metricKeys = getDisplayMetrics(Object.values(metrics)).map((metric) => metric.key);
+ // ToDo rollback once new metrics are available
+ const metricKeys = [
+ ...getDisplayMetrics(Object.values(metrics)).map((metric) => metric.key),
+ ...SOFTWARE_QUALITY_RATING_METRICS,
+ ];
if (isPullRequest(branch)) {
return metricKeys.filter((key) => isDiffMetric(key));
return metricKeys;
}
-export function getBubbleMetrics(domain: string, metrics: Dict<Metric>) {
- const conf = bubbles[domain];
+export function getBubbleMetrics(
+ bubblesByDomain: BubblesByDomain,
+ domain: string,
+ metrics: Dict<Metric>,
+) {
+ const conf = bubblesByDomain[domain];
return {
x: metrics[conf.x],
y: metrics[conf.y],
};
}
-export function getBubbleYDomain(domain: string) {
- return bubbles[domain].yDomain;
+export function getBubbleYDomain(bubblesByDomain: BubblesByDomain, domain: string) {
+ return bubblesByDomain[domain].yDomain;
}
export function isProjectOverview(metric: string) {
icon={
newSecurityReviewRating ? (
<RatingComponent
+ branchLike={branch}
componentKey={component.key}
ratingMetric={MetricKey.new_security_review_rating}
size="md"
icon={
securityRating ? (
<RatingComponent
+ branchLike={branch}
componentKey={component.key}
ratingMetric={MetricKey.security_review_rating}
size="md"
};
render() {
- const { condition, component } = this.props;
+ const { condition, component, branchLike } = this.props;
const { measure } = condition;
const { metric } = measure;
return this.wrapWithLink(
<div className="sw-flex sw-items-center sw-p-2">
<MeasureIndicator
+ branchLike={branchLike}
className="sw-flex sw-justify-center sw-w-6 sw-mx-4"
decimals={2}
componentKey={component.key}
<div className="sw-flex-grow sw-flex sw-justify-end">
<SoftwareImpactMeasureRating
+ branch={branch}
softwareQuality={softwareQuality}
componentKey={component.key}
ratingMetricKey={ratingMetricKey}
import { useIntl } from 'react-intl';
import RatingComponent from '../../../app/components/metrics/RatingComponent';
import { MetricKey } from '../../../sonar-aligned/types/metrics';
+import { Branch } from '../../../types/branch-like';
import { SoftwareImpactSeverity, SoftwareQuality } from '../../../types/clean-code-taxonomy';
export interface SoftwareImpactMeasureRatingProps {
+ branch?: Branch;
componentKey: string;
ratingMetricKey: MetricKey;
softwareQuality: SoftwareQuality;
}
export function SoftwareImpactMeasureRating(props: Readonly<SoftwareImpactMeasureRatingProps>) {
- const { ratingMetricKey, componentKey, softwareQuality } = props;
+ const { ratingMetricKey, componentKey, softwareQuality, branch } = props;
const intl = useIntl();
return (
<RatingComponent
+ branchLike={branch}
size="md"
className="sw-text-sm"
ratingMetric={ratingMetricKey}
import { BasicSeparator, CenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
import { uniq } from 'lodash';
import * as React from 'react';
-import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { useBranchStatusQuery } from '../../../queries/branch';
-import { useComponentMeasuresWithMetricsQuery } from '../../../queries/component';
+import { useMeasuresComponentQuery } from '../../../queries/measures';
import { useComponentQualityGateQuery } from '../../../queries/quality-gates';
import { PullRequest } from '../../../types/branch-like';
import { Component } from '../../../types/types';
component.key,
);
- const { data: componentMeasures, isLoading: isLoadingMeasures } =
- useComponentMeasuresWithMetricsQuery(
- component.key,
- uniq([...PR_METRICS, ...(conditions?.map((c) => c.metric) ?? [])]),
- getBranchLikeQuery(pullRequest),
- !isLoadingBranchStatusesData,
- );
+ const { data: componentMeasures, isLoading: isLoadingMeasures } = useMeasuresComponentQuery(
+ {
+ componentKey: component.key,
+ metricKeys: uniq([...PR_METRICS, ...(conditions?.map((c) => c.metric) ?? [])]),
+ branchLike: pullRequest,
+ },
+ { enabled: !isLoadingBranchStatusesData },
+ );
const measures = componentMeasures
? enhanceMeasuresWithMetrics(
{component && (
<Measure
+ branchLike={branchLike}
className="it__hs-review-percentage sw-body-sm-highlight sw-ml-2"
componentKey={component.key}
metricKey={
import * as React from 'react';
import Measure from '~sonar-aligned/components/measure/Measure';
import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
+import { BranchLike } from '../../types/branch-like';
import { duplicationRatingConverter } from './utils';
interface Props {
+ branchLike?: BranchLike;
className?: string;
componentKey: string;
decimals?: number;
MetricKey.new_code_smells,
];
+export const OLD_TAXONOMY_RATINGS = [
+ MetricKey.sqale_rating,
+ MetricKey.security_rating,
+ MetricKey.reliability_rating,
+ MetricKey.security_review_rating,
+];
+
+export const LEAK_OLD_TAXONOMY_RATINGS = [
+ MetricKey.new_maintainability_rating,
+ MetricKey.new_security_rating,
+ MetricKey.new_reliability_rating,
+ MetricKey.new_security_review_rating,
+];
+
export const OLD_TO_NEW_TAXONOMY_METRICS_MAP: { [key in MetricKey]?: MetricKey } = {
[MetricKey.vulnerabilities]: MetricKey.security_issues,
[MetricKey.bugs]: MetricKey.reliability_issues,
MetricKey.confirmed_issues,
];
+export const SOFTWARE_QUALITY_RATING_METRICS_MAP: Record<string, MetricKey> = {
+ [MetricKey.sqale_rating]: MetricKey.sqale_rating_new,
+ [MetricKey.security_rating]: MetricKey.security_rating_new,
+ [MetricKey.reliability_rating]: MetricKey.reliability_rating_new,
+ [MetricKey.security_review_rating]: MetricKey.security_review_rating_new,
+ [MetricKey.releasability_rating]: MetricKey.releasability_rating_new,
+ [MetricKey.new_maintainability_rating]: MetricKey.new_maintainability_rating_new,
+ [MetricKey.new_security_rating]: MetricKey.new_security_rating_new,
+ [MetricKey.new_reliability_rating]: MetricKey.new_reliability_rating_new,
+ [MetricKey.new_security_review_rating]: MetricKey.new_security_review_rating_new,
+};
+
+export const SOFTWARE_QUALITY_RATING_METRICS = [
+ MetricKey.sqale_rating_new,
+ MetricKey.security_rating_new,
+ MetricKey.reliability_rating_new,
+ MetricKey.security_review_rating_new,
+ MetricKey.releasability_rating_new,
+ MetricKey.new_maintainability_rating_new,
+ MetricKey.new_security_rating_new,
+ MetricKey.new_reliability_rating_new,
+ MetricKey.new_security_review_rating_new,
+];
+
export const PROJECT_KEY_MAX_LEN = 400;
export const IMPORT_COMPATIBLE_ALMS = [
CCT_SOFTWARE_QUALITY_METRICS,
LEAK_CCT_SOFTWARE_QUALITY_METRICS,
LEAK_OLD_TAXONOMY_METRICS,
+ SOFTWARE_QUALITY_RATING_METRICS,
} from './constants';
import { translate } from './l10n';
import { isDefined } from './types';
),
);
}
+export function areSoftwareQualityRatingsComputed(measures?: Measure[] | MeasureEnhanced[]) {
+ return SOFTWARE_QUALITY_RATING_METRICS.every((metric) =>
+ measures?.find((measure) =>
+ isMeasureEnhanced(measure) ? measure.metric.key === metric : measure.metric === metric,
+ ),
+ );
+}
export function areLeakAndOverallCCTMeasuresComputed(measures?: Measure[] | MeasureEnhanced[]) {
return areLeakCCTMeasuresComputed(measures) && areCCTMeasuresComputed(measures);
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import {
- UseQueryResult,
infiniteQueryOptions,
queryOptions,
useQuery,
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, Measure } from '../types/types';
import { StaleTime, createInfiniteQueryHook, createQueryHook } from './common';
});
}
-export function useComponentMeasuresWithMetricsQuery(
- key: string,
- metricKeys: string[],
- branchParameters: BranchParameters,
- enabled = true,
-): UseQueryResult<MeasuresAndMetaWithMetrics> {
- return useQuery({
- enabled,
- 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();
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';
-import { groupBy } from 'lodash';
+import {
+ infiniteQueryOptions,
+ queryOptions,
+ useQuery,
+ useQueryClient,
+} from '@tanstack/react-query';
+import { groupBy, isUndefined, omitBy } from 'lodash';
import { BranchParameters } from '~sonar-aligned/types/branch-like';
+import { getComponentTree } from '../api/components';
import {
getMeasures,
getMeasuresForProjects,
getMeasuresWithPeriodAndMetrics,
} from '../api/measures';
import { getAllTimeMachineData } from '../api/time-machine';
+import { SOFTWARE_QUALITY_RATING_METRICS } from '../helpers/constants';
+import { getNextPageParam, getPreviousPageParam } from '../helpers/react-query';
+import { getBranchLikeQuery } from '../sonar-aligned/helpers/branch-like';
import { MetricKey } from '../sonar-aligned/types/metrics';
+import { BranchLike } from '../types/branch-like';
import { Measure } from '../types/types';
-import { createQueryHook } from './common';
+import {createInfiniteQueryHook, createQueryHook} from './common';
const NEW_METRICS = [
MetricKey.software_quality_maintainability_rating,
});
}
+export const useMeasuresComponentQuery = createQueryHook(
+ ({
+ componentKey,
+ metricKeys,
+ branchLike,
+ }: {
+ branchLike?: BranchLike;
+ componentKey: string;
+ metricKeys: string[];
+ }) => {
+ const queryClient = useQueryClient();
+ const branchLikeQuery = getBranchLikeQuery(branchLike);
+
+ return queryOptions({
+ queryKey: ['measures', 'component', componentKey, 'branchLike', branchLikeQuery, metricKeys],
+ queryFn: async () => {
+ const data = await getMeasuresWithPeriodAndMetrics(
+ componentKey,
+ metricKeys.filter((m) => !SOFTWARE_QUALITY_RATING_METRICS.includes(m as MetricKey)),
+ branchLikeQuery,
+ );
+ metricKeys.forEach((metricKey) => {
+ const measure =
+ data.component.measures?.find((measure) => measure.metric === metricKey) ?? null;
+ queryClient.setQueryData<Measure | null>(
+ ['measures', 'details', componentKey, 'branchLike', branchLikeQuery, metricKey],
+ measure,
+ );
+ });
+
+ return data;
+ },
+ });
+ },
+);
+
+export const useComponentTreeQuery = createInfiniteQueryHook(
+ ({
+ strategy,
+ component,
+ metrics,
+ additionalData,
+ }: {
+ additionalData: Parameters<typeof getComponentTree>[3];
+ component: Parameters<typeof getComponentTree>[1];
+ metrics: Parameters<typeof getComponentTree>[2];
+ strategy: 'children' | 'leaves';
+ }) => {
+ const branchLikeQuery = omitBy(
+ {
+ branch: additionalData?.branch,
+ pullRequest: additionalData?.pullRequest,
+ },
+ isUndefined,
+ );
+
+ const queryClient = useQueryClient();
+ return infiniteQueryOptions({
+ queryKey: ['component', component, 'tree', strategy, { metrics, additionalData }],
+ queryFn: async ({ pageParam }) => {
+ const result = await getComponentTree(
+ strategy,
+ component,
+ metrics?.filter((m) => !SOFTWARE_QUALITY_RATING_METRICS.includes(m as MetricKey)),
+ { ...additionalData, p: pageParam, ...branchLikeQuery },
+ );
+
+ // const measuresMapByMetricKeyForBaseComponent = groupBy(
+ // result.baseComponent.measures,
+ // 'metric',
+ // );
+ // metrics?.forEach((metricKey) => {
+ // const measure = measuresMapByMetricKeyForBaseComponent[metricKey]?.[0] ?? null;
+ // queryClient.setQueryData<Measure>(
+ // [
+ // 'measures',
+ // 'details',
+ // result.baseComponent.key,
+ // 'branchLike',
+ // branchLikeQuery,
+ // 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, 'branchLike', branchLikeQuery, metricKey],
+ measure,
+ );
+ });
+ });
+ return result;
+ },
+ getNextPageParam: (data) => getNextPageParam({ page: data.paging }),
+ getPreviousPageParam: (data) => getPreviousPageParam({ page: data.paging }),
+ initialPageParam: 1,
+ staleTime: 60_000,
+ });
+ },
+);
+
export const useMeasuresForProjectsQuery = createQueryHook(
({ projectKeys, metricKeys }: { metricKeys: string[]; projectKeys: string[] }) => {
const queryClient = useQueryClient();
+
return queryOptions({
queryKey: ['measures', 'list', 'projects', projectKeys, metricKeys],
queryFn: async () => {
// TODO remove this once all metrics are supported
const filteredMetricKeys = metricKeys.filter(
- (metricKey) => !NEW_METRICS.includes(metricKey as MetricKey),
+ (metricKey) => !SOFTWARE_QUALITY_RATING_METRICS.includes(metricKey as MetricKey),
);
const measures = await getMeasuresForProjects(projectKeys, filteredMetricKeys);
const measuresMapByProjectKey = groupBy(measures, 'component');
metricKeys.forEach((metricKey) => {
const measure = measuresMapByMetricKey[metricKey]?.[0] ?? null;
queryClient.setQueryData<Measure>(
- ['measures', 'details', projectKey, metricKey],
+ ['measures', 'details', projectKey, 'branchLike', {}, metricKey],
measure,
);
});
);
export const useMeasureQuery = createQueryHook(
- ({ componentKey, metricKey }: { componentKey: string; metricKey: string }) => {
+ ({
+ componentKey,
+ metricKey,
+ branchLike,
+ }: {
+ branchLike?: BranchLike;
+ componentKey: string;
+ metricKey: string;
+ }) => {
+ const branchLikeQuery = getBranchLikeQuery(branchLike);
+
return queryOptions({
- queryKey: ['measures', 'details', componentKey, metricKey],
+ queryKey: ['measures', 'details', componentKey, 'branchLike', branchLikeQuery, metricKey],
queryFn: () =>
getMeasures({ component: componentKey, metricKeys: metricKey }).then(
(measures) => measures[0] ?? null,
import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
import RatingComponent from '../../../app/components/metrics/RatingComponent';
import RatingTooltipContent from '../../../components/measure/RatingTooltipContent';
+import { BranchLike } from '../../../types/branch-like';
interface Props {
badgeSize?: 'xs' | 'sm' | 'md';
+ branchLike?: BranchLike;
className?: string;
componentKey: string;
decimals?: number;
decimals,
fontClassName,
metricKey,
+ branchLike,
metricType,
small,
value,
const rating = (
<RatingComponent
+ branchLike={branchLike}
size={badgeSize ?? small ? 'sm' : 'md'}
getLabel={getLabel}
getTooltip={getTooltip}
new_reliability_rating_distribution = 'new_reliability_rating_distribution',
new_software_quality_reliability_rating_distribution = 'new_software_quality_reliability_rating_distribution',
new_reliability_remediation_effort = 'new_reliability_remediation_effort',
+ new_reliability_remediation_effort_new = 'new_reliability_remediation_effort_new',
new_security_hotspots = 'new_security_hotspots',
new_security_hotspots_reviewed = 'new_security_hotspots_reviewed',
new_security_issues = 'new_security_issues',
new_security_rating_distribution = 'new_security_rating_distribution',
new_software_quality_security_rating_distribution = 'new_software_quality_security_rating_distribution',
new_security_remediation_effort = 'new_security_remediation_effort',
+ new_security_remediation_effort_new = 'new_security_remediation_effort_new',
new_security_review_rating = 'new_security_review_rating',
new_software_quality_security_review_rating = 'new_software_quality_security_review_rating',
new_security_review_rating_distribution = 'new_security_review_rating_distribution',
software_quality_reliability_rating_distribution = 'software_quality_reliability_rating_distribution',
reliability_rating_effort = 'reliability_rating_effort',
reliability_remediation_effort = 'reliability_remediation_effort',
+ reliability_remediation_effort_new = 'reliability_remediation_effort_new',
reopened_issues = 'reopened_issues',
security_hotspots = 'security_hotspots',
security_hotspots_reviewed = 'security_hotspots_reviewed',
software_quality_security_rating_distribution = 'software_quality_security_rating_distribution',
security_rating_effort = 'security_rating_effort',
security_remediation_effort = 'security_remediation_effort',
+ security_remediation_effort_new = 'security_remediation_effort_new',
security_review_rating = 'security_review_rating',
software_quality_security_review_rating = 'software_quality_security_review_rating',
security_review_rating_distribution = 'security_review_rating_distribution',