classNames={{
container: () => 'sw-relative sw-inline-block sw-align-middle',
placeholder: () => 'sw-truncate sw-leading-4',
- menu: () => 'sw-w-auto',
menuList: () => 'sw-overflow-y-auto sw-py-2 sw-max-h-[12.25rem]',
control: ({ isDisabled }) =>
classNames(
- 'sw-absolut sw-box-border sw-rounded-2 sw-overflow-hidden sw-z-dropdown-menu',
+ 'sw-absolut sw-box-border sw-rounded-2 sw-overflow-hidden',
isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed'
),
+ menu: () => 'sw-z-dropdown-menu',
option: ({ isDisabled }) =>
classNames(
'sw-py-2 sw-px-3 sw-cursor-pointer',
isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed'
),
+ ...props.classNames,
}}
components={{
...props.components,
menu: (base) => ({
...base,
width: INPUT_SIZES[size],
- zIndex: 101,
}),
option: (base, { isFocused, isSelected }) => ({
...base,
import { KeyboardHintKeys } from './KeyboardHintKeys';
interface Props {
+ className?: string;
command: string;
title?: string;
}
-export function KeyboardHint({ title, command }: Props) {
+export function KeyboardHint({ title, command, className }: Props) {
const normalizedCommand = command
.replace(Key.Control, isMacOS() ? 'Command' : 'Control')
.replace(Key.Alt, isMacOS() ? 'Option' : 'Alt');
return (
- <Body>
+ <Body className={className}>
{title && <span className="sw-truncate">{title}</span>}
<KeyboardHintKeys command={normalizedCommand} />
</Body>
// Overview
seeDataAsListLink: byRole('link', { name: 'component_measures.overview.see_data_as_list' }),
bubbleChart: byTestId('bubble-chart'),
- newCodePeriodTxt: byText(
- 'overview.new_code_period_x.overview.period.previous_version_only_date'
- ),
+ newCodePeriodTxt: byText('component_measures.leak_legend.new_code'),
// Navigation
overviewDomainBtn: byRole('button', {
showAllBtn: byRole('button', {
name: 'component_measures.hidden_best_score_metrics_show_label',
}),
- goToActivityLink: byRole('link', { name: 'component_measures.show_metric_history' }),
+ goToActivityLink: byRole('link', { name: 'component_measures.see_metric_history' }),
};
const ui = {
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { ComponentQualifier } from '../../../types/component';
+import { MeasurePageView } from '../../../types/measures';
import { MetricKey } from '../../../types/metrics';
import { ComponentMeasure } from '../../../types/types';
import * as utils from '../utils';
describe('serializeQuery', () => {
it('should correctly serialize the query', () => {
- expect(utils.serializeQuery({ metric: '', selected: '', view: 'list' })).toEqual({
+ expect(utils.serializeQuery({ metric: '', selected: '', view: MeasurePageView.list })).toEqual({
view: 'list',
});
- expect(utils.serializeQuery({ metric: 'foo', selected: 'bar', view: 'tree' })).toEqual({
+ expect(
+ utils.serializeQuery({ metric: 'foo', selected: 'bar', view: MeasurePageView.tree })
+ ).toEqual({
metric: 'foo',
selected: 'bar',
});
});
it('should be memoized', () => {
- const query: utils.Query = { metric: 'foo', selected: 'bar', view: 'tree' };
+ const query: utils.Query = { metric: 'foo', selected: 'bar', view: MeasurePageView.tree };
expect(utils.serializeQuery(query)).toBe(utils.serializeQuery(query));
});
});
import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
+import { MeasurePageView } from '../../../types/measures';
import { MetricKey } from '../../../types/metrics';
import {
ComponentMeasure,
const metric = this.getSelectedMetric(query, false);
if (metric) {
- if (query.view === 'treemap' && !hasTreemap(metric.key, metric.type)) {
- query.view = 'tree';
- } else if (query.view === 'tree' && !hasTree(metric.key)) {
- query.view = 'list';
+ if (query.view === MeasurePageView.treemap && !hasTreemap(metric.key, metric.type)) {
+ query.view = MeasurePageView.tree;
+ } else if (query.view === MeasurePageView.tree && !hasTree(metric.key)) {
+ query.view = MeasurePageView.list;
}
}
}
return (
- <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
+ <StyledMain className="sw-rounded-1 sw-mb-4">
<MeasureContent
branchLike={branchLike}
leakPeriod={leakPeriod}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
+import styled from '@emotion/styled';
import { differenceInDays } from 'date-fns';
+import { Highlight, Note, themeBorder, themeColor } from 'design-system';
import * as React from 'react';
-import { injectIntl, WrappedComponentProps } from 'react-intl';
+import { WrappedComponentProps, injectIntl } from 'react-intl';
import Tooltip from '../../../components/controls/Tooltip';
import DateFormatter, { longFormatterOption } from '../../../components/intl/DateFormatter';
import DateFromNow from '../../../components/intl/DateFromNow';
import DateTimeFormatter, { formatterOption } from '../../../components/intl/DateTimeFormatter';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods';
+import { ComponentQualifier } from '../../../types/component';
import { ComponentMeasure, NewCodePeriodSettingType, Period } from '../../../types/types';
-interface Props {
- className?: string;
+export interface LeakPeriodLegendProps {
component: ComponentMeasure;
period: Period;
}
-export class LeakPeriodLegend extends React.PureComponent<Props & WrappedComponentProps> {
+class LeakPeriodLegend extends React.PureComponent<LeakPeriodLegendProps & WrappedComponentProps> {
formatDate = (date: string) => {
return this.props.intl.formatDate(date, longFormatterOption);
};
};
render() {
- const { className, component, period } = this.props;
- const leakClass = classNames('domain-measures-header leak-box', className);
- if (component.qualifier === 'APP') {
- return <div className={leakClass}>{translate('issues.new_code_period')}</div>;
+ const { component, period } = this.props;
+
+ if (component.qualifier === ComponentQualifier.Application) {
+ return (
+ <LeakPeriodLabel className="sw-px-2 sw-py-1 sw-rounded-1">
+ {translate('issues.new_code_period')}
+ </LeakPeriodLabel>
+ );
}
const leakPeriodLabel = getPeriodLabel(
period,
period.mode === 'manual_baseline' ? this.formatDateTime : this.formatDate
);
- if (!leakPeriodLabel) {
- return null;
- }
const label = (
- <div className={leakClass}>
- {translateWithParameters('overview.new_code_period_x', leakPeriodLabel)}
- </div>
+ <LeakPeriodLabel className="sw-px-2 sw-py-1 sw-rounded-1">
+ <Highlight>{translateWithParameters('component_measures.leak_legend.new_code')}</Highlight>{' '}
+ {leakPeriodLabel}
+ </LeakPeriodLabel>
);
if (period.mode === 'days' || period.mode === NewCodePeriodSettingType.NUMBER_OF_DAYS) {
}
export default injectIntl(LeakPeriodLegend);
+
+const LeakPeriodLabel = styled(Note)`
+ background-color: ${themeColor('newCodeLegend')};
+ border: ${themeBorder('default', 'newCodeLegendBorder')};
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Highlight, KeyboardHint } from 'design-system';
import * as React from 'react';
import { getComponentTree } from '../../../api/components';
import { getMeasures } from '../../../api/measures';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import { Router } from '../../../components/hoc/withRouter';
-import PageActions from '../../../components/ui/PageActions';
+import FilesCounter from '../../../components/ui/FilesCounter';
import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
+import { KeyboardKeys } from '../../../helpers/keycodes';
import { translate } from '../../../helpers/l10n';
import { isDiffMetric } from '../../../helpers/measures';
import { RequestData } from '../../../helpers/request';
+import { isDefined } from '../../../helpers/types';
import { getProjectUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { isFile, isView } from '../../../types/component';
const componentKey = selected || rootComponent.key;
const baseComponentMetrics = [requestedMetric.key];
if (requestedMetric.key === MetricKey.ncloc) {
- baseComponentMetrics.push('ncloc_language_distribution');
+ baseComponentMetrics.push(MetricKey.ncloc_language_distribution);
}
Promise.all([
getComponentTree(strategy, componentKey, metricKeys, opts),
metric: Pick<Metric, 'key' | 'direction'>,
options: Object = {}
) {
- const strategy = view === 'list' ? 'leaves' : 'children';
+ const strategy = view === MeasurePageView.list ? 'leaves' : 'children';
const metricKeys = [metric.key];
const opts: RequestData = {
...getBranchLikeQuery(this.props.branchLike),
};
const isDiff = isDiffMetric(metric.key);
- if (view === 'tree') {
+ if (view === MeasurePageView.tree) {
metricKeys.push(...(complementary[metric.key] || []));
opts.asc = true;
opts.s = 'qualifier,name';
- } else if (view === 'list') {
+ } else if (view === MeasurePageView.list) {
metricKeys.push(...(complementary[metric.key] || []));
opts.asc = metric.direction === 1;
opts.metricSort = metric.key;
setMetricSort();
- } else if (view === 'treemap') {
- const sizeMetric = isDiff ? 'new_lines' : 'ncloc';
+ } else if (view === MeasurePageView.treemap) {
+ const sizeMetric = isDiff ? MetricKey.new_lines : MetricKey.ncloc;
metricKeys.push(sizeMetric);
opts.asc = false;
opts.metricSort = sizeMetric;
getDefaultShowBestMeasures() {
const { asc, view } = this.props;
- if ((asc !== undefined && view === 'list') || view === 'tree') {
+ if ((asc !== undefined && view === MeasurePageView.list) || view === MeasurePageView.tree) {
return true;
}
return false;
if (!metric) {
return null;
}
- if (view === 'tree' || view === 'list') {
+ if (view === MeasurePageView.list || view === MeasurePageView.tree) {
const selectedIdx = this.getSelectedIndex();
return (
<FilesView
<MeasureContentHeader
left={
<MeasuresBreadcrumbs
- backToFirst={view === 'list'}
+ backToFirst={view === MeasurePageView.list}
branchLike={branchLike}
className="sw-flex-1"
component={baseComponent}
/>
}
right={
- <div className="display-flex-center">
+ <div className="sw-flex sw-items-center">
{!isFileComponent && metric && (
<>
- <div id="measures-view-selection-label">
+ <Highlight className="sw-whitespace-nowrap" id="measures-view-selection-label">
{translate('component_measures.view_as')}
- </div>
+ </Highlight>
<MeasureViewSelect
- className="measure-view-select spacer-left big-spacer-right"
+ className="measure-view-select sw-ml-2 sw-mr-4"
handleViewChange={this.updateView}
metric={metric}
view={view}
/>
- <PageActions
- componentQualifier={rootComponent.qualifier}
- current={
- selectedIdx !== undefined && view !== 'treemap' ? selectedIdx + 1 : undefined
- }
- showShortcuts={['list', 'tree'].includes(view)}
- total={paging && paging.total}
+ <KeyboardHint
+ className="sw-mr-4 sw-ml-6"
+ command={`${KeyboardKeys.DownArrow} ${KeyboardKeys.UpArrow}`}
+ title={translate('component_measures.select_files')}
/>
+
+ <KeyboardHint
+ command={`${KeyboardKeys.LeftArrow} ${KeyboardKeys.RightArrow}`}
+ title={translate('component_measures.navigate')}
+ />
+
+ {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>
}
/>
- <MeasureHeader
- branchLike={branchLike}
- component={baseComponent}
- leakPeriod={this.props.leakPeriod}
- measureValue={measureValue}
- metric={metric}
- secondaryMeasure={secondaryMeasure}
- />
- {isFileComponent ? (
- <div className="measure-details-viewer">
- <SourceViewer
- hideHeader={true}
- branchLike={branchLike}
- component={baseComponent.key}
- metricKey={this.state.metric?.key}
- onIssueChange={this.props.onIssueChange}
- />
- </div>
- ) : (
- this.renderMeasure()
- )}
+ <div className="sw-p-6">
+ <MeasureHeader
+ branchLike={branchLike}
+ component={baseComponent}
+ leakPeriod={this.props.leakPeriod}
+ measureValue={measureValue}
+ metric={metric}
+ secondaryMeasure={secondaryMeasure}
+ />
+ {isFileComponent ? (
+ <div className="measure-details-viewer">
+ <SourceViewer
+ hideHeader={true}
+ branchLike={branchLike}
+ component={baseComponent.key}
+ metricKey={this.state.metric?.key}
+ onIssueChange={this.props.onIssueChange}
+ />
+ </div>
+ ) : (
+ this.renderMeasure()
+ )}
+ </div>
</div>
);
}
export default function MeasureContentHeader({ left, right }: Props) {
return (
<StyledHeader className="sw-py-3 sw-px-6 sw-flex sw-justify-between sw-items-center">
- <div>{left}</div>
+ <div className="sw-flex sw-items-center">{left}</div>
<div>{right}</div>
</StyledHeader>
);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import classNames from 'classnames';
+import { Highlight, Link, MetricsLabel, MetricsRatingBadge } from 'design-system';
import * as React from 'react';
import LanguageDistribution from '../../../components/charts/LanguageDistribution';
-import Link from '../../../components/common/Link';
import Tooltip from '../../../components/controls/Tooltip';
-import HistoryIcon from '../../../components/icons/HistoryIcon';
-import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
import Measure from '../../../components/measure/Measure';
-import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
-import { isDiffMetric } from '../../../helpers/measures';
+import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
import { getMeasureHistoryUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
-import { ComponentMeasure, Measure as TypeMeasure, Metric, Period } from '../../../types/types';
+import { ComponentQualifier } from '../../../types/component';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { ComponentMeasure, Metric, Period, Measure as TypeMeasure } from '../../../types/types';
import { hasFullMeasures } from '../utils';
import LeakPeriodLegend from './LeakPeriodLegend';
const { branchLike, component, leakPeriod, measureValue, metric, secondaryMeasure } = props;
const isDiff = isDiffMetric(metric.key);
const hasHistory =
- ['VW', 'SVW', 'APP', 'TRK'].includes(component.qualifier) && hasFullMeasures(branchLike);
+ [
+ ComponentQualifier.Portfolio,
+ ComponentQualifier.SubPortfolio,
+ ComponentQualifier.Application,
+ ComponentQualifier.Project,
+ ].includes(component.qualifier as ComponentQualifier) && hasFullMeasures(branchLike);
const displayLeak = hasFullMeasures(branchLike);
return (
- <div className="measure-details-header big-spacer-bottom">
- <div className="measure-details-primary">
- <div className="measure-details-metric">
- <IssueTypeIcon className="little-spacer-right text-text-bottom" query={metric.key} />
- {getLocalizedMetricName(metric)}
- <span className="measure-details-value spacer-left">
- <strong>
- <Measure
- className={isDiff && displayLeak ? 'leak-box' : undefined}
- metricKey={metric.key}
- metricType={metric.type}
- value={measureValue}
+ <div className="sw-mb-4">
+ <div className="sw-flex sw-items-center sw-justify-between sw-gap-4">
+ <div className="it__measure-details-metric sw-flex sw-items-center sw-gap-1">
+ <strong className="sw-body-md-highlight">{getLocalizedMetricName(metric)}</strong>
+ <Measure
+ className={classNames('it__measure-details-value sw-body-md')}
+ metricKey={metric.key}
+ metricType={metric.type}
+ value={measureValue}
+ ratingComponent={
+ <MetricsRatingBadge
+ label={
+ measureValue
+ ? translateWithParameters(
+ 'metric.has_rating_X',
+ formatMeasure(measureValue, MetricType.Rating)
+ )
+ : translate('metric.no_rating')
+ }
+ rating={formatMeasure(measureValue, MetricType.Rating) as MetricsLabel}
/>
- </strong>
- </span>
+ }
+ />
+
{!isDiff && hasHistory && (
<Tooltip overlay={translate('component_measures.show_metric_history')}>
- <Link
- aria-label={translate('component_measures.show_metric_history')}
- className="js-show-history spacer-left button button-small"
- to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}
- >
- <HistoryIcon />
- </Link>
+ <Highlight>
+ <Link
+ className="it__show-history-link sw-font-semibold sw-ml-4"
+ to={getMeasureHistoryUrl(component.key, metric.key, branchLike)}
+ >
+ {translate('component_measures.see_metric_history')}
+ </Link>
+ </Highlight>
</Tooltip>
)}
</div>
- <div className="measure-details-primary-actions">
- {displayLeak && leakPeriod && (
- <LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
- )}
- </div>
+ {displayLeak && leakPeriod && (
+ <LeakPeriodLegend component={component} period={leakPeriod} />
+ )}
</div>
{secondaryMeasure &&
- secondaryMeasure.metric === 'ncloc_language_distribution' &&
+ secondaryMeasure.metric === MetricKey.ncloc_language_distribution &&
secondaryMeasure.value !== undefined && (
- <div className="measure-details-secondary">
+ <div className="sw-inline-block sw-mt-2">
<LanguageDistribution distribution={secondaryMeasure.value} />
</div>
)}
current={this.state.components.length}
/>
{leakPeriod && displayLeak && (
- <LeakPeriodLegend
- className="pull-right"
- component={component}
- period={leakPeriod}
- />
+ <LeakPeriodLegend component={component} period={leakPeriod} />
)}
</>
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { InputSelect } from 'design-system';
import * as React from 'react';
-import { components, OptionProps, SingleValueProps } from 'react-select';
-import Select from '../../../components/controls/Select';
-import ListIcon from '../../../components/icons/ListIcon';
-import TreeIcon from '../../../components/icons/TreeIcon';
-import TreemapIcon from '../../../components/icons/TreemapIcon';
import { translate } from '../../../helpers/l10n';
import { MeasurePageView } from '../../../types/measures';
import { Metric } from '../../../types/types';
import { hasList, hasTree, hasTreemap } from '../utils';
-interface Props {
+export interface MeasureViewSelectProps {
className?: string;
metric: Metric;
handleViewChange: (view: MeasurePageView) => void;
}
interface ViewOption {
- icon: JSX.Element;
label: string;
- value: string;
+ value: MeasurePageView;
}
-export default class MeasureViewSelect extends React.PureComponent<Props> {
- getOptions = () => {
- const { metric } = this.props;
- const options: ViewOption[] = [];
- if (hasTree(metric.key)) {
- options.push({
- icon: <TreeIcon />,
- label: translate('component_measures.tab.tree'),
- value: 'tree',
- });
- }
- if (hasList(metric.key)) {
- options.push({
- icon: <ListIcon />,
- label: translate('component_measures.tab.list'),
- value: 'list',
- });
- }
- if (hasTreemap(metric.key, metric.type)) {
- options.push({
- icon: <TreemapIcon />,
- label: translate('component_measures.tab.treemap'),
- value: 'treemap',
- });
- }
- return options;
- };
+export default function MeasureViewSelect(props: MeasureViewSelectProps) {
+ const { metric, view, className } = props;
+ const options = [];
+ if (hasTree(metric.key)) {
+ options.push({
+ label: translate('component_measures.tab.tree'),
+ value: MeasurePageView.tree,
+ });
+ }
+ if (hasList(metric.key)) {
+ options.push({
+ label: translate('component_measures.tab.list'),
+ value: MeasurePageView.list,
+ });
+ }
+ if (hasTreemap(metric.key, metric.type)) {
+ options.push({
+ label: translate('component_measures.tab.treemap'),
+ value: MeasurePageView.treemap,
+ });
+ }
- handleChange = (option: ViewOption) => {
- return this.props.handleViewChange(option.value as MeasurePageView);
+ const handleChange = (option: ViewOption) => {
+ return props.handleViewChange(option.value);
};
- renderOption = (props: OptionProps<ViewOption, false>) => (
- <components.Option {...props} className="display-flex-center">
- {props.data.icon}
- <span className="little-spacer-left">{props.data.label}</span>
- </components.Option>
+ return (
+ <InputSelect
+ size="small"
+ aria-labelledby="measures-view-selection-label"
+ blurInputOnSelect={true}
+ className={className}
+ onChange={handleChange}
+ options={options}
+ isSearchable={false}
+ value={options.find((o) => o.value === view)}
+ />
);
-
- renderValue = (props: SingleValueProps<ViewOption, false>) => (
- <components.SingleValue {...props} className="display-flex-center">
- {props.data.icon}
- <span className="little-spacer-left">{props.data.label}</span>
- </components.SingleValue>
- );
-
- render() {
- const { className, view } = this.props;
- const options = this.getOptions();
-
- return (
- <Select
- aria-labelledby="measures-view-selection-label"
- blurInputOnSelect={true}
- className={className}
- onChange={this.handleChange}
- components={{
- Option: this.renderOption,
- SingleValue: this.renderValue,
- }}
- options={options}
- isSearchable={false}
- value={options.find((o) => o.value === view)}
- />
- );
- }
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { differenceInDays } from 'date-fns';
-import { shallow } from 'enzyme';
+import { screen } from '@testing-library/react';
import * as React from 'react';
-import { IntlShape } from 'react-intl';
-import { ComponentMeasure, Period } from '../../../../types/types';
-import { LeakPeriodLegend } from '../LeakPeriodLegend';
+import { mockComponentMeasure } from '../../../../helpers/mocks/component';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { Period } from '../../../../types/types';
+import LeakPeriodLegend, { LeakPeriodLegendProps } from '../LeakPeriodLegend';
jest.mock('date-fns', () => {
const actual = jest.requireActual('date-fns');
return { ...actual, differenceInDays: jest.fn().mockReturnValue(10) };
});
-const PROJECT = {
- key: 'foo',
- name: 'Foo',
- qualifier: 'TRK',
-};
-
-const APP = {
- key: 'bar',
- name: 'Bar',
- qualifier: 'APP',
-};
-
const PERIOD: Period = {
date: '2017-05-16T13:50:02+0200',
index: 1,
parameter: '18',
};
-it('should render correctly', () => {
- expect(getWrapper(PROJECT, PERIOD)).toMatchSnapshot();
- expect(getWrapper(PROJECT, PERIOD_DAYS)).toMatchSnapshot();
+it('renders correctly for project', () => {
+ renderLeakPeriodLegend();
+ expect(screen.getByText('overview.period.previous_version.6,4')).toBeInTheDocument();
+ expect(screen.getByText('component_measures.leak_legend.new_code')).toBeInTheDocument();
});
-it('should render correctly for APP', () => {
- expect(getWrapper(APP, PERIOD)).toMatchSnapshot();
+it('renders correctly for application', () => {
+ renderLeakPeriodLegend({
+ component: mockComponentMeasure(undefined, { qualifier: ComponentQualifier.Application }),
+ });
+ expect(screen.getByText('issues.new_code_period')).toBeInTheDocument();
});
-it('should render a more precise date', () => {
- (differenceInDays as jest.Mock<any>).mockReturnValueOnce(0);
- expect(getWrapper(PROJECT, PERIOD)).toMatchSnapshot();
+it('renders correctly with big period', () => {
+ renderLeakPeriodLegend({ period: PERIOD_DAYS });
+ expect(screen.getByText('component_measures.leak_legend.new_code')).toBeInTheDocument();
+ expect(screen.queryByText('overview.period.previous_version.6,4')).not.toBeInTheDocument();
});
-function getWrapper(component: ComponentMeasure, period: Period) {
- return shallow(
- <LeakPeriodLegend
- component={component}
- intl={{ formatDate: (x: any) => x } as IntlShape}
- period={period}
- />
+function renderLeakPeriodLegend(overrides: Partial<LeakPeriodLegendProps> = {}) {
+ return renderComponent(
+ <LeakPeriodLegend component={mockComponentMeasure()} period={PERIOD} {...overrides} />
);
}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render a more precise date 1`] = `
-<Tooltip
- overlay={
- <div>
- <DateFromNow
- date={2017-05-16T11:50:02.000Z}
- />
- ,
- <DateTimeFormatter
- date={2017-05-16T11:50:02.000Z}
- />
- </div>
- }
->
- <div
- className="domain-measures-header leak-box"
- >
- overview.new_code_period_x.overview.period.previous_version.6,4
- </div>
-</Tooltip>
-`;
-
-exports[`should render correctly 1`] = `
-<Tooltip
- overlay={
- <div>
- <DateFromNow
- date={2017-05-16T11:50:02.000Z}
- />
- ,
- <DateFormatter
- date={2017-05-16T11:50:02.000Z}
- long={true}
- />
- </div>
- }
->
- <div
- className="domain-measures-header leak-box"
- >
- overview.new_code_period_x.overview.period.previous_version.6,4
- </div>
-</Tooltip>
-`;
-
-exports[`should render correctly 2`] = `
-<div
- className="domain-measures-header leak-box"
->
- overview.new_code_period_x.overview.period.days.18
-</div>
-`;
-
-exports[`should render correctly for APP 1`] = `
-<div
- className="domain-measures-header leak-box"
->
- issues.new_code_period
-</div>
-`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { MetricKey } from '../../../types/metrics';
import { Dict } from '../../../types/types';
-export const complementary: Dict<string[]> = {
- coverage: ['uncovered_lines', 'uncovered_conditions'],
- line_coverage: ['uncovered_lines'],
- branch_coverage: ['uncovered_conditions'],
- uncovered_lines: ['line_coverage'],
- uncovered_conditions: ['branch_coverage'],
+export const complementary: Dict<MetricKey[]> = {
+ coverage: [MetricKey.uncovered_lines, MetricKey.uncovered_conditions],
+ line_coverage: [MetricKey.uncovered_lines],
+ branch_coverage: [MetricKey.uncovered_conditions],
+ uncovered_lines: [MetricKey.line_coverage],
+ uncovered_conditions: [MetricKey.branch_coverage],
- new_coverage: ['new_uncovered_lines', 'new_uncovered_conditions'],
- new_line_coverage: ['new_uncovered_lines'],
- new_branch_coverage: ['new_uncovered_conditions'],
- new_uncovered_lines: ['new_line_coverage'],
- new_uncovered_conditions: ['new_branch_coverage'],
+ new_coverage: [MetricKey.new_uncovered_lines, MetricKey.new_uncovered_conditions],
+ new_line_coverage: [MetricKey.new_uncovered_lines],
+ new_branch_coverage: [MetricKey.new_uncovered_conditions],
+ new_uncovered_lines: [MetricKey.new_line_coverage],
+ new_uncovered_conditions: [MetricKey.new_branch_coverage],
- duplicated_lines_density: ['duplicated_lines'],
- new_duplicated_lines_density: ['new_duplicated_lines'],
- duplicated_lines: ['duplicated_lines_density'],
- new_duplicated_lines: ['new_duplicated_lines_density'],
+ duplicated_lines_density: [MetricKey.duplicated_lines],
+ new_duplicated_lines_density: [MetricKey.new_duplicated_lines],
+ duplicated_lines: [MetricKey.duplicated_lines_density],
+ new_duplicated_lines: [MetricKey.new_duplicated_lines_density],
};
let tail = component.name;
if (
- view === 'list' &&
+ view === MeasurePageView.list &&
(
[
ComponentQualifier.File,
import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
-import { MetricKey } from '../../../types/metrics';
+import { MetricKey, MetricType } from '../../../types/metrics';
import { ComponentMeasureEnhanced, ComponentMeasureIntern, Metric } from '../../../types/types';
import EmptyResult from './EmptyResult';
getRatingColorScale = () => scaleLinear<string, string>().domain([1, 2, 3, 4, 5]).range(COLORS);
getColorScale = (metric: Metric) => {
- if (metric.type === 'LEVEL') {
+ if (metric.type === MetricType.Level) {
return this.getLevelColorScale();
}
- if (metric.type === 'RATING') {
+ if (metric.type === MetricType.Rating) {
return this.getRatingColorScale();
}
return this.getPercentColorScale(metric);
renderLegend() {
const { metric } = this.props;
const colorScale = this.getColorScale(metric);
- if (['LEVEL', 'RATING'].includes(metric.type)) {
+ if ([MetricType.Level, MetricType.Rating].includes(metric.type as MetricType)) {
return (
<ColorBoxLegend
className="measure-details-treemap-legend color-box-full"
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Note } from 'design-system';
+import { MetricsLabel, MetricsRatingBadge, Note } from 'design-system';
import React from 'react';
import Measure from '../../../components/measure/Measure';
-import { isDiffMetric } from '../../../helpers/measures';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { MetricType } from '../../../types/metrics';
import { MeasureEnhanced } from '../../../types/types';
interface Props {
export default function SubnavigationMeasureValue({ measure }: Props) {
const isDiff = isDiffMetric(measure.metric.key);
+ const value = isDiff ? measure.leak : measure.value;
+ const formatted = formatMeasure(value, MetricType.Rating);
return (
<Note
id={`measure-${measure.metric.key}-${isDiff ? 'leak' : 'value'}`}
>
<Measure
+ ratingComponent={
+ <MetricsRatingBadge
+ size="xs"
+ label={
+ value
+ ? translateWithParameters('metric.has_rating_X', formatted)
+ : translate('metric.no_rating')
+ }
+ rating={formatted as MetricsLabel}
+ />
+ }
metricKey={measure.metric.key}
metricType={measure.metric.type}
- value={isDiff ? measure.leak : measure.value}
+ value={value}
/>
</Note>
);
border-bottom-left-radius: 0;
}
-.domain-measures-header {
- display: inline-block;
- padding: 4px 10px;
- white-space: nowrap;
-}
-
-.measure-details-metric {
- display: flex;
- align-items: center;
-}
-
-.measure-details-primary {
- display: flex;
- flex-wrap: nowrap;
- justify-content: space-between;
- align-items: center;
-}
-
-.measure-details-primary-actions {
- display: flex;
- align-items: center;
-}
-
-.measure-details-secondary {
- display: inline-block;
- width: 260px;
- margin-top: 4px;
-}
-
-.domain-measures-value .rating,
-.measure-details-value .rating {
- width: 18px;
- height: 18px;
- margin-top: -2px;
- margin-bottom: -2px;
- font-size: var(--smallFontSize);
-}
-
-.domain-measures-value .level {
- height: 18px;
- border-radius: 18px;
- margin-top: -2px;
- margin-bottom: -2px;
- margin-right: -4px;
-}
-
.measure-details-treemap-legend.color-box-legend {
margin-right: 0;
}
import { BranchLike } from '../../types/branch-like';
import { ComponentQualifier } from '../../types/component';
import { MeasurePageView } from '../../types/measures';
-import { MetricKey } from '../../types/metrics';
+import { MetricKey, MetricType } from '../../types/metrics';
import {
ComponentMeasure,
ComponentMeasureEnhanced,
export const BUBBLES_FETCH_LIMIT = 500;
export const PROJECT_OVERVEW = 'project_overview';
-export const DEFAULT_VIEW: MeasurePageView = 'tree';
+export const DEFAULT_VIEW = MeasurePageView.tree;
export const DEFAULT_METRIC = PROJECT_OVERVEW;
export const KNOWN_DOMAINS = [
'Releasability',
'Complexity',
];
const BANNED_MEASURES = [
- 'blocker_violations',
- 'new_blocker_violations',
- 'critical_violations',
- 'new_critical_violations',
- 'major_violations',
- 'new_major_violations',
- 'minor_violations',
- 'new_minor_violations',
- 'info_violations',
- 'new_info_violations',
+ MetricKey.blocker_violations,
+ MetricKey.new_blocker_violations,
+ MetricKey.critical_violations,
+ MetricKey.new_critical_violations,
+ MetricKey.major_violations,
+ MetricKey.new_major_violations,
+ MetricKey.minor_violations,
+ MetricKey.new_minor_violations,
+ MetricKey.info_violations,
+ MetricKey.new_info_violations,
];
export function filterMeasures(measures: MeasureEnhanced[]): MeasureEnhanced[] {
- return measures.filter((measure) => !BANNED_MEASURES.includes(measure.metric.key));
+ return measures.filter((measure) => !BANNED_MEASURES.includes(measure.metric.key as MetricKey));
}
export function sortMeasures(
export function banQualityGateMeasure({ measures = [], qualifier }: ComponentMeasure): Measure[] {
const bannedMetrics: string[] = [];
if (ComponentQualifier.Portfolio !== qualifier && ComponentQualifier.SubPortfolio !== qualifier) {
- bannedMetrics.push('alert_status');
+ bannedMetrics.push(MetricKey.alert_status);
}
if (qualifier === ComponentQualifier.Application) {
- bannedMetrics.push('releasability_rating', 'releasability_effort');
+ bannedMetrics.push(MetricKey.releasability_rating, MetricKey.releasability_effort);
}
return measures.filter((measure) => !bannedMetrics.includes(measure.metric));
}
});
export function hasList(metric: string): boolean {
- return !['releasability_rating', 'releasability_effort'].includes(metric);
+ return ![MetricKey.releasability_rating, MetricKey.releasability_effort].includes(
+ metric as MetricKey
+ );
}
export function hasTree(metric: string): boolean {
- return metric !== 'alert_status';
+ return metric !== MetricKey.alert_status;
}
export function hasTreemap(metric: string, type: string): boolean {
- return ['PERCENT', 'RATING', 'LEVEL'].includes(type) && hasTree(metric);
+ return (
+ [MetricType.Percent, MetricType.Rating, MetricType.Level].includes(type as MetricType) &&
+ hasTree(metric)
+ );
}
export function hasBubbleChart(domainName: string): boolean {
}
export function hasFacetStat(metric: string): boolean {
- return metric !== 'alert_status';
+ return metric !== MetricKey.alert_status;
}
export function hasFullMeasures(branch?: BranchLike) {
if (isPullRequest(branch)) {
return metricKeys.filter((key) => isDiffMetric(key));
- } else {
- return metricKeys;
}
+
+ return metricKeys;
}
export function getBubbleMetrics(domain: string, metrics: Dict<Metric>) {
return metric === PROJECT_OVERVEW;
}
-function parseView(metric: string, rawView?: string): MeasurePageView {
+function parseView(metric: MetricKey, rawView?: string): MeasurePageView {
const view = (parseAsString(rawView) || DEFAULT_VIEW) as MeasurePageView;
if (!hasTree(metric)) {
- return 'list';
- } else if (view === 'list' && !hasList(metric)) {
- return 'tree';
+ return MeasurePageView.list;
+ } else if (view === MeasurePageView.list && !hasList(metric)) {
+ return MeasurePageView.tree;
}
return view;
}
}
export const parseQuery = memoize((urlQuery: RawQuery): Query => {
- const metric = parseAsString(urlQuery['metric']) || DEFAULT_METRIC;
+ const metric = (parseAsString(urlQuery['metric']) || DEFAULT_METRIC) as MetricKey;
return {
metric,
selected: parseAsString(urlQuery['selected']),
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 styled from '@emotion/styled';
+import classNames from 'classnames';
+import { Note, themeColor } from 'design-system';
+import React from 'react';
+import { translate } from '../../helpers/l10n';
+import { formatMeasure } from '../../helpers/measures';
+import { isDefined } from '../../helpers/types';
+import { MetricType } from '../../types/metrics';
+
+interface Props {
+ className?: string;
+ current?: number;
+ total: number;
+}
+
+export default function FilesCounter({ className, current, total }: Props) {
+ return (
+ <Note className={classNames('sw-whitespace-nowrap', className)}>
+ <Counter className="sw-body-sm-highlight">
+ {isDefined(current) && formatMeasure(current, MetricType.Integer) + '/'}
+ {formatMeasure(total, MetricType.Integer)}
+ </Counter>{' '}
+ {translate('component_measures.files')}
+ </Note>
+ );
+}
+
+FilesCounter.displayName = 'FilesCounter';
+
+const Counter = styled.strong`
+ color: ${themeColor('pageContent')};
+`;
import { translate } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';
import { ComponentQualifier } from '../../types/component';
+import { MetricType } from '../../types/metrics';
export interface Props {
componentQualifier?: string;
<strong>
{current !== undefined && (
<span>
- {formatMeasure(current, 'INT')}
+ {formatMeasure(current, MetricType.Integer)}
{' / '}
</span>
)}
- {formatMeasure(total, 'INT')}
+ {formatMeasure(total, MetricType.Integer)}
</strong>{' '}
{translate('component_measures.files')}
</span>
import { AlmKeys } from '../../types/alm-settings';
import { ComponentQualifier } from '../../types/component';
import { IssueType } from '../../types/issues';
+import { MeasurePageView } from '../../types/measures';
import { SecurityStandard } from '../../types/security';
import { mockBranch, mockMainBranch, mockPullRequest } from '../mocks/branch-like';
import { mockLocation } from '../testMocks';
getIssuesUrl,
getPathUrlAsString,
getProjectSettingsUrl,
- getQualityGatesUrl,
getQualityGateUrl,
+ getQualityGatesUrl,
getReturnUrl,
isRelativeUrl,
queryToSearch,
COMPLEX_COMPONENT_KEY,
METRIC,
undefined,
- 'list'
+ MeasurePageView.list
)
).toEqual(
expect.objectContaining({
search: queryToSearch({
id: SIMPLE_COMPONENT_KEY,
metric: METRIC,
- view: 'list',
+ view: MeasurePageView.list,
selected: COMPLEX_COMPONENT_KEY,
}),
})
COMPLEX_COMPONENT_KEY,
METRIC,
mockMainBranch(),
- 'treemap'
+ MeasurePageView.treemap
)
).toEqual(
expect.objectContaining({
search: queryToSearch({
id: SIMPLE_COMPONENT_KEY,
metric: METRIC,
- view: 'treemap',
+ view: MeasurePageView.treemap,
selected: COMPLEX_COMPONENT_KEY,
}),
})
COMPLEX_COMPONENT_KEY,
METRIC,
mockPullRequest({ key: '1' }),
- 'tree'
+ MeasurePageView.tree
)
).toEqual(
expect.objectContaining({
selectionKey,
metric,
branchLike,
- treemapView: view === 'treemap',
- listView: view === 'list',
+ treemapView: view === MeasurePageView.treemap,
+ listView: view === MeasurePageView.list,
});
}
period: Period;
}
-export type MeasurePageView = 'list' | 'tree' | 'treemap';
+export enum MeasurePageView {
+ list = 'list',
+ tree = 'tree',
+ treemap = 'treemap',
+}
component_measures.domain_x_overview={0} Overview
component_measures.domain_overview=Overview
component_measures.files=files
-component_measures.show_metric_history=Show history of this metric
component_measures.tab.tree=Tree
component_measures.tab.list=List
component_measures.tab.treemap=Treemap
component_measures.no_history=There isn't enough data to generate an activity graph.
component_measures.not_found=The requested measure was not found.
component_measures.empty=No measures.
+component_measures.select_files=Select files
+component_measures.navigate=Navigate
component_measures.to_select_files=to select files
component_measures.to_navigate=to navigate
component_measures.to_navigate_files=to next/previous file
component_measures.hidden_best_score_metrics_show_label=Show hidden components
component_measures.navigation=Measures navigation
component_measures.skip_to_navigation=Skip to measure navigation
+component_measures.see_metric_history=See history
+component_measures.leak_legend.new_code=New Code:
component_measures.overview.project_overview.subnavigation=Project Overview
component_measures.overview.project_overview.title=Risk