diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2023-05-26 13:12:24 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-06-05 20:02:47 +0000 |
commit | d419d9392f7274dacfefc619bd0cee2fb71c4e7c (patch) | |
tree | d75b5c170dc18651b5b5d97e76ba088f98a7883d /server/sonar-web | |
parent | c078409f52612a2e18e18597044fbd354bc09344 (diff) | |
download | sonarqube-d419d9392f7274dacfefc619bd0cee2fb71c4e7c.tar.gz sonarqube-d419d9392f7274dacfefc619bd0cee2fb71c4e7c.zip |
SONAR-19391 Prepare new layout structure
Diffstat (limited to 'server/sonar-web')
-rw-r--r-- | server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.tsx | 62 | ||||
-rw-r--r-- | server/sonar-web/src/main/js/apps/component-measures/components/ComponentMeasuresApp.tsx | 187 | ||||
-rw-r--r-- | server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.tsx | 133 | ||||
-rw-r--r-- | server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentHeader.tsx | 6 | ||||
-rw-r--r-- | server/sonar-web/src/main/js/apps/component-measures/components/MeasureOverview.tsx | 67 | ||||
-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.tsx | 7 | ||||
-rw-r--r-- | server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.tsx | 121 | ||||
-rw-r--r-- | server/sonar-web/src/main/js/apps/component-measures/style.css | 24 |
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; } |