]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19472 Migrate the component table to the new UI
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 6 Jun 2023 14:11:38 +0000 (16:11 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 9 Jun 2023 20:03:10 +0000 (20:03 +0000)
16 files changed:
server/sonar-web/design-system/src/components/MetricsRatingBadge.tsx
server/sonar-web/design-system/src/components/Table.tsx
server/sonar-web/design-system/src/components/icons/PinIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/index.ts
server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
server/sonar-web/src/main/js/apps/code/components/CodeAppRenderer.tsx
server/sonar-web/src/main/js/apps/code/components/Component.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentMeasure.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx
server/sonar-web/src/main/js/apps/code/components/Components.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx
server/sonar-web/src/main/js/apps/code/components/SearchResults.tsx [deleted file]
server/sonar-web/src/main/js/components/measure/Measure.tsx
server/sonar-web/src/main/js/components/measure/__tests__/__snapshots__/Measure-test.tsx.snap

index f91758932d70eaaa4d4ac1edeb35fc7ba9c7f778..2bdf0ca7f9f9ee904982b1161b42a56d985881e2 100644 (file)
@@ -38,9 +38,14 @@ const SIZE_MAPPING = {
 export function MetricsRatingBadge({ className, size = 'sm', label, rating, ...ariaAttrs }: Props) {
   if (!rating) {
     return (
-      <span aria-label={label} className={className} {...ariaAttrs}>
-        –
-      </span>
+      <StyledNoRatingBadge
+        aria-label={label}
+        className={className}
+        size={SIZE_MAPPING[size]}
+        {...ariaAttrs}
+      >
+        —
+      </StyledNoRatingBadge>
     );
   }
   return (
@@ -56,6 +61,15 @@ export function MetricsRatingBadge({ className, size = 'sm', label, rating, ...a
   );
 }
 
+const StyledNoRatingBadge = styled.div<{ size: string }>`
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+
+  width: ${getProp('size')};
+  height: ${getProp('size')};
+`;
+
 const MetricsRatingBadgeStyled = styled.div<{ rating: MetricsLabel; size: string }>`
   width: ${getProp('size')};
   height: ${getProp('size')};
@@ -63,7 +77,10 @@ const MetricsRatingBadgeStyled = styled.div<{ rating: MetricsLabel; size: string
   font-size: ${({ size }) => (size === '2rem' ? '0.875rem' : '0.75rem')};
   background-color: ${({ rating }) => themeColor(`rating.${rating}`)};
 
-  ${tw`sw-inline-flex sw-items-center sw-justify-center`};
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+
   ${tw`sw-rounded-pill`};
   ${tw`sw-font-semibold`};
 `;
index ac3a8060b5431b5a354ebb8b3aad32e653d41dbe..8b8e75a240c7a85a029a537d7627bc2759e1f394 100644 (file)
@@ -152,13 +152,13 @@ export const TableRowInteractive = styled(TableRowInteractiveBase)`
 `;
 
 export const ContentCell = styled(CellComponent)`
-  ${tw`sw-text-left`}
+  ${tw`sw-text-left sw-justify-start`}
 `;
 export const NumericalCell = styled(CellComponent)`
-  ${tw`sw-text-right`}
+  ${tw`sw-text-right sw-justify-end`}
 `;
 export const RatingCell = styled(CellComponent)`
-  ${tw`sw-text-right`}
+  ${tw`sw-text-right sw-justify-end`}
 `;
 export const CheckboxCell = styled(CellComponent)`
   ${tw`sw-text-center`}
@@ -181,7 +181,7 @@ const StyledTable = styled.table<GenericTableProps | CustomTableProps>`
 
 const CellComponentStyled = styled.td`
   color: ${themeColor('pageContent')};
-
+  ${tw`sw-flex sw-items-center`}
   ${tw`sw-body-sm`}
   ${tw`sw-py-4 sw-px-2`}
   ${tw`sw-align-top`}
diff --git a/server/sonar-web/design-system/src/components/icons/PinIcon.tsx b/server/sonar-web/design-system/src/components/icons/PinIcon.tsx
new file mode 100644 (file)
index 0000000..e3bbab8
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * 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 { PinIcon as OcticonPinIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export const PinIcon = OcticonHoc(OcticonPinIcon, 'PinIcon');
index 5112f869cfd6a8920d720946d022e2aec4603565..a65e5e893fa26240aabc278edef992059deb4ca3 100644 (file)
@@ -54,6 +54,7 @@ export { OpenNewTabIcon } from './OpenNewTabIcon';
 export { OverviewQGNotComputedIcon } from './OverviewQGNotComputedIcon';
 export { OverviewQGPassedIcon } from './OverviewQGPassedIcon';
 export { PencilIcon } from './PencilIcon';
+export { PinIcon } from './PinIcon';
 export { ProjectIcon } from './ProjectIcon';
 export { PullRequestIcon } from './PullRequestIcon';
 export { QualifierIcon } from './QualifierIcon';
index bb2b888e1aaf3298a09027522cd322de702c2ff6..61b202ab4dc599548044e421e34f3d329d2ea8da 100644 (file)
@@ -78,7 +78,7 @@ it('should allow navigating through the tree', async () => {
 
   // Navigate by clicking on an element.
   await ui.clickOnChildComponent(/folderA$/);
-  expect(await ui.childComponent(/out\.tsx/).find()).toBeInTheDocument();
+  expect(await ui.childComponent(/out\.tsx/).findAll()).toHaveLength(2); // One for the pin, one for the name column
 
   // Navigate back using the breadcrumb.
   await ui.clickOnBreadcrumb(/Foo$/);
index f440a027ece9e9e56a4d42ac1b146fd8869e7b0a..47cab3fba89ed17a3b824241b4f2a49f36591540 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 { FlagMessage, HelperHintIcon, LargeCenteredLayout } from 'design-system';
+import {
+  Card,
+  DeferredSpinner,
+  FlagMessage,
+  HelperHintIcon,
+  LargeCenteredLayout,
+  LightLabel,
+} from 'design-system';
 import { intersection } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
@@ -36,7 +42,6 @@ import { getCodeMetrics } from '../utils';
 import CodeBreadcrumbs from './CodeBreadcrumbs';
 import Components from './Components';
 import Search from './Search';
-import SearchResults from './SearchResults';
 import SourceViewerWrapper from './SourceViewerWrapper';
 
 interface Props {
@@ -90,11 +95,6 @@ export default function CodeAppRenderer(props: Props) {
 
   const showComponentList = sourceViewer === undefined && components.length > 0 && !showSearch;
 
-  const componentsClassName = classNames('boxed-group', 'spacer-top', {
-    'new-loading': loading,
-    'search-results': showSearch,
-  });
-
   const metricKeys = intersection(
     getCodeMetrics(component.qualifier, branchLike, { newCode: newCodeSelected }),
     Object.keys(metrics)
@@ -111,7 +111,7 @@ export default function CodeAppRenderer(props: Props) {
   const isPortfolio = isPortfolioLike(qualifier);
 
   return (
-    <LargeCenteredLayout className="sw-py-8 sw-body-md">
+    <LargeCenteredLayout className="sw-py-8 sw-body-md" id="code-page">
       <Suggestions suggestions="code" />
       <Helmet defer={false} title={sourceViewer !== undefined ? sourceViewer.name : defaultTitle} />
 
@@ -145,15 +145,15 @@ export default function CodeAppRenderer(props: Props) {
         />
       )}
 
-      <div className="code-components">
+      <div>
         {!hasComponents && sourceViewer === undefined && (
-          <div className="display-flex-center display-flex-column no-file">
-            <span className="h1 text-muted">
+          <div className="sw-flex sw-align-center sw-flex-col sw-fixed sw-top-1/2">
+            <LightLabel>
               {translate(
                 'code_viewer.no_source_code_displayed_due_to_empty_analysis',
                 component.qualifier
               )}
-            </span>
+            </LightLabel>
           </div>
         )}
 
@@ -165,47 +165,46 @@ export default function CodeAppRenderer(props: Props) {
           />
         )}
 
-        <div className={componentsClassName}>
-          {showComponentList && (
-            <Components
-              baseComponent={baseComponent}
-              branchLike={branchLike}
-              components={components}
-              cycle
-              metrics={filteredMetrics}
-              onEndOfList={props.handleLoadMore}
-              onGoToParent={props.handleGoToParent}
-              onHighlight={props.handleHighlight}
-              onSelect={props.handleSelect}
-              rootComponent={component}
-              selected={highlighted}
-              newCodeSelected={newCodeSelected}
-              showAnalysisDate={isPortfolio}
-            />
-          )}
-
-          {showSearch && (
-            <SearchResults
-              branchLike={branchLike}
-              components={searchResults}
-              onHighlight={props.handleHighlight}
-              onSelect={props.handleSelect}
-              rootComponent={component}
-              selected={highlighted}
-            />
-          )}
-
-          <div role="status" className={showSearch ? 'text-center big-padded-bottom' : undefined}>
-            {searchResults?.length === 0 && translate('no_results')}
-          </div>
-        </div>
+        <Card className="sw-mt-2">
+          <DeferredSpinner loading={loading}>
+            {showComponentList && (
+              <Components
+                baseComponent={baseComponent}
+                branchLike={branchLike}
+                components={components}
+                cycle
+                metrics={filteredMetrics}
+                onEndOfList={props.handleLoadMore}
+                onGoToParent={props.handleGoToParent}
+                onHighlight={props.handleHighlight}
+                onSelect={props.handleSelect}
+                rootComponent={component}
+                selected={highlighted}
+                newCodeSelected={newCodeSelected}
+                showAnalysisDate={isPortfolio}
+              />
+            )}
+
+            {showSearch && (
+              <Components
+                branchLike={branchLike}
+                components={searchResults}
+                metrics={[]}
+                onHighlight={props.handleHighlight}
+                onSelect={props.handleSelect}
+                rootComponent={component}
+                selected={highlighted}
+              />
+            )}
+          </DeferredSpinner>
+        </Card>
 
         {showComponentList && (
           <ListFooter count={components.length} loadMore={props.handleLoadMore} total={total} />
         )}
 
         {sourceViewer !== undefined && !showSearch && (
-          <div className="spacer-top">
+          <div className="sw-mt-2">
             <SourceViewerWrapper
               branchLike={branchLike}
               component={sourceViewer.key}
index d42aa9fb47b6367e0a921b6d48e1b1245851a110..f18c72bc6fc66fc8de5076a1f6b5343da02d0bb5 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 { ContentCell, NumericalCell, TableRowInteractive } from 'design-system';
 import * as React from 'react';
-import { withScrollTo } from '../../../components/hoc/withScrollTo';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import { WorkspaceContext } from '../../../components/workspace/context';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier } from '../../../types/component';
-import { ComponentMeasure as TypeComponentMeasure, Metric } from '../../../types/types';
+import { Metric, ComponentMeasure as TypeComponentMeasure } from '../../../types/types';
 import ComponentMeasure from './ComponentMeasure';
 import ComponentName from './ComponentName';
 import ComponentPin from './ComponentPin';
@@ -34,7 +33,6 @@ interface Props {
   canBePinned?: boolean;
   canBrowse?: boolean;
   component: TypeComponentMeasure;
-  hasBaseComponent: boolean;
   isBaseComponent?: boolean;
   metrics: Metric[];
   previous?: TypeComponentMeasure;
@@ -44,75 +42,64 @@ interface Props {
   showAnalysisDate?: boolean;
 }
 
-class Component extends React.PureComponent<Props> {
-  render() {
-    const {
-      branchLike,
-      canBePinned = true,
-      canBrowse = false,
-      component,
-      hasBaseComponent,
-      isBaseComponent = false,
-      metrics,
-      previous,
-      rootComponent,
-      selected = false,
-      newCodeSelected,
-      showAnalysisDate,
-    } = this.props;
+export default function Component(props: Props) {
+  const {
+    branchLike,
+    canBePinned = true,
+    canBrowse = false,
+    component,
+    isBaseComponent = false,
+    metrics,
+    previous,
+    rootComponent,
+    selected = false,
+    newCodeSelected,
+    showAnalysisDate,
+  } = props;
 
-    const isFile =
-      component.qualifier === ComponentQualifier.File ||
-      component.qualifier === ComponentQualifier.TestFile;
+  const isFile =
+    component.qualifier === ComponentQualifier.File ||
+    component.qualifier === ComponentQualifier.TestFile;
 
-    return (
-      <tr className={classNames({ selected, 'current-folder': isBaseComponent })}>
-        {canBePinned && (
-          <td className="thin nowrap">
-            {isFile && (
-              <WorkspaceContext.Consumer>
-                {({ openComponent }) => (
-                  <ComponentPin
-                    branchLike={branchLike}
-                    component={component}
-                    openComponent={openComponent}
-                  />
-                )}
-              </WorkspaceContext.Consumer>
-            )}
-          </td>
-        )}
-        <td className="code-name-cell">
-          <div className="display-flex-center">
-            {hasBaseComponent && <div className="code-child-component-icon" />}
-            <ComponentName
-              branchLike={branchLike}
-              canBrowse={canBrowse}
-              component={component}
-              previous={previous}
-              rootComponent={rootComponent}
-              unclickable={isBaseComponent}
-              newCodeSelected={newCodeSelected}
-            />
-          </div>
-        </td>
+  return (
+    <TableRowInteractive selected={selected}>
+      {canBePinned && (
+        <ContentCell className="sw-py-3">
+          {isFile && (
+            <WorkspaceContext.Consumer>
+              {({ openComponent }) => (
+                <ComponentPin
+                  branchLike={branchLike}
+                  component={component}
+                  openComponent={openComponent}
+                />
+              )}
+            </WorkspaceContext.Consumer>
+          )}
+        </ContentCell>
+      )}
+      <ContentCell className="it__code-name-cell sw-overflow-hidden">
+        <ComponentName
+          branchLike={branchLike}
+          canBrowse={canBrowse}
+          component={component}
+          previous={previous}
+          rootComponent={rootComponent}
+          unclickable={isBaseComponent}
+          newCodeSelected={newCodeSelected}
+        />
+      </ContentCell>
 
-        {metrics.map((metric) => (
-          <td className="text-center" key={metric.key}>
-            <ComponentMeasure component={component} metric={metric} />
-          </td>
-        ))}
+      {metrics.map((metric) => (
+        <ComponentMeasure component={component} key={metric.key} metric={metric} />
+      ))}
 
-        {showAnalysisDate && isBaseComponent && <td />}
-
-        {showAnalysisDate && !isBaseComponent && (
-          <td className="text-center">
-            {component.analysisDate ? <DateFromNow date={component.analysisDate} /> : '—'}
-          </td>
-        )}
-      </tr>
-    );
-  }
+      {showAnalysisDate && (
+        <NumericalCell>
+          {!isBaseComponent &&
+            (component.analysisDate ? <DateFromNow date={component.analysisDate} /> : '—')}
+        </NumericalCell>
+      )}
+    </TableRowInteractive>
+  );
 }
-
-export default withScrollTo(Component);
index 3b2392fbc5722dbb4b49545cf8c4e00d1423a857..e6ec611dc88ce80849076ba07dbdbb34e5f527ef 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 {
+  ContentCell,
+  MetricsEnum,
+  MetricsRatingBadge,
+  NumericalCell,
+  QualityGateIndicator,
+  RatingCell,
+} from 'design-system';
 import * as React from 'react';
 import Measure from '../../../components/measure/Measure';
 import { getLeakValue } from '../../../components/measure/utils';
-import { isDiffMetric } from '../../../helpers/measures';
-import { ComponentMeasure as TypeComponentMeasure, Metric } from '../../../types/types';
+import { translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric } from '../../../helpers/measures';
+import { isApplication, isProject } from '../../../types/component';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { Metric, Status, ComponentMeasure as TypeComponentMeasure } from '../../../types/types';
 
 interface Props {
   component: TypeComponentMeasure;
   metric: Metric;
 }
 
-export default class ComponentMeasure extends React.PureComponent<Props> {
-  render() {
-    const { component, metric } = this.props;
-    const isProject = component.qualifier === 'TRK';
-    const isReleasability = metric.key === 'releasability_rating';
+export default function ComponentMeasure(props: Props) {
+  const { component, metric } = props;
+  const isProjectLike = isProject(component.qualifier) || isApplication(component.qualifier);
+  const isReleasability = metric.key === MetricKey.releasability_rating;
 
-    const finalMetricKey = isProject && isReleasability ? 'alert_status' : metric.key;
-    const finalMetricType = isProject && isReleasability ? 'LEVEL' : metric.type;
+  const finalMetricKey = isProjectLike && isReleasability ? MetricKey.alert_status : metric.key;
+  const finalMetricType = isProjectLike && isReleasability ? MetricType.Level : metric.type;
 
-    const measure =
-      Array.isArray(component.measures) &&
-      component.measures.find((measure) => measure.metric === finalMetricKey);
+  const measure = Array.isArray(component.measures)
+    ? component.measures.find((measure) => measure.metric === finalMetricKey)
+    : undefined;
 
-    if (!measure) {
-      return measure === false ? <span /> : <span>—</span>;
-    }
+  const value = isDiffMetric(metric.key) ? getLeakValue(measure) : measure?.value;
+
+  switch (finalMetricType) {
+    case MetricType.Level: {
+      const formatted = formatMeasure(value, MetricType.Level);
+      const ariaLabel = translateWithParameters('overview.quality_gate_x', formatted);
 
-    const value = isDiffMetric(metric.key) ? getLeakValue(measure) : measure.value;
-    return <Measure metricKey={finalMetricKey} metricType={finalMetricType} value={value} />;
+      return (
+        <ContentCell>
+          <QualityGateIndicator
+            status={(value as Status) ?? 'NONE'}
+            className="sw-mr-2"
+            ariaLabel={ariaLabel}
+            size="sm"
+          />
+          <span>{formatted}</span>
+        </ContentCell>
+      );
+    }
+    case MetricType.Rating:
+      return (
+        <RatingCell>
+          <MetricsRatingBadge
+            label={value ?? '—'}
+            rating={formatMeasure(value, MetricType.Rating) as MetricsEnum}
+          />
+        </RatingCell>
+      );
+    default:
+      return (
+        <NumericalCell>
+          <Measure metricKey={finalMetricKey} metricType={finalMetricType} value={value} />
+        </NumericalCell>
+      );
   }
 }
index dbe0000d59d6b4fec3914941cd508f08c80308eb..d3a5baf2cef6e82c7f2bb84141096bd621f65b52 100644 (file)
@@ -17,9 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { HoverLink, LightLabel, QualifierIcon } from 'design-system';
+import { Badge, BranchIcon, HoverLink, LightLabel, Note, QualifierIcon } from 'design-system';
 import * as React from 'react';
-import BranchIcon from '../../../components/icons/BranchIcon';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
 import { CodeScope, getComponentOverviewUrl, queryToSearch } from '../../../helpers/urls';
@@ -75,8 +74,8 @@ export default function ComponentName({
     )
   ) {
     return (
-      <span className="max-width-100 display-inline-flex-center">
-        <span className="text-ellipsis" title={getTooltip(component)} aria-label={ariaLabel}>
+      <span className="sw-flex sw-items-center sw-overflow-hidden">
+        <div className="sw-truncate" title={getTooltip(component)} aria-label={ariaLabel}>
           {renderNameWithIcon(
             branchLike,
             component,
@@ -86,14 +85,16 @@ export default function ComponentName({
             canBrowse,
             newCodeSelected
           )}
-        </span>
+        </div>
         {component.branch ? (
-          <span className="text-ellipsis spacer-left">
+          <div className="sw-truncate sw-ml-2">
             <BranchIcon className="sw-mr-1" />
-            <span className="note">{component.branch}</span>
-          </span>
+            <Note>{component.branch}</Note>
+          </div>
         ) : (
-          <span className="spacer-left badge flex-1">{translate('branches.main_branch')}</span>
+          <Badge className="sw-ml-1" variant="default">
+            {translate('branches.main_branch')}
+          </Badge>
         )}
       </span>
     );
@@ -129,6 +130,7 @@ function renderNameWithIcon(
       : undefined;
     return (
       <HoverLink
+        icon={<QualifierIcon className="sw-mr-2" qualifier={component.qualifier} />}
         to={getComponentOverviewUrl(
           component.refKey ?? component.key,
           component.qualifier,
@@ -136,8 +138,7 @@ function renderNameWithIcon(
           codeType
         )}
       >
-        <QualifierIcon className="sw-mr-1" qualifier={component.qualifier} />
-        <span>{name}</span>
+        {name}
       </HoverLink>
     );
   } else if (canBrowse) {
@@ -146,15 +147,17 @@ function renderNameWithIcon(
       Object.assign(query, { selected: component.key });
     }
     return (
-      <HoverLink to={{ pathname: '/code', search: queryToSearch(query) }}>
-        <QualifierIcon className="sw-mr-1" qualifier={component.qualifier} />
-        <span>{name}</span>
+      <HoverLink
+        icon={<QualifierIcon className="sw-mr-2" qualifier={component.qualifier} />}
+        to={{ pathname: '/code', search: queryToSearch(query) }}
+      >
+        {name}
       </HoverLink>
     );
   }
   return (
-    <span className="sw-flex sw-items-center">
-      <QualifierIcon className="sw-mr-1" qualifier={component.qualifier} />
+    <span>
+      <QualifierIcon className="sw-mr-2 sw-align-text-bottom" qualifier={component.qualifier} />
       {name}
     </span>
   );
index 05a31151eb013b64858a20f16045710e2d4d125a..15de4e4d04cfb38764a8752ca93c218d55746867 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 { InteractiveIcon, PinIcon } from 'design-system';
 import * as React from 'react';
-import theme from '../../../app/theme';
-import { ButtonPlain } from '../../../components/controls/buttons';
-import PinIcon from '../../../components/icons/PinIcon';
 import { WorkspaceContextShape } from '../../../components/workspace/context';
 import { translateWithParameters } from '../../../helpers/l10n';
 import { BranchLike } from '../../../types/branch-like';
@@ -32,27 +30,23 @@ interface Props {
   openComponent: WorkspaceContextShape['openComponent'];
 }
 
-export default class ComponentPin extends React.PureComponent<Props> {
-  handleClick = () => {
-    this.props.openComponent({
-      branchLike: this.props.branchLike,
-      key: this.props.component.key,
-      name: this.props.component.path,
-      qualifier: this.props.component.qualifier,
+export default function ComponentPin(props: Props) {
+  const { branchLike, component, openComponent } = props;
+
+  const handleClick = React.useCallback(() => {
+    openComponent({
+      branchLike,
+      key: component.key,
+      name: component.path,
+      qualifier: component.qualifier,
     });
-  };
+  }, [branchLike, component, openComponent]);
+
+  const label = translateWithParameters('component_viewer.open_in_workspace_X', component.name);
 
-  render() {
-    const { name } = this.props.component;
-    return (
-      <ButtonPlain
-        className="link-no-underline"
-        preventDefault
-        onClick={this.handleClick}
-        title={translateWithParameters('component_viewer.open_in_workspace_X', name)}
-      >
-        <PinIcon fill={theme.colors.primary} />
-      </ButtonPlain>
-    );
-  }
+  return (
+    <span title={label}>
+      <InteractiveIcon aria-label={label} Icon={PinIcon} onClick={handleClick} />
+    </span>
+  );
 }
index 738cc70f5a9e4f5a4edb008a4d0ec82cd8a3c914..588d0c8c198b690aa02f4cd22b1f4e8b4e5e2448 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ContentCell, Table, TableRow } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
 import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation';
@@ -52,7 +53,7 @@ function Components(props: ComponentsProps) {
   } = props;
 
   const canBePinned =
-    baseComponent &&
+    baseComponent !== undefined &&
     ![
       ComponentQualifier.Application,
       ComponentQualifier.Portfolio,
@@ -61,69 +62,71 @@ function Components(props: ComponentsProps) {
 
   return (
     <div className="big-spacer-bottom table-wrapper">
-      <table className="data zebra">
-        {baseComponent && (
-          <ComponentsHeader
-            baseComponent={baseComponent}
-            canBePinned={canBePinned}
-            metrics={metrics.map((metric) => metric.key)}
-            rootComponent={rootComponent}
-            showAnalysisDate={showAnalysisDate}
-          />
-        )}
-        <tbody>
-          {baseComponent && (
-            <>
-              <Component
-                branchLike={branchLike}
+      <Table
+        gridTemplate={`${canBePinned ? 'min-content' : ''} auto repeat(${
+          metrics.length + (showAnalysisDate ? 1 : 0)
+        }, max-content)`}
+        header={
+          baseComponent && (
+            <TableRow>
+              <ComponentsHeader
+                baseComponent={baseComponent}
                 canBePinned={canBePinned}
-                component={baseComponent}
-                hasBaseComponent={false}
-                isBaseComponent
-                key={baseComponent.key}
-                metrics={metrics}
+                metrics={metrics.map((metric) => metric.key)}
                 rootComponent={rootComponent}
-                newCodeSelected={newCodeSelected}
                 showAnalysisDate={showAnalysisDate}
               />
-              <tr className="blank">
-                <td
-                  colSpan={metrics.length + 1 + (canBePinned ? 1 : 0) + (showAnalysisDate ? 1 : 0)}
-                />
-              </tr>
-            </>
-          )}
+            </TableRow>
+          )
+        }
+      >
+        {baseComponent && (
+          <>
+            <Component
+              branchLike={branchLike}
+              canBePinned={canBePinned}
+              component={baseComponent}
+              isBaseComponent
+              key={baseComponent.key}
+              metrics={metrics}
+              rootComponent={rootComponent}
+              newCodeSelected={newCodeSelected}
+              showAnalysisDate={showAnalysisDate}
+            />
+            <TableRow>
+              <ContentCell className="sw-col-span-full" />
+            </TableRow>
+          </>
+        )}
 
-          {components.length ? (
-            sortBy(
-              components,
-              (c) => c.qualifier,
-              (c) => c.name.toLowerCase(),
-              (c) => (c.branch ? c.branch.toLowerCase() : '')
-            ).map((component, index, list) => (
-              <Component
-                branchLike={branchLike}
-                canBePinned={canBePinned}
-                canBrowse
-                component={component}
-                hasBaseComponent={baseComponent !== undefined}
-                key={getComponentMeasureUniqueKey(component)}
-                metrics={metrics}
-                previous={index > 0 ? list[index - 1] : undefined}
-                rootComponent={rootComponent}
-                newCodeSelected={newCodeSelected}
-                showAnalysisDate={showAnalysisDate}
-                selected={
-                  selected &&
-                  getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected)
-                }
-              />
-            ))
-          ) : (
-            <ComponentsEmpty canBePinned={canBePinned} />
-          )}
-        </tbody>
-      </table>
+        {components.length ? (
+          sortBy(
+            components,
+            (c) => c.qualifier,
+            (c) => c.name.toLowerCase(),
+            (c) => (c.branch ? c.branch.toLowerCase() : '')
+          ).map((component, index, list) => (
+            <Component
+              branchLike={branchLike}
+              canBePinned={canBePinned}
+              canBrowse
+              component={component}
+              key={getComponentMeasureUniqueKey(component)}
+              metrics={metrics}
+              previous={index > 0 ? list[index - 1] : undefined}
+              rootComponent={rootComponent}
+              newCodeSelected={newCodeSelected}
+              showAnalysisDate={showAnalysisDate}
+              selected={
+                selected &&
+                getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected)
+              }
+            />
+          ))
+        ) : (
+          <ComponentsEmpty />
+        )}
+      </Table>
     </div>
   );
 }
index c6214240a139538666189b08d470831cfe612d25..ea492fc6c2e27d6663c46ce0606f9a6e8bb5d40c 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 { ContentCell, Note, TableRow } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 
-interface Props {
-  canBePinned?: boolean;
-}
-
-export default function ComponentsEmpty({ canBePinned = true }: Props) {
+export default function ComponentsEmpty() {
   return (
-    <tr>
-      {canBePinned && <td />}
-      <td className="note" colSpan={10}>
-        {translate('no_results')}
-      </td>
-    </tr>
+    <TableRow>
+      <ContentCell className="sw-col-span-full">
+        <Note role="status">{translate('no_results')}</Note>
+      </ContentCell>
+    </TableRow>
   );
 }
index 6041ebe807f37bf301f6f49f64ff29c1881bf42c..d37f4a9c544cec19e1b13c0bc2789315ca5cfa0d 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 { ContentCell, NumericalCell, RatingCell } from 'design-system';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 import { isPortfolioLike } from '../../../types/component';
+import { MetricKey } from '../../../types/metrics';
 import { ComponentMeasure } from '../../../types/types';
 
 interface ComponentsHeaderProps {
@@ -31,16 +33,17 @@ interface ComponentsHeaderProps {
 }
 
 const SHORT_NAME_METRICS = [
-  'duplicated_lines_density',
-  'new_lines',
-  'new_coverage',
-  'new_duplicated_lines_density',
+  MetricKey.duplicated_lines_density,
+  MetricKey.new_lines,
+  MetricKey.new_coverage,
+  MetricKey.new_duplicated_lines_density,
 ];
 
 export default function ComponentsHeader(props: ComponentsHeaderProps) {
   const { baseComponent, canBePinned = true, metrics, rootComponent, showAnalysisDate } = props;
   const isPortfolio = isPortfolioLike(rootComponent.qualifier);
   let columns: string[] = [];
+  let Cell: typeof NumericalCell;
   if (isPortfolio) {
     columns = [
       translate('metric_domain.Releasability'),
@@ -54,24 +57,25 @@ export default function ComponentsHeader(props: ComponentsHeaderProps) {
     if (showAnalysisDate) {
       columns.push(translate('code.last_analysis_date'));
     }
+
+    Cell = RatingCell;
   } else {
     columns = metrics.map((metric) =>
-      translate('metric', metric, SHORT_NAME_METRICS.includes(metric) ? 'short_name' : 'name')
+      translate(
+        'metric',
+        metric,
+        SHORT_NAME_METRICS.includes(metric as MetricKey) ? 'short_name' : 'name'
+      )
     );
+
+    Cell = NumericalCell;
   }
 
   return (
-    <thead>
-      <tr className="code-components-header">
-        {canBePinned && <th className="thin" aria-label={translate('code.pin')} />}
-        <th className="code-name-cell" aria-label={translate('code.name')} />
-        {baseComponent &&
-          columns.map((column) => (
-            <th className="text-center" key={column}>
-              {column}
-            </th>
-          ))}
-      </tr>
-    </thead>
+    <>
+      {canBePinned && <ContentCell aria-label={translate('code.pin')} />}
+      <ContentCell aria-label={translate('code.name')} />
+      {baseComponent && columns.map((column) => <Cell key={column}>{column}</Cell>)}
+    </>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/code/components/SearchResults.tsx b/server/sonar-web/src/main/js/apps/code/components/SearchResults.tsx
deleted file mode 100644 (file)
index db9e03c..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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 classNames from 'classnames';
-import { sortBy } from 'lodash';
-import * as React from 'react';
-import withKeyboardNavigation, {
-  WithKeyboardNavigationProps,
-} from '../../../components/hoc/withKeyboardNavigation';
-import { getComponentMeasureUniqueKey } from '../../../helpers/component';
-import { BranchLike } from '../../../types/branch-like';
-import { ComponentMeasure } from '../../../types/types';
-import ComponentName from './ComponentName';
-
-export interface SearchResultsProps extends WithKeyboardNavigationProps {
-  branchLike?: BranchLike;
-  rootComponent: ComponentMeasure;
-  newCodeSelected?: boolean;
-}
-
-function SearchResults(props: SearchResultsProps) {
-  const { branchLike, components, newCodeSelected, rootComponent, selected } = props;
-
-  return (
-    <ul>
-      {components &&
-        components.length > 0 &&
-        sortBy(
-          components,
-          (c) => c.qualifier,
-          (c) => c.name.toLowerCase(),
-          (c) => (c.branch ? c.branch.toLowerCase() : '')
-        ).map((component) => (
-          <li
-            className={classNames({ selected: selected?.key === component.key })}
-            key={getComponentMeasureUniqueKey(component)}
-          >
-            <ComponentName
-              branchLike={branchLike}
-              canBrowse
-              component={component}
-              rootComponent={rootComponent}
-              newCodeSelected={newCodeSelected}
-            />
-          </li>
-        ))}
-    </ul>
-  );
-}
-
-export default withKeyboardNavigation(SearchResults);
index bf83d336a6a4ac7a6761ac5e0ec804b816d3dc66..4ed74baf0ae5b2f49ab81292ae050f934ac9bf13 100644 (file)
@@ -45,7 +45,7 @@ export default function Measure({
   ratingComponent,
 }: Props) {
   if (value === undefined) {
-    return <span className={className}>â\80\93</span>;
+    return <span className={className}>â\80\94</span>;
   }
 
   if (metricType === MetricType.Level) {
@@ -57,11 +57,11 @@ export default function Measure({
       decimals,
       omitExtraDecimalZeros: metricType === MetricType.Percent,
     });
-    return <span className={className}>{formattedValue != null ? formattedValue : '–'}</span>;
+    return <span className={className}>{formattedValue ?? '—'}</span>;
   }
 
   const tooltip = <RatingTooltipContent metricKey={metricKey} value={value} />;
-  const rating = ratingComponent || <Rating value={value} />;
+  const rating = ratingComponent ?? <Rating value={value} />;
 
   if (tooltip) {
     return (
index 29e685efb6bb2344d2510b384572ba0aecd16032..3a30c9bed847b8433b5078fbd918d6233cf1b335 100644 (file)
@@ -37,6 +37,6 @@ exports[`renders trivial measure 1`] = `
 
 exports[`renders undefined measure 1`] = `
 <span>
-  â\80\93
+  â\80\94
 </span>
 `;