aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2023-05-26 13:12:24 +0200
committersonartech <sonartech@sonarsource.com>2023-06-05 20:02:47 +0000
commitd419d9392f7274dacfefc619bd0cee2fb71c4e7c (patch)
treed75b5c170dc18651b5b5d97e76ba088f98a7883d /server/sonar-web/src/main/js/apps
parentc078409f52612a2e18e18597044fbd354bc09344 (diff)
downloadsonarqube-d419d9392f7274dacfefc619bd0cee2fb71c4e7c.tar.gz
sonarqube-d419d9392f7274dacfefc619bd0cee2fb71c4e7c.zip
SONAR-19391 Prepare new layout structure
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx62
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx187
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx133
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentHeader.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx67
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx (renamed from server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx)36
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx121
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/style.css24
9 files changed, 289 insertions, 354 deletions
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
index d1d3c14888a..00000000000
--- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx
+++ /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/ComponentMeasuresApp.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
index a99b3684ad6..2aba7aba066 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx
@@ -17,7 +17,16 @@
* 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;
+`);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
index 304be402e5e..5f241e9e2c4 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentHeader.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentHeader.tsx
index 55e74cd8421..8f061c91919 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentHeader.tsx
@@ -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>
);
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
index 501ec9fac2e..2bac35e3559 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx
@@ -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/Breadcrumbs.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx
index d86ac9cae16..b5c578c07e8 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresBreadcrumbs.tsx
@@ -17,13 +17,16 @@
* 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';
-import Breadcrumb from './Breadcrumb';
interface Props {
backToFirst: boolean;
@@ -38,7 +41,7 @@ interface State {
breadcrumbs: ComponentMeasure[];
}
-export default class Breadcrumbs extends React.PureComponent<Props, State> {
+export default class MeasuresBreadcrumbs extends React.PureComponent<Props, State> {
mounted = false;
state: State = { breadcrumbs: [] };
@@ -94,22 +97,33 @@ export default class Breadcrumbs extends React.PureComponent<Props, State> {
render() {
const { breadcrumbs } = this.state;
+
if (breadcrumbs.length <= 0) {
return null;
}
- const lastItem = breadcrumbs[breadcrumbs.length - 1];
+
return (
- <div className={this.props.className}>
+ <Breadcrumbs
+ ariaLabel={translate('breadcrumbs')}
+ className={this.props.className}
+ maxWidth={500}
+ >
{breadcrumbs.map((component) => (
- <Breadcrumb
- canBrowse={component.key !== lastItem.key}
- component={component}
- handleSelect={this.props.handleSelect}
- isLast={component.key === lastItem.key}
+ <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>
))}
- </div>
+ </Breadcrumbs>
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx
index 4bfc270d36d..72bcaca5599 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasuresEmpty.tsx
@@ -17,13 +17,10 @@
* 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>;
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
index 7ffcfb03c8d..0b786954998 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
+++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx
@@ -17,72 +17,117 @@
* 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')};
+`);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css
index 4a23fbb3217..251b014ba44 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/style.css
+++ b/server/sonar-web/src/main/js/apps/component-measures/style.css
@@ -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;
}