+++ /dev/null
-/*
- * 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>
- );
- }
-}
+++ /dev/null
-/*
- * 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>
- );
- }
-}
* 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';
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,
import Sidebar from '../sidebar/Sidebar';
import '../style.css';
import {
+ Query,
banQualityGateMeasure,
getMeasuresPageMetricKeys,
groupByDomains,
hasTree,
hasTreemap,
parseQuery,
- Query,
serializeQuery,
sortMeasures,
} from '../utils';
);
}
- componentDidUpdate(prevProps: Props, prevState: State) {
+ componentDidUpdate(prevProps: Props) {
const prevQuery = parseQuery(prevProps.location.query);
const query = parseQuery(this.props.location.query);
) {
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']) {
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 =
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;
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
}
export default AppWithComponentContext;
+
+const StyledMain = withTheme(styled.main`
+ background-color: ${themeColor('filterbar')};
+ background-color: ${themeColor('pageBlock')};
+ border: ${themeBorder('default', 'pageBlockBorder')}l;
+`);
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';
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;
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>
);
}
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>
);
}
*/
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';
} 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;
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>
);
}
}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
* 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>;
}
* 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) {
}
return newOpenFacets;
}
+
+const StyledSidebar = withTheme(styled.div`
+ box-sizing: border-box;
+ margin-top: -2rem;
+
+ background-color: ${themeColor('filterbar')};
+ border-right: ${themeBorder('default', 'filterbarBorder')};
+`);
}
}
-.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;
}
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