aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2023-06-19 11:17:47 +0200
committersonartech <sonartech@sonarsource.com>2023-06-26 20:03:54 +0000
commit3b8f363175f8ef01fe4e888bccfd693bb9953852 (patch)
tree5300921de4e8a7fbb9c5148874d1c7b1f8d5301b
parentdf412e40611054143f326a2d364028b64f4a8bac (diff)
downloadsonarqube-3b8f363175f8ef01fe4e888bccfd693bb9953852.tar.gz
sonarqube-3b8f363175f8ef01fe4e888bccfd693bb9953852.zip
SONAR-19604 New UI for the Activity Graph header
-rw-r--r--server/sonar-web/design-system/src/components/MultiSelectMenu.tsx4
-rw-r--r--server/sonar-web/design-system/src/components/MultiSelectMenuOption.tsx22
-rw-r--r--server/sonar-web/design-system/src/theme/light.ts1
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx28
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx45
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx131
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx33
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx12
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts4
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/utils.ts4
-rw-r--r--server/sonar-web/src/main/js/types/metrics.ts1
13 files changed, 155 insertions, 140 deletions
diff --git a/server/sonar-web/design-system/src/components/MultiSelectMenu.tsx b/server/sonar-web/design-system/src/components/MultiSelectMenu.tsx
index b0c77bfcd74..ecfaa459512 100644
--- a/server/sonar-web/design-system/src/components/MultiSelectMenu.tsx
+++ b/server/sonar-web/design-system/src/components/MultiSelectMenu.tsx
@@ -264,6 +264,8 @@ export class MultiSelectMenu extends PureComponent<Props, State> {
noResultsLabel,
searchInputAriaLabel,
} = this.props;
+ const { renderLabel } = this.props as PropsWithDefault;
+
const { query, activeIdx, selectedElements, unselectedElements } = this.state;
const activeElement = this.getAllElements(this.props, this.state)[activeIdx];
const showNewElement = allowNewElements && this.isNewElement(query, this.props);
@@ -301,6 +303,7 @@ export class MultiSelectMenu extends PureComponent<Props, State> {
key={element}
onHover={this.handleElementHover}
onSelectChange={this.handleSelectChange}
+ renderLabel={renderLabel}
selected
/>
))}
@@ -314,6 +317,7 @@ export class MultiSelectMenu extends PureComponent<Props, State> {
key={element}
onHover={this.handleElementHover}
onSelectChange={this.handleSelectChange}
+ renderLabel={renderLabel}
/>
))}
{showNewElement && (
diff --git a/server/sonar-web/design-system/src/components/MultiSelectMenuOption.tsx b/server/sonar-web/design-system/src/components/MultiSelectMenuOption.tsx
index 27147703f96..e68dd7c3687 100644
--- a/server/sonar-web/design-system/src/components/MultiSelectMenuOption.tsx
+++ b/server/sonar-web/design-system/src/components/MultiSelectMenuOption.tsx
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
+import { identity } from 'lodash';
import { ItemCheckbox } from './DropdownMenu';
export interface MultiSelectOptionProps {
@@ -28,12 +29,27 @@ export interface MultiSelectOptionProps {
element: string;
onHover: (element: string) => void;
onSelectChange: (selected: boolean, element: string) => void;
+ renderLabel?: (element: string) => React.ReactNode;
selected?: boolean;
}
export function MultiSelectMenuOption(props: MultiSelectOptionProps) {
- const { active, createElementLabel, custom, disabled, element, onSelectChange, selected } = props;
- const onHover = () => props.onHover(element);
+ const {
+ active,
+ createElementLabel,
+ custom,
+ disabled,
+ element,
+ onSelectChange,
+ selected,
+ renderLabel = identity,
+ } = props;
+
+ const onHover = () => {
+ props.onHover(element);
+ };
+
+ const label = renderLabel(element);
return (
<ItemCheckbox
@@ -57,7 +73,7 @@ export function MultiSelectMenuOption(props: MultiSelectOptionProps) {
{element}
</span>
) : (
- <span className="sw-ml-3">{element}</span>
+ <span className="sw-ml-3">{label}</span>
)}
</ItemCheckbox>
);
diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts
index ac7d1fdd012..b7acbc729b2 100644
--- a/server/sonar-web/design-system/src/theme/light.ts
+++ b/server/sonar-web/design-system/src/theme/light.ts
@@ -460,6 +460,7 @@ export const lightTheme = {
graphZoomBackgroundColor: COLORS.blueGrey[25],
graphZoomBorderColor: COLORS.blueGrey[100],
graphZoomHandleColor: COLORS.blueGrey[400],
+ graphLegendBorder: secondary.darker,
// page
pageTitle: COLORS.blueGrey[700],
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx
index 3f54362aef6..3acdcfb0e13 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx
@@ -39,7 +39,7 @@ import {
Serie,
} from '../../../types/project-activity';
import { Metric } from '../../../types/types';
-import { datesQueryChanged, historyQueryChanged, Query } from '../utils';
+import { Query, datesQueryChanged, historyQueryChanged } from '../utils';
import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityApp';
interface Props {
@@ -132,8 +132,8 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
'x'
);
return {
- graphEndDate: lastValid && lastValid.x,
- graphStartDate: firstValid && firstValid.x,
+ graphEndDate: lastValid?.x,
+ graphStartDate: firstValid?.x,
};
}
return null;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
index f1c8eeb9311..2fd8e4b83c1 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
@@ -461,7 +461,9 @@ function getPageObject() {
},
async changeGraphType(type: GraphType) {
- await selectEvent.select(ui.graphTypeSelect.get(), [`project_activity.graphs.${type}`]);
+ await user.click(ui.graphTypeSelect.get());
+ const optionForType = await screen.findByText(`project_activity.graphs.${type}`);
+ await user.click(optionForType);
},
async openMetricsDropdown() {
diff --git a/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx
index 22f33da079a..57c72a6dc34 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx
@@ -17,13 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ButtonSecondary, ChevronDownIcon, Dropdown, TextMuted } from 'design-system';
import { sortBy } from 'lodash';
import * as React from 'react';
-import { Button } from '../../components/controls/buttons';
-import Dropdown from '../../components/controls/Dropdown';
-import DropdownIcon from '../../components/icons/DropdownIcon';
import { getLocalizedMetricName, translate } from '../../helpers/l10n';
import { isDiffMetric } from '../../helpers/measures';
+import { MetricType } from '../../types/metrics';
import { Metric } from '../../types/types';
import AddGraphMetricPopup from './AddGraphMetricPopup';
@@ -63,7 +62,7 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
if (
metric.hidden ||
isDiffMetric(metric.key) ||
- ['DATA', 'DISTRIB'].includes(metric.type) ||
+ [MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType) ||
selectedMetrics.includes(metric.key) ||
!getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase())
) {
@@ -120,11 +119,13 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
this.props.metrics,
this.props.selectedMetrics
);
+
return (
<Dropdown
- className="display-inline-block"
+ allowResizing
+ size="large"
closeOnClick={false}
- closeOnClickOutside
+ id="activity-graph-custom-metric-selector"
overlay={
<AddGraphMetricPopup
elements={filteredMetrics}
@@ -138,12 +139,15 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
/>
}
>
- <Button className="spacer-left">
- <span className="text-ellipsis text-middle">
- {translate('project_activity.graphs.custom.add')}
- </span>
- <DropdownIcon className="text-top little-spacer-left" />
- </Button>
+ <ButtonSecondary
+ className={
+ 'sw-ml-2 sw-body-sm sw-flex sw-flex-row sw-justify-between sw-pl-3 sw-pr-2 sw-w-32 ' +
+ 'sw-z-normal' // needed because the legends overlap part of the button
+ }
+ >
+ <TextMuted text={translate('project_activity.graphs.custom.add')} />
+ <ChevronDownIcon className="sw-ml-1 sw-mr-0 sw-pr-0" />
+ </ButtonSecondary>
</Dropdown>
);
}
diff --git a/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx
index 583b849a2f4..f762e83d855 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx
@@ -17,10 +17,9 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { FlagMessage, MultiSelectMenu } from 'design-system';
import * as React from 'react';
-import { Alert } from '../../components/ui/Alert';
import { translate, translateWithParameters } from '../../helpers/l10n';
-import MultiSelect from '../common/MultiSelect';
export interface AddGraphMetricPopupProps {
elements: string[];
@@ -43,13 +42,13 @@ export default function AddGraphMetricPopup({
if (props.selectedElements.length >= 6) {
footerNode = (
- <Alert className="spacer-left spacer-right spacer-top" variant="info">
+ <FlagMessage className="sw-m-2" variant="info">
{translate('project_activity.graphs.custom.add_metric_info')}
- </Alert>
+ </FlagMessage>
);
} else if (metricsTypeFilter && metricsTypeFilter.length > 0) {
footerNode = (
- <Alert className="spacer-left spacer-right spacer-top" variant="info">
+ <FlagMessage className="sw-m-2" variant="info">
{translateWithParameters(
'project_activity.graphs.custom.type_x_message',
metricsTypeFilter
@@ -57,26 +56,28 @@ export default function AddGraphMetricPopup({
.sort((a, b) => a.localeCompare(b))
.join(', ')
)}
- </Alert>
+ </FlagMessage>
);
}
return (
- <div className="menu abs-width-300">
- <MultiSelect
- allowNewElements={false}
- allowSelection={props.selectedElements.length < 6}
- elements={elements}
- filterSelected={props.filterSelected}
- footerNode={footerNode}
- legend={translate('project_activity.graphs.custom.select_metric')}
- onSearch={props.onSearch}
- onSelect={(item: string) => elements.includes(item) && props.onSelect(item)}
- onUnselect={props.onUnselect}
- placeholder={translate('search.search_for_metrics')}
- renderLabel={props.renderLabel}
- selectedElements={props.selectedElements}
- />
- </div>
+ <MultiSelectMenu
+ clearIconAriaLabel={translate('clear_verb')}
+ createElementLabel=""
+ searchInputAriaLabel={translate('project_activity.graphs.custom.select_metric')}
+ allowNewElements={false}
+ allowSelection={props.selectedElements.length < 6}
+ elements={elements}
+ filterSelected={props.filterSelected}
+ footerNode={footerNode}
+ noResultsLabel={translateWithParameters('no_results')}
+ onSearch={props.onSearch}
+ onSelect={(item: string) => elements.includes(item) && props.onSelect(item)}
+ onUnselect={props.onUnselect}
+ placeholder={translate('search.search_for_metrics')}
+ renderLabel={props.renderLabel}
+ selectedElements={props.selectedElements}
+ listSize={0}
+ />
);
}
diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx
index 971dbd53157..e2165221004 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx
@@ -31,7 +31,6 @@ import * as React from 'react';
import { translate } from '../../helpers/l10n';
import { GraphType } from '../../types/project-activity';
import { Metric } from '../../types/types';
-import Select from '../controls/Select';
import AddGraphMetric from './AddGraphMetric';
import './styles.css';
import { getGraphTypes, isCustomGraph } from './utils';
@@ -47,90 +46,76 @@ interface Props {
onUpdateGraph: (graphType: string) => void;
}
-export default class GraphsHeader extends React.PureComponent<Props> {
- handleGraphChange = (option: { value: string }) => {
- if (option.value !== this.props.graph) {
- this.props.onUpdateGraph(option.value);
- }
- };
+export default function GraphsHeader(props: Props) {
+ const {
+ className,
+ graph,
+ metrics,
+ metricsTypeFilter,
+ onUpdateGraph,
+ selectedMetrics = [],
+ } = props;
- render() {
- const { className, graph, metrics, metricsTypeFilter, selectedMetrics = [] } = this.props;
+ const handleGraphChange = React.useCallback(
+ (value: GraphType) => {
+ if (value !== graph) {
+ onUpdateGraph(value);
+ }
+ },
+ [graph, onUpdateGraph]
+ );
- const noCustomGraph =
- this.props.onAddCustomMetric === undefined || this.props.onRemoveCustomMetric === undefined;
+ const noCustomGraph =
+ props.onAddCustomMetric === undefined || props.onRemoveCustomMetric === undefined;
+ const options = React.useMemo(() => {
const types = getGraphTypes(noCustomGraph);
- const overlayItems: JSX.Element[] = [];
-
- const selectOptions: Array<{
- label: string;
- value: GraphType;
- }> = [];
-
- types.forEach((type) => {
+ return types.map((type) => {
const label = translate('project_activity.graphs', type);
- selectOptions.push({ label, value: type });
-
- overlayItems.push(
- <ItemButton key={label} onClick={() => this.handleGraphChange({ value: type })}>
+ return (
+ <ItemButton key={label} onClick={() => handleGraphChange(type)}>
{label}
</ItemButton>
);
});
+ }, [noCustomGraph, handleGraphChange]);
- const selectedOption = selectOptions.find((option) => option.value === graph);
- const selectedLabel = selectedOption?.label ?? '';
+ return (
+ <div className={className}>
+ <div className="sw-flex">
+ <Dropdown
+ id="activity-graph-type"
+ size="auto"
+ placement={PopupPlacement.BottomLeft}
+ zLevel={PopupZLevel.Content}
+ overlay={options}
+ >
+ <ButtonSecondary
+ aria-label={translate('project_activity.graphs.choose_type')}
+ className={
+ 'sw-body-sm sw-flex sw-flex-row sw-justify-between sw-pl-3 sw-pr-2 sw-w-32 ' +
+ 'sw-z-normal' // needed because the legends overlap part of the button
+ }
+ >
+ <TextMuted text={translate('project_activity.graphs', graph)} />
+ <ChevronDownIcon className="sw-ml-1 sw-mr-0 sw-pr-0" />
+ </ButtonSecondary>
+ </Dropdown>
- return (
- <div className={className}>
- <div className="display-flex-end">
- <div className="display-flex-column">
- {noCustomGraph ? (
- <Dropdown
- id="activity-graph-type"
- size="auto"
- placement={PopupPlacement.BottomLeft}
- zLevel={PopupZLevel.Content}
- overlay={overlayItems}
- >
- <ButtonSecondary
- aria-label={translate('project_activity.graphs.choose_type')}
- className={
- 'sw-body-sm sw-flex sw-flex-row sw-justify-between sw-pl-3 sw-pr-2 sw-w-32 ' +
- 'sw-z-normal' // needed because the legends overlap part of the button
- }
- >
- <TextMuted text={selectedLabel} />
- <ChevronDownIcon className="sw-ml-1 sw-mr-0 sw-pr-0" />
- </ButtonSecondary>
- </Dropdown>
- ) : (
- <Select
- aria-label={translate('project_activity.graphs.choose_type')}
- className="input-medium"
- isSearchable={false}
- onChange={this.handleGraphChange}
- options={selectOptions}
- value={selectedOption}
- />
- )}
- </div>
- {isCustomGraph(graph) &&
- this.props.onAddCustomMetric !== undefined &&
- this.props.onRemoveCustomMetric !== undefined && (
- <AddGraphMetric
- onAddMetric={this.props.onAddCustomMetric}
- metrics={metrics}
- metricsTypeFilter={metricsTypeFilter}
- onRemoveMetric={this.props.onRemoveCustomMetric}
- selectedMetrics={selectedMetrics}
- />
- )}
- </div>
+ {isCustomGraph(graph) &&
+ props.onAddCustomMetric !== undefined &&
+ props.onRemoveCustomMetric !== undefined && (
+ <AddGraphMetric
+ onAddMetric={props.onAddCustomMetric}
+ metrics={metrics}
+ metricsTypeFilter={metricsTypeFilter}
+ onRemoveMetric={props.onRemoveCustomMetric}
+ selectedMetrics={selectedMetrics}
+ />
+ )}
</div>
- );
- }
+ </div>
+ );
}
diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx
index e216dbf51ea..a272da3be62 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx
@@ -19,11 +19,10 @@
*/
import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
import classNames from 'classnames';
-import { Theme, themeColor } from 'design-system';
+import { CloseIcon, DestructiveIcon, FlagWarningIcon, Theme, themeColor } from 'design-system';
import * as React from 'react';
-import { ClearButton } from '../../components/controls/buttons';
-import AlertWarnIcon from '../../components/icons/AlertWarnIcon';
import { ChartLegendIcon } from '../../components/icons/ChartLegendIcon';
import { translateWithParameters } from '../../helpers/l10n';
@@ -48,29 +47,31 @@ export function GraphsLegendItem({
const isActionable = removeMetric !== undefined;
- const legendClass = classNames({ 'activity-graph-legend-actionable': isActionable }, className);
-
return (
- <span className={legendClass}>
+ <StyledLegendItem className={classNames('sw-px-2 sw-py-1 sw-rounded-pill', className)}>
{showWarning ? (
- <AlertWarnIcon className="sw-mr-2" />
+ <FlagWarningIcon className="sw-mx-2" />
) : (
- <ChartLegendIcon className="sw-align-middle sw-mr-2" index={index} />
+ <ChartLegendIcon className="sw-mx-2" index={index} />
)}
- <span
- className="sw-align-middle sw-body-sm"
- style={{ color: themeColor('graphCursorLineColor')({ theme }) }}
- >
+ <span className="sw-body-sm" style={{ color: themeColor('graphCursorLineColor')({ theme }) }}>
{name}
</span>
{isActionable && (
- <ClearButton
+ <DestructiveIcon
+ Icon={CloseIcon}
aria-label={translateWithParameters('project_activity.graphs.custom.remove_metric', name)}
- className="button-tiny sw-align-middle sw-ml-2"
- iconProps={{ size: 12 }}
+ className="sw-ml-2"
+ size="small"
onClick={() => removeMetric(metric)}
/>
)}
- </span>
+ </StyledLegendItem>
);
}
+
+const StyledLegendItem = styled.div`
+ display: flex;
+ align-items: center;
+ border: 1px solid ${themeColor('graphLegendBorder')};
+`;
diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
index c4256bf96f8..7292dba8360 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/ActivityGraph-it.tsx
@@ -21,12 +21,12 @@ import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { times } from 'lodash';
import * as React from 'react';
-import selectEvent from 'react-select-event';
import { parseDate } from '../../../helpers/dates';
import { mockHistoryItem, mockMeasureHistory } from '../../../helpers/mocks/project-activity';
import { mockMetric } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
import { byLabelText, byPlaceholderText, byRole, byText } from '../../../helpers/testSelector';
+import { FCProps } from '../../../helpers/testUtils';
import { MetricKey } from '../../../types/metrics';
import { GraphType, MeasureHistory } from '../../../types/project-activity';
import { Metric } from '../../../types/types';
@@ -143,9 +143,7 @@ it('should correctly handle adding/removing custom metrics', async () => {
// We cannot select anymore options. It should disable all remaining options, and
// show a different alert.
expect(ui.maxOptionsAlert.get()).toBeInTheDocument();
- // See https://github.com/testing-library/jest-dom/issues/144 for why we cannot
- // use isDisabled().
- expect(ui.vulnerabilityCheckbox.get()).toHaveAttribute('aria-disabled', 'true');
+ expect(ui.vulnerabilityCheckbox.get()).toBeDisabled();
// Disable a few options.
await ui.clickOnMetric(MetricKey.bugs);
@@ -212,7 +210,9 @@ function getPageObject() {
ui: {
...ui,
async changeGraphType(type: GraphType) {
- await selectEvent.select(ui.graphTypeSelect.get(), [`project_activity.graphs.${type}`]);
+ await user.click(ui.graphTypeSelect.get());
+ const optionForType = await screen.findByText(`project_activity.graphs.${type}`);
+ await user.click(optionForType);
},
async openAddMetrics() {
await user.click(ui.addMetricBtn.get());
@@ -238,7 +238,7 @@ function getPageObject() {
function renderActivityGraph(
graphsHistoryProps: Partial<GraphsHistory['props']> = {},
- graphsHeaderProps: Partial<GraphsHeader['props']> = {}
+ graphsHeaderProps: Partial<FCProps<typeof GraphsHeader>> = {}
) {
function ActivityGraph() {
const [selectedMetrics, setSelectedMetrics] = React.useState<string[]>([]);
diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts b/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts
index 22e86d7bdde..83cf694261e 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts
@@ -189,13 +189,13 @@ describe('saveActivityGraph', () => {
expect(save).toHaveBeenCalledWith('foo', GraphType.issues, 'bar');
});
- it.each([undefined, [MetricKey.bugs, MetricKey.alert_status]])(
+ it.each([[[]], [[MetricKey.bugs, MetricKey.alert_status]]])(
'should correctly store data for custom graph types',
(metrics) => {
utils.saveActivityGraph('foo', 'bar', GraphType.custom, metrics);
expect(save).toHaveBeenCalledWith('foo', GraphType.custom, 'bar');
// eslint-disable-next-line jest/no-conditional-in-test
- expect(save).toHaveBeenCalledWith('foo.custom', metrics ? metrics.join(',') : '', 'bar');
+ expect(save).toHaveBeenCalledWith('foo.custom', metrics.join(','), 'bar');
}
);
});
diff --git a/server/sonar-web/src/main/js/components/activity-graph/utils.ts b/server/sonar-web/src/main/js/components/activity-graph/utils.ts
index a24dfad62f5..d7628fab4f6 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/utils.ts
+++ b/server/sonar-web/src/main/js/components/activity-graph/utils.ts
@@ -148,11 +148,11 @@ export function saveActivityGraph(
namespace: string,
project: string,
graph: GraphType,
- metrics: string[] = []
+ metrics?: string[]
) {
save(namespace, graph, project);
- if (isCustomGraph(graph)) {
+ if (isCustomGraph(graph) && metrics) {
save(`${namespace}.custom`, metrics.join(','), project);
}
}
diff --git a/server/sonar-web/src/main/js/types/metrics.ts b/server/sonar-web/src/main/js/types/metrics.ts
index ce097cb4ada..5a333151ab8 100644
--- a/server/sonar-web/src/main/js/types/metrics.ts
+++ b/server/sonar-web/src/main/js/types/metrics.ts
@@ -154,6 +154,7 @@ export enum MetricType {
ShortInteger = 'SHORT_INT',
ShortWorkDuration = 'SHORT_WORK_DUR',
Data = 'DATA',
+ Distribution = 'DISTRIB',
}
export function isMetricKey(key: string): key is MetricKey {