]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19391 Measure Header uses new design
authorstanislavh <stanislav.honcharov@sonarsource.com>
Fri, 2 Jun 2023 12:43:14 +0000 (14:43 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 5 Jun 2023 20:02:48 +0000 (20:02 +0000)
25 files changed:
server/sonar-web/design-system/src/components/InputSelect.tsx
server/sonar-web/design-system/src/components/KeyboardHint.tsx
server/sonar-web/src/main/js/apps/component-measures/__tests__/ComponentMeasures-it.tsx
server/sonar-web/src/main/js/apps/component-measures/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentHeader.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.tsx
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/LeakPeriodLegend-test.tsx
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/component-measures/config/complementary.ts
server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.tsx
server/sonar-web/src/main/js/apps/component-measures/drilldown/TreeMapView.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/SubnavigationMeasureValue.tsx
server/sonar-web/src/main/js/apps/component-measures/style.css
server/sonar-web/src/main/js/apps/component-measures/utils.ts
server/sonar-web/src/main/js/components/ui/FilesCounter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/ui/PageActions.tsx
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
server/sonar-web/src/main/js/helpers/urls.ts
server/sonar-web/src/main/js/types/measures.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 59bb6d728d9f749939c5b765ba7584dfff3c8594..3b51e2a9a4f7dab809f98d32d752dcedea523922 100644 (file)
@@ -120,18 +120,19 @@ export function InputSelect<
       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,
@@ -178,7 +179,6 @@ export function selectStyle<
     menu: (base) => ({
       ...base,
       width: INPUT_SIZES[size],
-      zIndex: 101,
     }),
     option: (base, { isFocused, isSelected }) => ({
       ...base,
index cbfa57434c3a09f49b0a26f4457165a0854910e0..845ab2f5c91605ba533d1a42b569a37f16292318 100644 (file)
@@ -24,17 +24,18 @@ import { Key } from '../helpers/keyboard';
 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>
index 6d8679e4a7ba6d5b206b563dc9347074e900fead..53ef62d652a8214a0d1b59a48872b7cfc7ab0f76 100644 (file)
@@ -420,9 +420,7 @@ function getPageObject() {
     // 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', {
@@ -482,7 +480,7 @@ function getPageObject() {
     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 = {
index 49988516fd2400fffa6002b1418fa8d584f397e0..1ba9141ce063b0770731c115bbf0199b93c9e766 100644 (file)
@@ -18,6 +18,7 @@
  * 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';
@@ -142,17 +143,19 @@ describe('parseQuery', () => {
 
 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));
   });
 });
index 7a90f5734fda96cc0ac2ece0ba92754712b82536..13aaf746887aeed9d9615a28d1f578ea95b9be51 100644 (file)
@@ -42,6 +42,7 @@ import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../he
 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,
@@ -188,10 +189,10 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
 
     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;
       }
     }
 
@@ -255,7 +256,7 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
     }
 
     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}
index f6f5acb46a526d7867ece964328da2681defce29..1041a89d5f973867bc02aec2eca36e8b50fc2563 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import 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);
   };
@@ -45,24 +46,26 @@ export class LeakPeriodLegend extends React.PureComponent<Props & WrappedCompone
   };
 
   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) {
@@ -87,3 +90,8 @@ export class LeakPeriodLegend extends React.PureComponent<Props & WrappedCompone
 }
 
 export default injectIntl(LeakPeriodLegend);
+
+const LeakPeriodLabel = styled(Note)`
+  background-color: ${themeColor('newCodeLegend')};
+  border: ${themeBorder('default', 'newCodeLegendBorder')};
+`;
index aca949a8c603f93ecd9c17cee0db54e3f5866dd6..f353bcc6c3b97e166cb451defec3b544d8611fe6 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { 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';
@@ -118,7 +121,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
     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),
@@ -205,7 +208,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
     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),
@@ -223,17 +226,17 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
     };
 
     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;
@@ -291,7 +294,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
 
   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;
@@ -303,7 +306,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
     if (!metric) {
       return null;
     }
