]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19391 Prepare new layout structure
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Fri, 26 May 2023 11:12:24 +0000 (13:12 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 5 Jun 2023 20:02:47 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx [deleted file]
server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.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/MeasureOverview.tsx
server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/component-measures/style.css
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx
deleted file mode 100644 (file)
index d1d3c14..0000000
+++ /dev/null
@@ -1,62 +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 * as React from 'react';
-import Tooltip from '../../../components/controls/Tooltip';
-import { collapsePath, limitComponentName } from '../../../helpers/path';
-import { ComponentMeasure, ComponentMeasureIntern } from '../../../types/types';
-
-interface Props {
-  canBrowse: boolean;
-  component: ComponentMeasure;
-  isLast: boolean;
-  handleSelect: (component: ComponentMeasureIntern) => void;
-}
-
-export default class Breadcrumb extends React.PureComponent<Props> {
-  handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
-    event.preventDefault();
-    event.currentTarget.blur();
-    this.props.handleSelect(this.props.component);
-  };
-
-  render() {
-    const { canBrowse, component, isLast } = this.props;
-    const isPath = component.qualifier === 'DIR';
-    const componentName = isPath
-      ? collapsePath(component.name, 15)
-      : limitComponentName(component.name);
-    const breadcrumbItem = canBrowse ? (
-      <a href="#" onClick={this.handleClick}>
-        {componentName}
-      </a>
-    ) : (
-      <span>{componentName}</span>
-    );
-
-    return (
-      <span>
-        <Tooltip overlay={component.name !== componentName ? component.name : undefined}>
-          {breadcrumbItem}
-        </Tooltip>
-        {!isLast && <span className="slash-separator" />}
-      </span>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx
deleted file mode 100644 (file)
index d86ac9c..0000000
+++ /dev/null
@@ -1,115 +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 * as React from 'react';
-import { getBreadcrumbs } from '../../../api/components';
-import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
-import { KeyboardKeys } from '../../../helpers/keycodes';
-import { BranchLike } from '../../../types/branch-like';
-import { ComponentMeasure, ComponentMeasureIntern } from '../../../types/types';
-import Breadcrumb from './Breadcrumb';
-
-interface Props {
-  backToFirst: boolean;
-  branchLike?: BranchLike;
-  className?: string;
-  component: ComponentMeasure;
-  handleSelect: (component: ComponentMeasureIntern) => void;
-  rootComponent: ComponentMeasure;
-}
-
-interface State {
-  breadcrumbs: ComponentMeasure[];
-}
-
-export default class Breadcrumbs extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { breadcrumbs: [] };
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchBreadcrumbs();
-    document.addEventListener('keydown', this.handleKeyDown);
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (
-      this.props.component !== prevProps.component ||
-      !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
-    ) {
-      this.fetchBreadcrumbs();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-    document.removeEventListener('keydown', this.handleKeyDown);
-  }
-
-  handleKeyDown = (event: KeyboardEvent) => {
-    if (event.key === KeyboardKeys.LeftArrow) {
-      event.preventDefault();
-      const { breadcrumbs } = this.state;
-      if (breadcrumbs.length > 1) {
-        const idx = this.props.backToFirst ? 0 : breadcrumbs.length - 2;
-        this.props.handleSelect(breadcrumbs[idx]);
-      }
-    }
-  };
-
-  fetchBreadcrumbs = () => {
-    const { branchLike, component, rootComponent } = this.props;
-    const isRoot = component.key === rootComponent.key;
-    if (isRoot) {
-      if (this.mounted) {
-        this.setState({ breadcrumbs: [component] });
-      }
-      return;
-    }
-    getBreadcrumbs({ component: component.key, ...getBranchLikeQuery(branchLike) }).then(
-      (breadcrumbs) => {
-        if (this.mounted) {
-          this.setState({ breadcrumbs });
-        }
-      },
-      () => {}
-    );
-  };
-
-  render() {
-    const { breadcrumbs } = this.state;
-    if (breadcrumbs.length <= 0) {
-      return null;
-    }
-    const lastItem = breadcrumbs[breadcrumbs.length - 1];
-    return (
-      <div className={this.props.className}>
-        {breadcrumbs.map((component) => (
-          <Breadcrumb
-            canBrowse={component.key !== lastItem.key}
-            component={component}
-            handleSelect={this.props.handleSelect}
-            isLast={component.key === lastItem.key}
-            key={component.key}
-          />
-        ))}
-      </div>
-    );
-  }
-}
index a99b3684ad6bf68b3fb742a0257b7a63d93b08eb..2aba7aba06678d291ba4b37ab39196b7c2db65fc 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 { withTheme } from '@emotion/react';
 import styled from '@emotion/styled';
+import {
+  DeferredSpinner,
+  LargeCenteredLayout,
+  Note,
+  PageContentFontWrapper,
+  themeBorder,
+  themeColor,
+} from 'design-system';
 import { debounce, keyBy } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
@@ -25,23 +34,14 @@ import { getMeasuresWithPeriod } from '../../../api/measures';
 import { getAllMetrics } from '../../../api/metrics';
 import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
 import { ComponentContext } from '../../../app/components/componentContext/ComponentContext';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
 import Suggestions from '../../../components/embed-docs-modal/Suggestions';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
 import { enhanceMeasure } from '../../../components/measure/utils';
 import '../../../components/search-navigator.css';
-import { Alert } from '../../../components/ui/Alert';
 import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../helpers/branch-like';
 import { translate } from '../../../helpers/l10n';
-import {
-  addSideBarClass,
-  addWhitePageClass,
-  removeSideBarClass,
-  removeWhitePageClass,
-} from '../../../helpers/pages';
 import { BranchLike } from '../../../types/branch-like';
-import { ComponentQualifier, isPortfolioLike } from '../../../types/component';
+import { ComponentQualifier } from '../../../types/component';
 import {
   ComponentMeasure,
   Dict,
@@ -53,6 +53,7 @@ import {
 import Sidebar from '../sidebar/Sidebar';
 import '../style.css';
 import {
+  Query,
   banQualityGateMeasure,
   getMeasuresPageMetricKeys,
   groupByDomains,
@@ -61,7 +62,6 @@ import {
   hasTree,
   hasTreemap,
   parseQuery,
-  Query,
   serializeQuery,
   sortMeasures,
 } from '../utils';
@@ -111,7 +111,7 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
     );
   }
 
-  componentDidUpdate(prevProps: Props, prevState: State) {
+  componentDidUpdate(prevProps: Props) {
     const prevQuery = parseQuery(prevProps.location.query);
     const query = parseQuery(this.props.location.query);
 
@@ -122,17 +122,10 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
     ) {
       this.fetchMeasures(this.state.metrics);
     }
-
-    if (prevState.measures.length === 0 && this.state.measures.length > 0) {
-      addWhitePageClass();
-      addSideBarClass();
-    }
   }
 
   componentWillUnmount() {
     this.mounted = false;
-    removeWhitePageClass();
-    removeSideBarClass();
   }
 
   fetchMeasures(metrics: State['metrics']) {
@@ -221,25 +214,31 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
   renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => {
     const { branchLike, component } = this.props;
     const { leakPeriod } = this.state;
+
     if (displayOverview) {
       return (
-        <MeasureOverviewContainer
-          branchLike={branchLike}
-          className="layout-page-main"
-          domain={query.metric}
-          leakPeriod={leakPeriod}
-          metrics={this.state.metrics}
-          onIssueChange={this.handleIssueChange}
-          rootComponent={component}
-          router={this.props.router}
-          selected={query.selected}
-          updateQuery={this.updateQuery}
-        />
+        <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
+          <MeasureOverviewContainer
+            branchLike={branchLike}
+            domain={query.metric}
+            leakPeriod={leakPeriod}
+            metrics={this.state.metrics}
+            onIssueChange={this.handleIssueChange}
+            rootComponent={component}
+            router={this.props.router}
+            selected={query.selected}
+            updateQuery={this.updateQuery}
+          />
+        </StyledMain>
       );
     }
 
     if (!metric) {
-      return <MeasuresEmpty />;
+      return (
+        <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
+          <MeasuresEmpty />
+        </StyledMain>
+      );
     }
 
     const hideDrilldown =
@@ -248,40 +247,32 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
 
     if (hideDrilldown) {
       return (
-        <main className="layout-page-main">
-          <div className="layout-page-main-inner">
-            <div className="note">{translate('component_measures.details_are_not_available')}</div>
-          </div>
-        </main>
+        <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
+          <Note>{translate('component_measures.details_are_not_available')}</Note>
+        </StyledMain>
       );
     }
 
     return (
-      <MeasureContent
-        branchLike={branchLike}
-        leakPeriod={leakPeriod}
-        metrics={this.state.metrics}
-        onIssueChange={this.handleIssueChange}
-        requestedMetric={metric}
-        rootComponent={component}
-        router={this.props.router}
-        selected={query.selected}
-        asc={query.asc}
-        updateQuery={this.updateQuery}
-        view={query.view}
-      />
+      <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
+        <MeasureContent
+          branchLike={branchLike}
+          leakPeriod={leakPeriod}
+          metrics={this.state.metrics}
+          onIssueChange={this.handleIssueChange}
+          requestedMetric={metric}
+          rootComponent={component}
+          router={this.props.router}
+          selected={query.selected}
+          asc={query.asc}
+          updateQuery={this.updateQuery}
+          view={query.view}
+        />
+      </StyledMain>
     );
   };
 
   render() {
-    if (this.state.loading) {
-      return (
-        <div className="display-flex-justify-center huge-spacer-top">
-          <i aria-label={translate('loading')} className="spinner" />
-        </div>
-      );
-    }
-
     const { branchLike } = this.props;
     const { measures } = this.state;
     const { canBrowseAllChildProjects, qualifier } = this.props.component;
@@ -291,58 +282,40 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> {
     const metric = this.getSelectedMetric(query, displayOverview);
 
     return (
-      <div id="component-measures">
+      <LargeCenteredLayout id="component-measures" className="sw-pt-8">
         <Suggestions suggestions="component_measures" />
         <Helmet defer={false} title={translate('layout.measures')} />
-        {measures.length > 0 ? (
-          <div className="layout-page">
-            <ScreenPositionHelper className="layout-page-side-outer">
-              {({ top }) => (
-                <div className="layout-page-side" style={{ top }}>
-                  <div className="layout-page-side-inner">
-                    {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
-                      <Alert
-                        className="big-spacer-top big-spacer-right big-spacer-left it__portfolio_warning"
-                        variant="warning"
-                      >
-                        <AlertContent>
-                          {translate('component_measures.not_all_measures_are_shown')}
-                          <HelpTooltip
-                            className="spacer-left"
-                            overlay={translate(
-                              'component_measures.not_all_measures_are_shown.help'
-                            )}
-                          />
-                        </AlertContent>
-                      </Alert>
-                    )}
-                    <div className="layout-page-filters">
-                      <Sidebar
-                        measures={measures}
-                        selectedMetric={metric ? metric.key : query.metric}
-                        showFullMeasures={showFullMeasures}
-                        updateQuery={this.updateQuery}
-                      />
-                    </div>
-                  </div>
-                </div>
-              )}
-            </ScreenPositionHelper>
-            {this.renderContent(displayOverview, query, metric)}
-          </div>
-        ) : (
-          <MeasuresEmpty />
-        )}
-      </div>
+        <PageContentFontWrapper>
+          <DeferredSpinner
+            className="my-10 sw-flex sw-content-center"
+            loading={this.state.loading}
+          />
+
+          {measures.length > 0 ? (
+            <div className="sw-grid sw-grid-cols-12 sw-w-full">
+              <Sidebar
+                canBrowseAllChildProjects={!!canBrowseAllChildProjects}
+                measures={measures}
+                qualifier={qualifier}
+                selectedMetric={metric ? metric.key : query.metric}
+                showFullMeasures={showFullMeasures}
+                updateQuery={this.updateQuery}
+              />
+              <div className="sw-col-span-9 sw-ml-12">
+                {this.renderContent(displayOverview, query, metric)}
+              </div>
+            </div>
+          ) : (
+            <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
+              <MeasuresEmpty />
+            </StyledMain>
+          )}
+        </PageContentFontWrapper>
+      </LargeCenteredLayout>
     );
   }
 }
 
-const AlertContent = styled.div`
-  display: flex;
-  align-items: center;
-`;
-
 /*
  * This needs to be refactored: the issue
  * is that we can't use the usual withComponentContext HOC, because the type
@@ -357,3 +330,9 @@ function AppWithComponentContext() {
 }
 
 export default AppWithComponentContext;
+
+const StyledMain = withTheme(styled.main`
+  background-color: ${themeColor('filterbar')};
+  background-color: ${themeColor('pageBlock')};
+  border: ${themeBorder('default', 'pageBlockBorder')}l;
+`);
index 304be402e5ef1956b94073f5e3a84db4cb10b33b..5f241e9e2c409c6e44baf29e219d3510c64e3b5f 100644 (file)
@@ -20,9 +20,9 @@
 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 SourceViewer from '../../../components/SourceViewer/SourceViewer';
 import PageActions from '../../../components/ui/PageActions';
 import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
 import { getComponentMeasureUniqueKey } from '../../../helpers/component';
@@ -48,11 +48,11 @@ import {
 import { complementary } from '../config/complementary';
 import FilesView from '../drilldown/FilesView';
 import TreeMapView from '../drilldown/TreeMapView';
-import { enhanceComponent, Query } from '../utils';
-import Breadcrumbs from './Breadcrumbs';
+import { Query, enhanceComponent } from '../utils';
 import MeasureContentHeader from './MeasureContentHeader';
 import MeasureHeader from './MeasureHeader';
 import MeasureViewSelect from './MeasureViewSelect';
+import MeasuresBreadcrumbs from './MeasuresBreadcrumbs';
 
 interface Props {
   branchLike?: BranchLike;
@@ -352,81 +352,68 @@ export default class MeasureContent extends React.PureComponent<Props, State> {
     const selectedIdx = this.getSelectedIndex();
 
     return (
-      <div
-        className="layout-page-main no-outline"
-        ref={(container) => (this.container = container)}
-      >
+      <div ref={(container) => (this.container = container)}>
         <A11ySkipTarget anchor="measures_main" />
 
-        <div className="layout-page-header-panel layout-page-main-header">
-          <div className="layout-page-header-panel-inner layout-page-main-header-inner">
-            <div className="layout-page-main-inner">
-              <MeasureContentHeader
-                left={
-                  <Breadcrumbs
-                    backToFirst={view === 'list'}
-                    branchLike={branchLike}
-                    className="text-ellipsis flex-1"
-                    component={baseComponent}
-                    handleSelect={this.onOpenComponent}
-                    rootComponent={rootComponent}
-                  />
-                }
-                right={
-                  <div className="display-flex-center">
-                    {!isFileComponent && metric && (
-                      <>
-                        <div id="measures-view-selection-label">
-                          {translate('component_measures.view_as')}
-                        </div>
-                        <MeasureViewSelect
-                          className="measure-view-select spacer-left big-spacer-right"
-                          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}
-                        />
-                      </>
-                    )}
+        <MeasureContentHeader
+          left={
+            <MeasuresBreadcrumbs
+              backToFirst={view === 'list'}
+              branchLike={branchLike}
+              className="sw-flex-1"
+              component={baseComponent}
+              handleSelect={this.onOpenComponent}
+              rootComponent={rootComponent}
+            />
+          }
+          right={
+            <div className="display-flex-center">
+              {!isFileComponent && metric && (
+                <>
+                  <div id="measures-view-selection-label">
+                    {translate('component_measures.view_as')}
                   </div>
-                }
-              />
+                  <MeasureViewSelect
+                    className="measure-view-select spacer-left big-spacer-right"
+                    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}
+                  />
+                </>
+              )}
             </div>
+          }
+        />
+
+        <MeasureHeader
+          branchLike={branchLike}
+          component={baseComponent}
+          leakPeriod={this.props.leakPeriod}
+          measureValue={measureValue}
+          metric={metric}
+          secondaryMeasure={secondaryMeasure}
+        />
+        {isFileComponent ? (
+          <div className="measure-details-viewer">
+            <SourceViewer
+              branchLike={branchLike}
+              component={baseComponent.key}
+              metricKey={this.state.metric?.key}
+              onIssueChange={this.props.onIssueChange}
+            />
           </div>
-        </div>
-
-        <div className="layout-page-main-inner measure-details-content">
-          <MeasureHeader
-            branchLike={branchLike}
-            component={baseComponent}
-            leakPeriod={this.props.leakPeriod}
-            measureValue={measureValue}
-            metric={metric}
-            secondaryMeasure={secondaryMeasure}
-          />
-          {isFileComponent ? (
-            <div className="measure-details-viewer">
-              <SourceViewer
-                branchLike={branchLike}
-                component={baseComponent.key}
-                metricKey={this.state.metric?.key}
-                onIssueChange={this.props.onIssueChange}
-              />
-            </div>
-          ) : (
-            this.renderMeasure()
-          )}
-        </div>
+        ) : (
+          this.renderMeasure()
+        )}
       </div>
     );
   }
index 55e74cd842113fa1747583672bd0d2ff12e26a15..8f061c919190e929144f3c76b5fd77c296ab4227 100644 (file)
@@ -26,9 +26,9 @@ interface Props {
 
 export default function MeasureContentHeader({ left, right }: Props) {
   return (
-    <div className="measure-content-header">
-      <div className="measure-content-header-left">{left}</div>
-      <div className="measure-content-header-right">{right}</div>
+    <div>
+      <div>{left}</div>
+      <div>{right}</div>
     </div>
   );
 }
index 501ec9fac2eb237868d8372c949c739c190e3157..2bac35e3559ec2c5db17da1bfe9e89a5cc0451b6 100644 (file)
@@ -19,8 +19,8 @@
  */
 import * as React from 'react';
 import { getComponentLeaves } from '../../../api/components';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
 import SourceViewer from '../../../components/SourceViewer/SourceViewer';
+import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
 import PageActions from '../../../components/ui/PageActions';
 import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
@@ -38,9 +38,9 @@ import {
 } from '../../../types/types';
 import BubbleChart from '../drilldown/BubbleChart';
 import { BUBBLES_FETCH_LIMIT, enhanceComponent, getBubbleMetrics, hasFullMeasures } from '../utils';
-import Breadcrumbs from './Breadcrumbs';
 import LeakPeriodLegend from './LeakPeriodLegend';
 import MeasureContentHeader from './MeasureContentHeader';
+import MeasuresBreadcrumbs from './MeasuresBreadcrumbs';
 
 interface Props {
   branchLike?: BranchLike;
@@ -154,43 +154,34 @@ export default class MeasureOverview extends React.PureComponent<Props, State> {
     const { branchLike, className, component, leakPeriod, loading, rootComponent } = this.props;
     const displayLeak = hasFullMeasures(branchLike);
     return (
-      <main className={className}>
-        <div className="layout-page-header-panel layout-page-main-header">
-          <A11ySkipTarget anchor="measures_main" />
+      <div className={className}>
+        <A11ySkipTarget anchor="measures_main" />
 
-          <div className="layout-page-header-panel-inner layout-page-main-header-inner">
-            <div className="layout-page-main-inner">
-              <MeasureContentHeader
-                left={
-                  <Breadcrumbs
-                    backToFirst={true}
-                    branchLike={branchLike}
-                    className="text-ellipsis"
-                    component={component}
-                    handleSelect={this.props.updateSelected}
-                    rootComponent={rootComponent}
-                  />
-                }
-                right={
-                  <PageActions
-                    componentQualifier={rootComponent.qualifier}
-                    current={this.state.components.length}
-                  />
-                }
-              />
-            </div>
-          </div>
-        </div>
-        <div className="layout-page-main-inner measure-details-content">
-          <div className="clearfix big-spacer-bottom">
-            {leakPeriod && displayLeak && (
-              <LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} />
-            )}
-          </div>
-          <DeferredSpinner loading={loading} />
-          {!loading && this.renderContent()}
-        </div>
-      </main>
+        <MeasureContentHeader
+          left={
+            <MeasuresBreadcrumbs
+              backToFirst={true}
+              branchLike={branchLike}
+              component={component}
+              handleSelect={this.props.updateSelected}
+              rootComponent={rootComponent}
+            />
+          }
+          right={
+            <PageActions
+              componentQualifier={rootComponent.qualifier}
+              current={this.state.components.length}
+            />
+          }
+        />
+        {leakPeriod && displayLeak && (
+          <LeakPeriodLegend className="pull-right" component={component} period={leakPeriod} />
+        )}
+
+        <DeferredSpinner loading={loading} />
+
+        {!loading && this.renderContent()}
+      </div>
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx
new file mode 100644 (file)
index 0000000..b5c578c
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ * 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 { Breadcrumbs, HoverLink } from 'design-system';
+import * as React from 'react';
+import { getBreadcrumbs } from '../../../api/components';
+import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like';
+import { KeyboardKeys } from '../../../helpers/keycodes';
+import { translate } from '../../../helpers/l10n';
+import { collapsePath, limitComponentName } from '../../../helpers/path';
+import { BranchLike } from '../../../types/branch-like';
+import { ComponentQualifier } from '../../../types/component';
+import { ComponentMeasure, ComponentMeasureIntern } from '../../../types/types';
+
+interface Props {
+  backToFirst: boolean;
+  branchLike?: BranchLike;
+  className?: string;
+  component: ComponentMeasure;
+  handleSelect: (component: ComponentMeasureIntern) => void;
+  rootComponent: ComponentMeasure;
+}
+
+interface State {
+  breadcrumbs: ComponentMeasure[];
+}
+
+export default class MeasuresBreadcrumbs extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { breadcrumbs: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchBreadcrumbs();
+    document.addEventListener('keydown', this.handleKeyDown);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (
+      this.props.component !== prevProps.component ||
+      !isSameBranchLike(prevProps.branchLike, this.props.branchLike)
+    ) {
+      this.fetchBreadcrumbs();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    document.removeEventListener('keydown', this.handleKeyDown);
+  }
+
+  handleKeyDown = (event: KeyboardEvent) => {
+    if (event.key === KeyboardKeys.LeftArrow) {
+      event.preventDefault();
+      const { breadcrumbs } = this.state;
+      if (breadcrumbs.length > 1) {
+        const idx = this.props.backToFirst ? 0 : breadcrumbs.length - 2;
+        this.props.handleSelect(breadcrumbs[idx]);
+      }
+    }
+  };
+
+  fetchBreadcrumbs = () => {
+    const { branchLike, component, rootComponent } = this.props;
+    const isRoot = component.key === rootComponent.key;
+    if (isRoot) {
+      if (this.mounted) {
+        this.setState({ breadcrumbs: [component] });
+      }
+      return;
+    }
+    getBreadcrumbs({ component: component.key, ...getBranchLikeQuery(branchLike) }).then(
+      (breadcrumbs) => {
+        if (this.mounted) {
+          this.setState({ breadcrumbs });
+        }
+      },
+      () => {}
+    );
+  };
+
+  render() {
+    const { breadcrumbs } = this.state;
+
+    if (breadcrumbs.length <= 0) {
+      return null;
+    }
+
+    return (
+      <Breadcrumbs
+        ariaLabel={translate('breadcrumbs')}
+        className={this.props.className}
+        maxWidth={500}
+      >
+        {breadcrumbs.map((component) => (
+          <HoverLink
+            key={component.key}
+            to="#"
+            onClick={(event: React.MouseEvent<HTMLAnchorElement>) => {
+              event.preventDefault();
+              event.currentTarget.blur();
+              this.props.handleSelect(component);
+            }}
+          >
+            {component.qualifier === ComponentQualifier.Directory
+              ? collapsePath(component.name, 15)
+              : limitComponentName(component.name)}
+          </HoverLink>
+        ))}
+      </Breadcrumbs>
+    );
+  }
+}
index 4bfc270d36dc59cc7df459c50536fad74c3bf8d8..72bcaca55993119afdcb337019bf604a69698ab0 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 { Note } from 'design-system/lib';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 
 export default function MeasuresEmpty() {
-  return (
-    <div className="page page-limited">
-      <div className="note">{translate('component_measures.empty')}</div>
-    </div>
-  );
+  return <Note>{translate('component_measures.empty')}</Note>;
 }
index 7ffcfb03c8d76a2775305638bf164a4fa0ffaee0..0b7869549984a7b397fd853e754342b1a2983be5 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 { withTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import {
+  FlagMessage,
+  LAYOUT_FOOTER_HEIGHT,
+  LAYOUT_GLOBAL_NAV_HEIGHT,
+  LAYOUT_PROJECT_NAV_HEIGHT,
+  themeBorder,
+  themeColor,
+} from 'design-system/lib';
 import * as React from 'react';
 import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
+import HelpTooltip from '../../../components/controls/HelpTooltip';
 import { translate } from '../../../helpers/l10n';
+import useFollowScroll from '../../../hooks/useFollowScroll';
+import { isPortfolioLike } from '../../../types/component';
 import { Dict, MeasureEnhanced } from '../../../types/types';
-import { groupByDomains, KNOWN_DOMAINS, PROJECT_OVERVEW, Query } from '../utils';
+import { KNOWN_DOMAINS, PROJECT_OVERVEW, Query, groupByDomains } from '../utils';
 import DomainFacet from './DomainFacet';
 import ProjectOverviewFacet from './ProjectOverviewFacet';
 
 interface Props {
+  canBrowseAllChildProjects: boolean;
   measures: MeasureEnhanced[];
+  qualifier: string;
   selectedMetric: string;
   showFullMeasures: boolean;
   updateQuery: (query: Partial<Query>) => void;
 }
 
-interface State {
-  openFacets: Dict<boolean>;
-}
-
-export default class Sidebar extends React.PureComponent<Props, State> {
-  static getDerivedStateFromProps(props: Props, state: State) {
-    return { openFacets: getOpenFacets(state.openFacets, props) };
-  }
+export default function Sidebar(props: Props) {
+  const {
+    showFullMeasures,
+    canBrowseAllChildProjects,
+    qualifier,
+    updateQuery,
+    selectedMetric,
+    measures,
+  } = props;
+  const [openFacets, setOpenFacets] = React.useState(getOpenFacets({}, props));
+  const { top: topScroll } = useFollowScroll();
 
-  state: State = {
-    openFacets: {},
-  };
+  const handleToggleFacet = React.useCallback(
+    (name: string) => {
+      setOpenFacets((openFacets) => ({ ...openFacets, [name]: !openFacets[name] }));
+    },
+    [setOpenFacets]
+  );
 
-  toggleFacet = (name: string) => {
-    this.setState(({ openFacets }) => ({
-      openFacets: { ...openFacets, [name]: !openFacets[name] },
-    }));
-  };
+  const handleChangeMetric = React.useCallback(
+    (metric: string) => {
+      updateQuery({ metric });
+    },
+    [updateQuery]
+  );
 
-  changeMetric = (metric: string) => {
-    this.props.updateQuery({ metric });
-  };
+  const distanceFromBottom = topScroll + window.innerHeight - document.body.clientHeight;
+  const footerVisibleHeight =
+    distanceFromBottom > -LAYOUT_FOOTER_HEIGHT ? LAYOUT_FOOTER_HEIGHT + distanceFromBottom : 0;
 
-  render() {
-    const { showFullMeasures } = this.props;
-    return (
-      <nav aria-label={translate('secondary')}>
+  return (
+    <StyledSidebar
+      className="sw-col-span-3"
+      style={{
+        top: `${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_PROJECT_NAV_HEIGHT}px`,
+        height: `calc(
+            100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_PROJECT_NAV_HEIGHT + footerVisibleHeight}px
+          )`,
+      }}
+    >
+      {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
+        <FlagMessage
+          ariaLabel={translate('component_measures.not_all_measures_are_shown')}
+          className="it__portfolio_warning"
+          variant="warning"
+        >
+          {translate('component_measures.not_all_measures_are_shown')}
+          <HelpTooltip
+            className="spacer-left"
+            overlay={translate('component_measures.not_all_measures_are_shown.help')}
+          />
+        </FlagMessage>
+      )}
+      <nav
+        className="sw-flex sw-flex-col sw-gap-4 sw-p-4"
+        aria-label={translate('component_measures.navigation')}
+      >
         <A11ySkipTarget
           anchor="measures_filters"
-          label={translate('component_measures.skip_to_filters')}
+          label={translate('component_measures.skip_to_navigation')}
           weight={10}
         />
         <ProjectOverviewFacet
-          onChange={this.changeMetric}
-          selected={this.props.selectedMetric}
+          onChange={handleChangeMetric}
+          selected={selectedMetric}
           value={PROJECT_OVERVEW}
         />
-        {groupByDomains(this.props.measures).map((domain) => (
+        {groupByDomains(measures).map((domain) => (
           <DomainFacet
             domain={domain}
             key={domain.name}
-            onChange={this.changeMetric}
-            onToggle={this.toggleFacet}
-            open={this.state.openFacets[domain.name] === true}
-            selected={this.props.selectedMetric}
+            onChange={handleChangeMetric}
+            onToggle={handleToggleFacet}
+            open={openFacets[domain.name] === true}
+            selected={selectedMetric}
             showFullMeasures={showFullMeasures}
           />
         ))}
       </nav>
-    );
-  }
+    </StyledSidebar>
+  );
 }
 
 function getOpenFacets(openFacets: Dict<boolean>, { measures, selectedMetric }: Props) {
@@ -95,3 +140,11 @@ function getOpenFacets(openFacets: Dict<boolean>, { measures, selectedMetric }:
   }
   return newOpenFacets;
 }
+
+const StyledSidebar = withTheme(styled.div`
+  box-sizing: border-box;
+  margin-top: -2rem;
+
+  background-color: ${themeColor('filterbar')};
+  border-right: ${themeBorder('default', 'filterbarBorder')};
+`);
index 4a23fbb32175af5abe4fd70f8285be78119352ed..251b014ba440531d9da78419d77b6855741dbcc2 100644 (file)
@@ -136,30 +136,6 @@ button.search-navigator-facet {
   }
 }
 
-.measure-content-header {
-  display: flex;
-  align-items: center;
-}
-
-.measure-content-header .measure-view-select {
-  width: 102px;
-}
-
-.measure-content-header-left {
-  flex: 1;
-  min-width: 0;
-  white-space: nowrap;
-}
-
-.measure-content-header-right .page-actions {
-  margin-bottom: 0;
-}
-
-.measure-content-header-right {
-  margin-left: calc(2 * var(--gridSize));
-  white-space: nowrap;
-}
-
 .measure-favorite svg {
   vertical-align: middle;
 }
index 5c4c7e949f54aba2a535ca4269ddeb7a79a113ff..6ea6b7b2ae9a88699d7be49a610435f313d11a00 100644 (file)
@@ -3656,7 +3656,8 @@ component_measures.to_select_files=to select files
 component_measures.to_navigate=to navigate
 component_measures.to_navigate_files=to next/previous file
 component_measures.hidden_best_score_metrics=There are {0} hidden components with a score of {1}.
-component_measures.skip_to_filters=Skip to measure filters
+component_measures.navigation=Measures navigation
+component_measures.skip_to_navigation=Skip to measure navigation
 
 component_measures.overview.project_overview.facet=Project Overview
 component_measures.overview.project_overview.title=Risk