@@ -23,7 +23,6 @@ interface Props { | |||
allowNewElements?: boolean; | |||
allowSearch?: boolean; | |||
createElementLabel: string; | |||
disableMessage?: string; | |||
elements: string[]; | |||
headerLabel: string; | |||
listSize?: number; | |||
@@ -31,6 +30,7 @@ interface Props { | |||
onSearch?: (query: string) => Promise<void>; | |||
onSelect: (item: string) => void; | |||
onUnselect: (item: string) => void; | |||
renderTooltip?: (item: string, disabled: boolean) => React.ReactNode; | |||
searchInputAriaLabel: string; | |||
selectedElements: string[]; | |||
selectedElementsDisabled?: string[]; | |||
@@ -42,7 +42,6 @@ export function MultiSelector(props: Readonly<Props>) { | |||
const { | |||
allowNewElements, | |||
createElementLabel, | |||
disableMessage, | |||
selectedElementsDisabled, | |||
headerLabel, | |||
noResultsLabel, | |||
@@ -50,6 +49,7 @@ export function MultiSelector(props: Readonly<Props>) { | |||
selectedElements, | |||
elements, | |||
allowSearch = true, | |||
renderTooltip, | |||
listSize = LIST_SIZE, | |||
} = props; | |||
@@ -58,7 +58,6 @@ export function MultiSelector(props: Readonly<Props>) { | |||
allowNewElements={allowNewElements} | |||
allowSearch={allowSearch} | |||
createElementLabel={createElementLabel} | |||
disableMessage={disableMessage} | |||
elements={elements} | |||
headerNode={<div className="sw-mt-4 sw-font-semibold">{headerLabel}</div>} | |||
listSize={listSize} | |||
@@ -67,6 +66,7 @@ export function MultiSelector(props: Readonly<Props>) { | |||
onSelect={props.onSelect} | |||
onUnselect={props.onUnselect} | |||
placeholder={searchInputAriaLabel} | |||
renderTooltip={renderTooltip} | |||
searchInputAriaLabel={searchInputAriaLabel} | |||
selectedElements={selectedElements} | |||
selectedElementsDisabled={selectedElementsDisabled} |
@@ -30,7 +30,6 @@ interface Props { | |||
allowSearch?: boolean; | |||
allowSelection?: boolean; | |||
createElementLabel: string; | |||
disableMessage?: string; | |||
elements: string[]; | |||
elementsDisabled?: string[]; | |||
footerNode?: React.ReactNode; | |||
@@ -42,6 +41,7 @@ interface Props { | |||
onSelect: (item: string) => void; | |||
onUnselect: (item: string) => void; | |||
placeholder: string; | |||
renderTooltip?: (element: string, disabled: boolean) => React.ReactNode; | |||
searchInputAriaLabel: string; | |||
selectedElements: string[]; | |||
selectedElementsDisabled?: string[]; | |||
@@ -265,7 +265,6 @@ export class MultiSelectMenu extends PureComponent<Props, State> { | |||
allowSelection = true, | |||
allowNewElements = true, | |||
createElementLabel, | |||
disableMessage, | |||
selectedElementsDisabled = [], | |||
headerNode = '', | |||
footerNode = '', | |||
@@ -273,6 +272,7 @@ export class MultiSelectMenu extends PureComponent<Props, State> { | |||
noResultsLabel, | |||
searchInputAriaLabel, | |||
elementsDisabled, | |||
renderTooltip, | |||
} = this.props; | |||
const { renderLabel } = this.props as PropsWithDefault; | |||
@@ -313,13 +313,13 @@ export class MultiSelectMenu extends PureComponent<Props, State> { | |||
<MultiSelectMenuOption | |||
active={activeElement === element} | |||
createElementLabel={createElementLabel} | |||
disableMessage={disableMessage} | |||
disabled={selectedElementsDisabled.includes(element)} | |||
element={element} | |||
key={element} | |||
onHover={this.handleElementHover} | |||
onSelectChange={this.handleSelectChange} | |||
renderLabel={renderLabel} | |||
renderTooltip={renderTooltip} | |||
selected | |||
/> | |||
))} | |||
@@ -334,6 +334,7 @@ export class MultiSelectMenu extends PureComponent<Props, State> { | |||
onHover={this.handleElementHover} | |||
onSelectChange={this.handleSelectChange} | |||
renderLabel={renderLabel} | |||
renderTooltip={renderTooltip} | |||
/> | |||
))} | |||
{elementsDisabled?.map((element) => ( | |||
@@ -346,6 +347,7 @@ export class MultiSelectMenu extends PureComponent<Props, State> { | |||
onHover={this.handleElementHover} | |||
onSelectChange={this.handleSelectChange} | |||
renderLabel={renderLabel} | |||
renderTooltip={renderTooltip} | |||
/> | |||
))} | |||
{showNewElement && ( |
@@ -27,12 +27,12 @@ export interface MultiSelectOptionProps { | |||
active?: boolean; | |||
createElementLabel: string; | |||
custom?: boolean; | |||
disableMessage?: string; | |||
disabled?: boolean; | |||
element: string; | |||
onHover: (element: string) => void; | |||
onSelectChange: (selected: boolean, element: string) => void; | |||
renderLabel?: (element: string) => React.ReactNode; | |||
renderTooltip?: (element: string, disabled: boolean) => React.ReactNode; | |||
selected?: boolean; | |||
} | |||
@@ -41,12 +41,12 @@ export function MultiSelectMenuOption(props: MultiSelectOptionProps) { | |||
active, | |||
createElementLabel, | |||
custom, | |||
disabled, | |||
disableMessage, | |||
disabled = false, | |||
element, | |||
onSelectChange, | |||
selected, | |||
renderLabel = identity, | |||
renderTooltip, | |||
} = props; | |||
const onHover = () => { | |||
@@ -56,7 +56,7 @@ export function MultiSelectMenuOption(props: MultiSelectOptionProps) { | |||
const label = renderLabel(element); | |||
return ( | |||
<Tooltip overlay={disabled && disableMessage} placement={PopupPlacement.Right}> | |||
<Tooltip overlay={renderTooltip?.(element, disabled)} placement={PopupPlacement.Right}> | |||
<ItemCheckbox | |||
checked={Boolean(selected)} | |||
className={classNames('sw-flex sw-py-2 sw-px-4', { active })} |
@@ -70,12 +70,13 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta | |||
}; | |||
render() { | |||
const availableTags = difference(this.state.searchResult, this.props.tags); | |||
const selectedTags = [...this.props.sysTags, ...this.props.tags]; | |||
const { sysTags, tags } = this.props; | |||
const { searchResult } = this.state; | |||
const availableTags = difference(searchResult, tags); | |||
const selectedTags = [...sysTags, ...tags]; | |||
return ( | |||
<MultiSelector | |||
createElementLabel={translate('coding_rules.create_tag')} | |||
disableMessage={translate('coding_rules.system_tags_tooltip')} | |||
headerLabel={translate('tags')} | |||
searchInputAriaLabel={translate('search.search_for_tags')} | |||
noResultsLabel={translate('no_results')} | |||
@@ -83,8 +84,14 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta | |||
onSelect={this.onSelect} | |||
onUnselect={this.onUnselect} | |||
selectedElements={selectedTags} | |||
selectedElementsDisabled={this.props.sysTags} | |||
selectedElementsDisabled={sysTags} | |||
elements={availableTags} | |||
renderTooltip={(element: string, disabled: boolean) => { | |||
if (sysTags.includes(element) && disabled) { | |||
return translate('coding_rules.system_tags_tooltip'); | |||
} | |||
return null; | |||
}} | |||
/> | |||
); | |||
} |
@@ -31,7 +31,6 @@ import { | |||
} from '../../../components/activity-graph/utils'; | |||
import { useLocation, useRouter } from '../../../components/hoc/withRouter'; | |||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | |||
import { HIDDEN_METRICS } from '../../../helpers/constants'; | |||
import { parseDate } from '../../../helpers/dates'; | |||
import useApplicationLeakQuery from '../../../queries/applications'; | |||
import { useBranchesQuery } from '../../../queries/branch'; | |||
@@ -106,16 +105,19 @@ export function ProjectActivityApp() { | |||
}, [appLeaks, component?.leakPeriodDate, component?.qualifier]); | |||
const filteredMetrics = React.useMemo(() => { | |||
if (isPortfolioLike(component?.qualifier)) { | |||
return Object.values(metrics).filter( | |||
(metric) => metric.key !== MetricKey.security_hotspots_reviewed, | |||
); | |||
} | |||
return Object.values(metrics).filter((metric) => { | |||
if ( | |||
isPortfolioLike(component?.qualifier) && | |||
metric.key === MetricKey.security_hotspots_reviewed | |||
) { | |||
return false; | |||
} | |||
if (isProject(component?.qualifier) && metric.key === MetricKey.security_review_rating) { | |||
return false; | |||
} | |||
return Object.values(metrics).filter( | |||
(metric) => | |||
![...HIDDEN_METRICS, MetricKey.security_review_rating].includes(metric.key as MetricKey), | |||
); | |||
return true; | |||
}); | |||
}, [component?.qualifier, metrics]); | |||
const handleUpdateQuery = (newQuery: Query) => { |
@@ -20,9 +20,10 @@ | |||
import { ButtonSecondary, ChevronDownIcon, Dropdown, TextMuted } from 'design-system'; | |||
import { sortBy } from 'lodash'; | |||
import * as React from 'react'; | |||
import { HIDDEN_METRICS } from '../../helpers/constants'; | |||
import { getLocalizedMetricName, translate } from '../../helpers/l10n'; | |||
import { isDiffMetric } from '../../helpers/measures'; | |||
import { MetricType } from '../../types/metrics'; | |||
import { MetricKey, MetricType } from '../../types/metrics'; | |||
import { Metric } from '../../types/types'; | |||
import AddGraphMetricPopup from './AddGraphMetricPopup'; | |||
@@ -59,10 +60,19 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> { | |||
) => { | |||
return metrics | |||
.filter((metric) => { | |||
if (metric.hidden) { | |||
return false; | |||
} | |||
if (isDiffMetric(metric.key)) { | |||
return false; | |||
} | |||
if ([MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType)) { | |||
return false; | |||
} | |||
if (HIDDEN_METRICS.includes(metric.key as MetricKey)) { | |||
return false; | |||
} | |||
if ( | |||
metric.hidden || | |||
isDiffMetric(metric.key) || | |||
[MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType) || | |||
selectedMetrics.includes(metric.key) || | |||
!getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase()) | |||
) { | |||
@@ -134,7 +144,6 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> { | |||
onSearch={this.onSearch} | |||
onSelect={this.onSelect} | |||
onUnselect={this.onUnselect} | |||
renderLabel={(element) => this.getLocalizedMetricNameFromKey(element)} | |||
selectedElements={selectedMetrics} | |||
/> | |||
} |
@@ -17,9 +17,13 @@ | |||
* 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 { Badge, FlagMessage, MultiSelectMenu } from 'design-system'; | |||
import * as React from 'react'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { FormattedMessage, useIntl } from 'react-intl'; | |||
import { DEPRECATED_ACTIVITY_METRICS } from '../../helpers/constants'; | |||
import { getLocalizedMetricName, translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { MetricKey } from '../../types/metrics'; | |||
import DocumentationLink from '../common/DocumentationLink'; | |||
export interface AddGraphMetricPopupProps { | |||
elements: string[]; | |||
@@ -29,7 +33,6 @@ export interface AddGraphMetricPopupProps { | |||
onSelect: (item: string) => void; | |||
onUnselect: (item: string) => void; | |||
popupPosition?: any; | |||
renderLabel: (element: string) => React.ReactNode; | |||
selectedElements: string[]; | |||
} | |||
@@ -38,6 +41,7 @@ export default function AddGraphMetricPopup({ | |||
metricsTypeFilter, | |||
...props | |||
}: AddGraphMetricPopupProps) { | |||
const intl = useIntl(); | |||
let footerNode: React.ReactNode = ''; | |||
if (props.selectedElements.length >= 6) { | |||
@@ -60,6 +64,45 @@ export default function AddGraphMetricPopup({ | |||
); | |||
} | |||
const renderLabel = (key: string) => { | |||
const metricName = getLocalizedMetricName({ key }); | |||
const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key as MetricKey); | |||
return ( | |||
<> | |||
{metricName} | |||
{isDeprecated && ( | |||
<Badge className="sw-ml-1">{intl.formatMessage({ id: 'deprecated' })}</Badge> | |||
)} | |||
</> | |||
); | |||
}; | |||
const renderTooltip = (key: string) => { | |||
const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key as MetricKey); | |||
if (isDeprecated) { | |||
return ( | |||
<FormattedMessage | |||
id="project_activity.custom_metric.deprecated" | |||
tagName="div" | |||
values={{ | |||
learn_more: ( | |||
<DocumentationLink | |||
className="sw-ml-2 sw-whitespace-nowrap" | |||
to="/user-guide/clean-code/code-analysis/" | |||
> | |||
{intl.formatMessage({ id: 'learn_more' })} | |||
</DocumentationLink> | |||
), | |||
}} | |||
/> | |||
); | |||
} | |||
return null; | |||
}; | |||
return ( | |||
<MultiSelectMenu | |||
createElementLabel="" | |||
@@ -74,7 +117,8 @@ export default function AddGraphMetricPopup({ | |||
onSelect={(item: string) => elements.includes(item) && props.onSelect(item)} | |||
onUnselect={props.onUnselect} | |||
placeholder={translate('search.search_for_metrics')} | |||
renderLabel={props.renderLabel} | |||
renderLabel={renderLabel} | |||
renderTooltip={renderTooltip} | |||
selectedElements={props.selectedElements} | |||
listSize={0} | |||
/> |
@@ -21,6 +21,7 @@ import { useTheme } from '@emotion/react'; | |||
import styled from '@emotion/styled'; | |||
import classNames from 'classnames'; | |||
import { | |||
Badge, | |||
CloseIcon, | |||
FlagWarningIcon, | |||
InteractiveIcon, | |||
@@ -29,7 +30,12 @@ import { | |||
themeColor, | |||
} from 'design-system'; | |||
import * as React from 'react'; | |||
import { FormattedMessage, useIntl } from 'react-intl'; | |||
import { DEPRECATED_ACTIVITY_METRICS } from '../../helpers/constants'; | |||
import { translateWithParameters } from '../../helpers/l10n'; | |||
import { MetricKey } from '../../types/metrics'; | |||
import DocumentationLink from '../common/DocumentationLink'; | |||
import Tooltip from '../controls/Tooltip'; | |||
import { ChartLegend } from './ChartLegend'; | |||
interface Props { | |||
@@ -49,9 +55,11 @@ export function GraphsLegendItem({ | |||
removeMetric, | |||
showWarning, | |||
}: Props) { | |||
const intl = useIntl(); | |||
const theme = useTheme() as Theme; | |||
const isActionable = removeMetric !== undefined; | |||
const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(metric as MetricKey); | |||
return ( | |||
<StyledLegendItem | |||
@@ -66,6 +74,29 @@ export function GraphsLegendItem({ | |||
<span className="sw-body-sm" style={{ color: themeColor('graphCursorLineColor')({ theme }) }}> | |||
{name} | |||
</span> | |||
{isDeprecated && ( | |||
<Tooltip | |||
overlay={ | |||
<FormattedMessage | |||
id="project_activity.custom_metric.deprecated" | |||
values={{ | |||
learn_more: ( | |||
<DocumentationLink | |||
className="sw-ml-2 sw-whitespace-nowrap" | |||
to="/user-guide/clean-code/code-analysis/" | |||
> | |||
{intl.formatMessage({ id: 'learn_more' })} | |||
</DocumentationLink> | |||
), | |||
}} | |||
/> | |||
} | |||
> | |||
<div> | |||
<Badge className="sw-ml-1">{intl.formatMessage({ id: 'deprecated' })}</Badge> | |||
</div> | |||
</Tooltip> | |||
)} | |||
{isActionable && ( | |||
<InteractiveIcon | |||
Icon={CloseIcon} |
@@ -132,6 +132,9 @@ it('should correctly handle adding/removing custom metrics', async () => { | |||
// We should see 2 graphs, correctly labelled. | |||
expect(ui.graphs.getAll()).toHaveLength(2); | |||
// old types and confirmed metrics should be deprecated and show a badge (both in dropdown and in legend) | |||
expect(ui.deprecatedBadge.getAll()).toHaveLength(6); | |||
// We cannot select anymore Int types. It should hide options, and show an alert. | |||
expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument(); | |||
expect(ui.hiddenOptionsAlert.get()).toBeInTheDocument(); | |||
@@ -172,6 +175,7 @@ function getPageObject() { | |||
// Add/remove metrics. | |||
addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }), | |||
deprecatedBadge: byText('deprecated'), | |||
bugsCheckbox: byRole('checkbox', { name: MetricKey.bugs }), | |||
newBugsCheckbox: byRole('checkbox', { name: MetricKey.new_bugs }), | |||
burnedBudgetCheckbox: byRole('checkbox', { name: MetricKey.burned_budget }), |
@@ -137,7 +137,23 @@ export const RATING_COLORS = [ | |||
{ fill: colors.error400, fillTransparent: colors.error400a20, stroke: colors.error700 }, | |||
]; | |||
export const HIDDEN_METRICS = [MetricKey.open_issues, MetricKey.reopened_issues]; | |||
export const HIDDEN_METRICS = [ | |||
MetricKey.open_issues, | |||
MetricKey.reopened_issues, | |||
MetricKey.high_impact_accepted_issues, | |||
]; | |||
export const DEPRECATED_ACTIVITY_METRICS = [ | |||
MetricKey.blocker_violations, | |||
MetricKey.critical_violations, | |||
MetricKey.major_violations, | |||
MetricKey.minor_violations, | |||
MetricKey.info_violations, | |||
MetricKey.code_smells, | |||
MetricKey.bugs, | |||
MetricKey.vulnerabilities, | |||
MetricKey.confirmed_issues, | |||
]; | |||
export const PROJECT_KEY_MAX_LEN = 400; | |||
@@ -1952,7 +1952,7 @@ project_activity.graphs.data_table.no_data_warning_check_dates_y=There is no dat | |||
project_activity.graphs.data_table.no_data_warning_check_dates_x_y=There is no data for the selected date range ({start} to {end}). Try modifying the date filters on the main page. | |||
project_activity.custom_metric.covered_lines=Covered Lines | |||
project_activity.custom_metric.deprecated=We are deprecating those filters to align the Activity page with our definition of Clean Code. They will be removed in a future release. {learn_more} | |||
#------------------------------------------------------------------------------ | |||
# |