allowNewElements?: boolean; | allowNewElements?: boolean; | ||||
allowSearch?: boolean; | allowSearch?: boolean; | ||||
createElementLabel: string; | createElementLabel: string; | ||||
disableMessage?: string; | |||||
elements: string[]; | elements: string[]; | ||||
headerLabel: string; | headerLabel: string; | ||||
listSize?: number; | listSize?: number; | ||||
onSearch?: (query: string) => Promise<void>; | onSearch?: (query: string) => Promise<void>; | ||||
onSelect: (item: string) => void; | onSelect: (item: string) => void; | ||||
onUnselect: (item: string) => void; | onUnselect: (item: string) => void; | ||||
renderTooltip?: (item: string, disabled: boolean) => React.ReactNode; | |||||
searchInputAriaLabel: string; | searchInputAriaLabel: string; | ||||
selectedElements: string[]; | selectedElements: string[]; | ||||
selectedElementsDisabled?: string[]; | selectedElementsDisabled?: string[]; | ||||
const { | const { | ||||
allowNewElements, | allowNewElements, | ||||
createElementLabel, | createElementLabel, | ||||
disableMessage, | |||||
selectedElementsDisabled, | selectedElementsDisabled, | ||||
headerLabel, | headerLabel, | ||||
noResultsLabel, | noResultsLabel, | ||||
selectedElements, | selectedElements, | ||||
elements, | elements, | ||||
allowSearch = true, | allowSearch = true, | ||||
renderTooltip, | |||||
listSize = LIST_SIZE, | listSize = LIST_SIZE, | ||||
} = props; | } = props; | ||||
allowNewElements={allowNewElements} | allowNewElements={allowNewElements} | ||||
allowSearch={allowSearch} | allowSearch={allowSearch} | ||||
createElementLabel={createElementLabel} | createElementLabel={createElementLabel} | ||||
disableMessage={disableMessage} | |||||
elements={elements} | elements={elements} | ||||
headerNode={<div className="sw-mt-4 sw-font-semibold">{headerLabel}</div>} | headerNode={<div className="sw-mt-4 sw-font-semibold">{headerLabel}</div>} | ||||
listSize={listSize} | listSize={listSize} | ||||
onSelect={props.onSelect} | onSelect={props.onSelect} | ||||
onUnselect={props.onUnselect} | onUnselect={props.onUnselect} | ||||
placeholder={searchInputAriaLabel} | placeholder={searchInputAriaLabel} | ||||
renderTooltip={renderTooltip} | |||||
searchInputAriaLabel={searchInputAriaLabel} | searchInputAriaLabel={searchInputAriaLabel} | ||||
selectedElements={selectedElements} | selectedElements={selectedElements} | ||||
selectedElementsDisabled={selectedElementsDisabled} | selectedElementsDisabled={selectedElementsDisabled} |
allowSearch?: boolean; | allowSearch?: boolean; | ||||
allowSelection?: boolean; | allowSelection?: boolean; | ||||
createElementLabel: string; | createElementLabel: string; | ||||
disableMessage?: string; | |||||
elements: string[]; | elements: string[]; | ||||
elementsDisabled?: string[]; | elementsDisabled?: string[]; | ||||
footerNode?: React.ReactNode; | footerNode?: React.ReactNode; | ||||
onSelect: (item: string) => void; | onSelect: (item: string) => void; | ||||
onUnselect: (item: string) => void; | onUnselect: (item: string) => void; | ||||
placeholder: string; | placeholder: string; | ||||
renderTooltip?: (element: string, disabled: boolean) => React.ReactNode; | |||||
searchInputAriaLabel: string; | searchInputAriaLabel: string; | ||||
selectedElements: string[]; | selectedElements: string[]; | ||||
selectedElementsDisabled?: string[]; | selectedElementsDisabled?: string[]; | ||||
allowSelection = true, | allowSelection = true, | ||||
allowNewElements = true, | allowNewElements = true, | ||||
createElementLabel, | createElementLabel, | ||||
disableMessage, | |||||
selectedElementsDisabled = [], | selectedElementsDisabled = [], | ||||
headerNode = '', | headerNode = '', | ||||
footerNode = '', | footerNode = '', | ||||
noResultsLabel, | noResultsLabel, | ||||
searchInputAriaLabel, | searchInputAriaLabel, | ||||
elementsDisabled, | elementsDisabled, | ||||
renderTooltip, | |||||
} = this.props; | } = this.props; | ||||
const { renderLabel } = this.props as PropsWithDefault; | const { renderLabel } = this.props as PropsWithDefault; | ||||
<MultiSelectMenuOption | <MultiSelectMenuOption | ||||
active={activeElement === element} | active={activeElement === element} | ||||
createElementLabel={createElementLabel} | createElementLabel={createElementLabel} | ||||
disableMessage={disableMessage} | |||||
disabled={selectedElementsDisabled.includes(element)} | disabled={selectedElementsDisabled.includes(element)} | ||||
element={element} | element={element} | ||||
key={element} | key={element} | ||||
onHover={this.handleElementHover} | onHover={this.handleElementHover} | ||||
onSelectChange={this.handleSelectChange} | onSelectChange={this.handleSelectChange} | ||||
renderLabel={renderLabel} | renderLabel={renderLabel} | ||||
renderTooltip={renderTooltip} | |||||
selected | selected | ||||
/> | /> | ||||
))} | ))} | ||||
onHover={this.handleElementHover} | onHover={this.handleElementHover} | ||||
onSelectChange={this.handleSelectChange} | onSelectChange={this.handleSelectChange} | ||||
renderLabel={renderLabel} | renderLabel={renderLabel} | ||||
renderTooltip={renderTooltip} | |||||
/> | /> | ||||
))} | ))} | ||||
{elementsDisabled?.map((element) => ( | {elementsDisabled?.map((element) => ( | ||||
onHover={this.handleElementHover} | onHover={this.handleElementHover} | ||||
onSelectChange={this.handleSelectChange} | onSelectChange={this.handleSelectChange} | ||||
renderLabel={renderLabel} | renderLabel={renderLabel} | ||||
renderTooltip={renderTooltip} | |||||
/> | /> | ||||
))} | ))} | ||||
{showNewElement && ( | {showNewElement && ( |
active?: boolean; | active?: boolean; | ||||
createElementLabel: string; | createElementLabel: string; | ||||
custom?: boolean; | custom?: boolean; | ||||
disableMessage?: string; | |||||
disabled?: boolean; | disabled?: boolean; | ||||
element: string; | element: string; | ||||
onHover: (element: string) => void; | onHover: (element: string) => void; | ||||
onSelectChange: (selected: boolean, element: string) => void; | onSelectChange: (selected: boolean, element: string) => void; | ||||
renderLabel?: (element: string) => React.ReactNode; | renderLabel?: (element: string) => React.ReactNode; | ||||
renderTooltip?: (element: string, disabled: boolean) => React.ReactNode; | |||||
selected?: boolean; | selected?: boolean; | ||||
} | } | ||||
active, | active, | ||||
createElementLabel, | createElementLabel, | ||||
custom, | custom, | ||||
disabled, | |||||
disableMessage, | |||||
disabled = false, | |||||
element, | element, | ||||
onSelectChange, | onSelectChange, | ||||
selected, | selected, | ||||
renderLabel = identity, | renderLabel = identity, | ||||
renderTooltip, | |||||
} = props; | } = props; | ||||
const onHover = () => { | const onHover = () => { | ||||
const label = renderLabel(element); | const label = renderLabel(element); | ||||
return ( | return ( | ||||
<Tooltip overlay={disabled && disableMessage} placement={PopupPlacement.Right}> | |||||
<Tooltip overlay={renderTooltip?.(element, disabled)} placement={PopupPlacement.Right}> | |||||
<ItemCheckbox | <ItemCheckbox | ||||
checked={Boolean(selected)} | checked={Boolean(selected)} | ||||
className={classNames('sw-flex sw-py-2 sw-px-4', { active })} | className={classNames('sw-flex sw-py-2 sw-px-4', { active })} |
}; | }; | ||||
render() { | 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 ( | return ( | ||||
<MultiSelector | <MultiSelector | ||||
createElementLabel={translate('coding_rules.create_tag')} | createElementLabel={translate('coding_rules.create_tag')} | ||||
disableMessage={translate('coding_rules.system_tags_tooltip')} | |||||
headerLabel={translate('tags')} | headerLabel={translate('tags')} | ||||
searchInputAriaLabel={translate('search.search_for_tags')} | searchInputAriaLabel={translate('search.search_for_tags')} | ||||
noResultsLabel={translate('no_results')} | noResultsLabel={translate('no_results')} | ||||
onSelect={this.onSelect} | onSelect={this.onSelect} | ||||
onUnselect={this.onUnselect} | onUnselect={this.onUnselect} | ||||
selectedElements={selectedTags} | selectedElements={selectedTags} | ||||
selectedElementsDisabled={this.props.sysTags} | |||||
selectedElementsDisabled={sysTags} | |||||
elements={availableTags} | elements={availableTags} | ||||
renderTooltip={(element: string, disabled: boolean) => { | |||||
if (sysTags.includes(element) && disabled) { | |||||
return translate('coding_rules.system_tags_tooltip'); | |||||
} | |||||
return null; | |||||
}} | |||||
/> | /> | ||||
); | ); | ||||
} | } |
} from '../../../components/activity-graph/utils'; | } from '../../../components/activity-graph/utils'; | ||||
import { useLocation, useRouter } from '../../../components/hoc/withRouter'; | import { useLocation, useRouter } from '../../../components/hoc/withRouter'; | ||||
import { getBranchLikeQuery } from '../../../helpers/branch-like'; | import { getBranchLikeQuery } from '../../../helpers/branch-like'; | ||||
import { HIDDEN_METRICS } from '../../../helpers/constants'; | |||||
import { parseDate } from '../../../helpers/dates'; | import { parseDate } from '../../../helpers/dates'; | ||||
import useApplicationLeakQuery from '../../../queries/applications'; | import useApplicationLeakQuery from '../../../queries/applications'; | ||||
import { useBranchesQuery } from '../../../queries/branch'; | import { useBranchesQuery } from '../../../queries/branch'; | ||||
}, [appLeaks, component?.leakPeriodDate, component?.qualifier]); | }, [appLeaks, component?.leakPeriodDate, component?.qualifier]); | ||||
const filteredMetrics = React.useMemo(() => { | 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]); | }, [component?.qualifier, metrics]); | ||||
const handleUpdateQuery = (newQuery: Query) => { | const handleUpdateQuery = (newQuery: Query) => { |
import { ButtonSecondary, ChevronDownIcon, Dropdown, TextMuted } from 'design-system'; | import { ButtonSecondary, ChevronDownIcon, Dropdown, TextMuted } from 'design-system'; | ||||
import { sortBy } from 'lodash'; | import { sortBy } from 'lodash'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { HIDDEN_METRICS } from '../../helpers/constants'; | |||||
import { getLocalizedMetricName, translate } from '../../helpers/l10n'; | import { getLocalizedMetricName, translate } from '../../helpers/l10n'; | ||||
import { isDiffMetric } from '../../helpers/measures'; | import { isDiffMetric } from '../../helpers/measures'; | ||||
import { MetricType } from '../../types/metrics'; | |||||
import { MetricKey, MetricType } from '../../types/metrics'; | |||||
import { Metric } from '../../types/types'; | import { Metric } from '../../types/types'; | ||||
import AddGraphMetricPopup from './AddGraphMetricPopup'; | import AddGraphMetricPopup from './AddGraphMetricPopup'; | ||||
) => { | ) => { | ||||
return metrics | return metrics | ||||
.filter((metric) => { | .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 ( | if ( | ||||
metric.hidden || | |||||
isDiffMetric(metric.key) || | |||||
[MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType) || | |||||
selectedMetrics.includes(metric.key) || | selectedMetrics.includes(metric.key) || | ||||
!getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase()) | !getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase()) | ||||
) { | ) { | ||||
onSearch={this.onSearch} | onSearch={this.onSearch} | ||||
onSelect={this.onSelect} | onSelect={this.onSelect} | ||||
onUnselect={this.onUnselect} | onUnselect={this.onUnselect} | ||||
renderLabel={(element) => this.getLocalizedMetricNameFromKey(element)} | |||||
selectedElements={selectedMetrics} | selectedElements={selectedMetrics} | ||||
/> | /> | ||||
} | } |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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 * 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 { | export interface AddGraphMetricPopupProps { | ||||
elements: string[]; | elements: string[]; | ||||
onSelect: (item: string) => void; | onSelect: (item: string) => void; | ||||
onUnselect: (item: string) => void; | onUnselect: (item: string) => void; | ||||
popupPosition?: any; | popupPosition?: any; | ||||
renderLabel: (element: string) => React.ReactNode; | |||||
selectedElements: string[]; | selectedElements: string[]; | ||||
} | } | ||||
metricsTypeFilter, | metricsTypeFilter, | ||||
...props | ...props | ||||
}: AddGraphMetricPopupProps) { | }: AddGraphMetricPopupProps) { | ||||
const intl = useIntl(); | |||||
let footerNode: React.ReactNode = ''; | let footerNode: React.ReactNode = ''; | ||||
if (props.selectedElements.length >= 6) { | if (props.selectedElements.length >= 6) { | ||||
); | ); | ||||
} | } | ||||
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 ( | return ( | ||||
<MultiSelectMenu | <MultiSelectMenu | ||||
createElementLabel="" | createElementLabel="" | ||||
onSelect={(item: string) => elements.includes(item) && props.onSelect(item)} | onSelect={(item: string) => elements.includes(item) && props.onSelect(item)} | ||||
onUnselect={props.onUnselect} | onUnselect={props.onUnselect} | ||||
placeholder={translate('search.search_for_metrics')} | placeholder={translate('search.search_for_metrics')} | ||||
renderLabel={props.renderLabel} | |||||
renderLabel={renderLabel} | |||||
renderTooltip={renderTooltip} | |||||
selectedElements={props.selectedElements} | selectedElements={props.selectedElements} | ||||
listSize={0} | listSize={0} | ||||
/> | /> |
import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import { | import { | ||||
Badge, | |||||
CloseIcon, | CloseIcon, | ||||
FlagWarningIcon, | FlagWarningIcon, | ||||
InteractiveIcon, | InteractiveIcon, | ||||
themeColor, | themeColor, | ||||
} from 'design-system'; | } from 'design-system'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { FormattedMessage, useIntl } from 'react-intl'; | |||||
import { DEPRECATED_ACTIVITY_METRICS } from '../../helpers/constants'; | |||||
import { translateWithParameters } from '../../helpers/l10n'; | import { translateWithParameters } from '../../helpers/l10n'; | ||||
import { MetricKey } from '../../types/metrics'; | |||||
import DocumentationLink from '../common/DocumentationLink'; | |||||
import Tooltip from '../controls/Tooltip'; | |||||
import { ChartLegend } from './ChartLegend'; | import { ChartLegend } from './ChartLegend'; | ||||
interface Props { | interface Props { | ||||
removeMetric, | removeMetric, | ||||
showWarning, | showWarning, | ||||
}: Props) { | }: Props) { | ||||
const intl = useIntl(); | |||||
const theme = useTheme() as Theme; | const theme = useTheme() as Theme; | ||||
const isActionable = removeMetric !== undefined; | const isActionable = removeMetric !== undefined; | ||||
const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(metric as MetricKey); | |||||
return ( | return ( | ||||
<StyledLegendItem | <StyledLegendItem | ||||
<span className="sw-body-sm" style={{ color: themeColor('graphCursorLineColor')({ theme }) }}> | <span className="sw-body-sm" style={{ color: themeColor('graphCursorLineColor')({ theme }) }}> | ||||
{name} | {name} | ||||
</span> | </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 && ( | {isActionable && ( | ||||
<InteractiveIcon | <InteractiveIcon | ||||
Icon={CloseIcon} | Icon={CloseIcon} |
// We should see 2 graphs, correctly labelled. | // We should see 2 graphs, correctly labelled. | ||||
expect(ui.graphs.getAll()).toHaveLength(2); | 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. | // We cannot select anymore Int types. It should hide options, and show an alert. | ||||
expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument(); | expect(ui.vulnerabilityCheckbox.query()).not.toBeInTheDocument(); | ||||
expect(ui.hiddenOptionsAlert.get()).toBeInTheDocument(); | expect(ui.hiddenOptionsAlert.get()).toBeInTheDocument(); | ||||
// Add/remove metrics. | // Add/remove metrics. | ||||
addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }), | addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }), | ||||
deprecatedBadge: byText('deprecated'), | |||||
bugsCheckbox: byRole('checkbox', { name: MetricKey.bugs }), | bugsCheckbox: byRole('checkbox', { name: MetricKey.bugs }), | ||||
newBugsCheckbox: byRole('checkbox', { name: MetricKey.new_bugs }), | newBugsCheckbox: byRole('checkbox', { name: MetricKey.new_bugs }), | ||||
burnedBudgetCheckbox: byRole('checkbox', { name: MetricKey.burned_budget }), | burnedBudgetCheckbox: byRole('checkbox', { name: MetricKey.burned_budget }), |
{ fill: colors.error400, fillTransparent: colors.error400a20, stroke: colors.error700 }, | { 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; | export const PROJECT_KEY_MAX_LEN = 400; | ||||
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.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.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} | |||||
#------------------------------------------------------------------------------ | #------------------------------------------------------------------------------ | ||||
# | # |