-    if (view === 'tree' || view === 'list') {
+    if (view === MeasurePageView.list || view === MeasurePageView.tree) {
       const selectedIdx = this.getSelectedIndex();
       return (
         <FilesView
@@ -358,7 +361,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
         <MeasureContentHeader
           left={
             <MeasuresBreadcrumbs
-              backToFirst={view === 'list'}
+              backToFirst={view === MeasurePageView.list}
               branchLike={branchLike}
               className="sw-flex-1"
               component={baseComponent}
@@ -367,54 +370,70 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
             />
           }
           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>
     );
   }
index 21af06f91578166612059a2c13ec3c67f811ffb0..e97a6ca889e1dc2e0d6bfc4e9c8b15bf518113ad 100644 (file)
@@ -29,7 +29,7 @@ interface Props {
 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>
   );
index 5a0fbaf137d427fc3d87a4d831ab3862bb51cd47..e26f7d7d2b63c7d3e82194be34eb71802cc0af36 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import 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';
 
@@ -45,46 +46,59 @@ export default function MeasureHeader(props: Props) {
   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>
         )}
index ff9f686eafbd8e5a441e9303f4c1ee1a226dc90a..f6ef3bf6236ea00620c782c1efe1389eb276b405 100644 (file)
@@ -177,11 +177,7 @@ export default class MeasureOverview extends React.PureComponent<Props, State> {
                 current={this.state.components.length}
               />
               {leakPeriod && displayLeak && (
-                <LeakPeriodLegend
-                  className="pull-right"
-                  component={component}
-                  period={leakPeriod}
-                />
+                <LeakPeriodLegend component={component} period={leakPeriod} />
               )}
             </>
           }
index d4eda50883225fd5c50dfd61e1cae87dc4ef052a..2b490fedaf94bc5981d51a88257a786304a4e912 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { 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;
@@ -36,75 +32,46 @@ interface Props {
 }
 
 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)}
-      />
-    );
-  }
 }
index 4553867f482433f0ed1c1af72a4c0effff8d7eb2..5c908450ce96e101ed8451b20b08efdc29788b22 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { 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,
@@ -55,26 +44,27 @@ const PERIOD_DAYS: Period = {
   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} />
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.tsx.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/LeakPeriodLegend-test.tsx.snap
deleted file mode 100644 (file)
index 80065f5..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-// 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>
-`;
index a4946d6e8a204b9d6cb04cd655e96e42ad3839bb..dfa15f26c450b246dd6816d1944a547017ab51fc 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { 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],
 };
index 4a2c158e82c35f49ab4a6e07f389d97922f6203d..6f00fe0d0fbe18114fba2447ee26ac5b74a8b7c7 100644 (file)
@@ -44,7 +44,7 @@ export default function ComponentCell(props: ComponentCellProps) {
   let tail = component.name;
 
   if (
-    view === 'list' &&
+    view === MeasurePageView.list &&
     (
       [
         ComponentQualifier.File,
index 2de67a1381e7c4df6d3b1d4cbed2ca4df3e19380..88bac6bcd83b0f27909d9111e6a5ade195085fd1 100644 (file)
@@ -30,7 +30,7 @@ import { RATING_COLORS } from '../../../helpers/constants';
 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';
 
@@ -130,10 +130,10 @@ export default class TreeMapView extends React.PureComponent<Props, State> {
   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);
@@ -174,7 +174,7 @@ export default class TreeMapView extends React.PureComponent<Props, State> {
   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"
index 4dce88ea909fe0a8f8e6a17b8520570bef6c22cf..40582ece0a80e34fa275996695c3fc8690d0cbbd 100644 (file)
  * 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 {
@@ -30,6 +32,8 @@ 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
@@ -37,9 +41,20 @@ export default function SubnavigationMeasureValue({ measure }: Props) {
       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>
   );
index 218abc319668345b51c6218d397f117f5791c491..0e16fbf65869cbaa33a93caec0f52cdce3ec8515 100644 (file)
@@ -49,52 +49,6 @@ button.search-navigator-facet {
   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;
 }
index d3a1efcc6317fc0a55928ca31e0c5268a3e5d292..74cfb3557954b8b41c220e21bc9de5cdc51be02e 100644 (file)
@@ -31,7 +31,7 @@ import {
 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,
@@ -46,7 +46,7 @@ import { domains } from './config/domains';
 
 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',
@@ -60,20 +60,20 @@ export const KNOWN_DOMAINS = [
   '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(
@@ -133,10 +133,10 @@ export function isSecurityReviewMetric(metricKey: MetricKey | string): boolean {
 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));
 }
@@ -157,15 +157,20 @@ export const groupByDomains = memoize((measures: MeasureEnhanced[]) => {
 });
 
 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 {
@@ -173,7 +178,7 @@ 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) {
@@ -185,9 +190,9 @@ export function getMeasuresPageMetricKeys(metrics: Dict<Metric>, branch?: Branch
 
   if (isPullRequest(branch)) {
     return metricKeys.filter((key) => isDiffMetric(key));
-  } else {
-    return metricKeys;
   }
+
+  return metricKeys;
 }
 
 export function getBubbleMetrics(domain: string, metrics: Dict<Metric>) {
@@ -208,12 +213,12 @@ export function isProjectOverview(metric: string) {
   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;
 }
@@ -226,7 +231,7 @@ export interface Query {
 }
 
 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']),
diff --git a/server/sonar-web/src/main/js/components/ui/FilesCounter.tsx b/server/sonar-web/src/main/js/components/ui/FilesCounter.tsx
new file mode 100644 (file)
index 0000000..dcf20a3
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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')};
+`;
index 8aa271db58a77557c79c0ccd02447ea2275957fb..69ef3bad5fb9330942b5d1c0a35c7e353a2ef3b1 100644 (file)
@@ -21,6 +21,7 @@ import * as React from 'react';
 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;
@@ -55,11 +56,11 @@ export default function PageActions(props: Props) {
             <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>
index 410084c3fff3350cf3221852fcff464d60dcd565..c5435a6928d18f08371af64ffcf9d07cd71c1ff2 100644 (file)
@@ -20,6 +20,7 @@
 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';
@@ -38,8 +39,8 @@ import {
   getIssuesUrl,
   getPathUrlAsString,
   getProjectSettingsUrl,
-  getQualityGatesUrl,
   getQualityGateUrl,
+  getQualityGatesUrl,
   getReturnUrl,
   isRelativeUrl,
   queryToSearch,
@@ -312,7 +313,7 @@ describe('#getComponentDrilldownUrlWithSelection', () => {
         COMPLEX_COMPONENT_KEY,
         METRIC,
         undefined,
-        'list'
+        MeasurePageView.list
       )
     ).toEqual(
       expect.objectContaining({
@@ -320,7 +321,7 @@ describe('#getComponentDrilldownUrlWithSelection', () => {
         search: queryToSearch({
           id: SIMPLE_COMPONENT_KEY,
           metric: METRIC,
-          view: 'list',
+          view: MeasurePageView.list,
           selected: COMPLEX_COMPONENT_KEY,
         }),
       })
@@ -332,7 +333,7 @@ describe('#getComponentDrilldownUrlWithSelection', () => {
         COMPLEX_COMPONENT_KEY,
         METRIC,
         mockMainBranch(),
-        'treemap'
+        MeasurePageView.treemap
       )
     ).toEqual(
       expect.objectContaining({
@@ -340,7 +341,7 @@ describe('#getComponentDrilldownUrlWithSelection', () => {
         search: queryToSearch({
           id: SIMPLE_COMPONENT_KEY,
           metric: METRIC,
-          view: 'treemap',
+          view: MeasurePageView.treemap,
           selected: COMPLEX_COMPONENT_KEY,
         }),
       })
@@ -352,7 +353,7 @@ describe('#getComponentDrilldownUrlWithSelection', () => {
         COMPLEX_COMPONENT_KEY,
         METRIC,
         mockPullRequest({ key: '1' }),
-        'tree'
+        MeasurePageView.tree
       )
     ).toEqual(
       expect.objectContaining({
index 3bb59779bc9e33c758911422c5649198e9b8ca82..4945c1be868e12ffef957b8206666a92f679b7ad 100644 (file)
@@ -267,8 +267,8 @@ export function getComponentDrilldownUrlWithSelection(
     selectionKey,
     metric,
     branchLike,
-    treemapView: view === 'treemap',
-    listView: view === 'list',
+    treemapView: view === MeasurePageView.treemap,
+    listView: view === MeasurePageView.list,
   });
 }
 
index ce0bbbfeb6b2ff2750f753a9aaf0cc0d00f22974..5bd90050148bc1311612f52bfaad4b8a75dc443d 100644 (file)
@@ -36,4 +36,8 @@ export interface MeasuresAndMetaWithPeriod {
   period: Period;
 }
 
-export type MeasurePageView = 'list' | 'tree' | 'treemap';
+export enum MeasurePageView {
+  list = 'list',
+  tree = 'tree',
+  treemap = 'treemap',
+}
index f1c615589cd24008ef81685f597147f7acd8f1c6..3e125df757d54c5be6f7423b39d1dd780516428e 100644 (file)
@@ -3639,7 +3639,6 @@ component_measures.details_are_not_available=Details are not available for estim
 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
@@ -3652,6 +3651,8 @@ component_measures.legend.only_first_500_files=Only showing data for the first 5
 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
@@ -3659,6 +3660,8 @@ component_measures.hidden_best_score_metrics=There are {0} hidden components wit
 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