aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx54
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx33
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/utils.ts23
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx164
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx44
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx3
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx2
-rw-r--r--server/sonar-web/src/main/js/helpers/activity-graph.ts55
-rw-r--r--server/sonar-web/src/main/js/helpers/constants.ts14
-rw-r--r--server/sonar-web/src/main/js/queries/applications.ts10
-rw-r--r--server/sonar-web/src/main/js/queries/branch.tsx7
-rw-r--r--server/sonar-web/src/main/js/queries/project-analyses.ts11
14 files changed, 208 insertions, 233 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
index 9ab33dd9f2a..1cb21f29650 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
@@ -32,11 +32,11 @@ import {
getHistoryMetrics,
isCustomGraph,
} from '../../../components/activity-graph/utils';
-import { mergeRatingMeasureHistory } from '../../../helpers/activity-graph';
-import { SOFTWARE_QUALITY_RATING_METRICS } from '../../../helpers/constants';
+import { mergeMeasureHistory } from '../../../helpers/activity-graph';
import { parseDate } from '../../../helpers/dates';
-import useApplicationLeakQuery from '../../../queries/applications';
+import { useApplicationLeakQuery } from '../../../queries/applications';
import { useCurrentBranchQuery } from '../../../queries/branch';
+import { StaleTime } from '../../../queries/common';
import { useAllMeasuresHistoryQuery } from '../../../queries/measures';
import { useAllProjectAnalysesQuery } from '../../../queries/project-analyses';
import { useStandardExperienceMode } from '../../../queries/settings';
@@ -59,22 +59,27 @@ export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph';
export function ProjectActivityApp() {
const { query, pathname } = useLocation();
- const parsedQuery = parseQuery(query);
+ const { data: isStandardMode, isLoading: isLoadingStandardMode } = useStandardExperienceMode();
+ const parsedQuery = parseQuery(query, isStandardMode);
const router = useRouter();
const { component } = useComponent();
const metrics = useMetrics();
- const { data: branchLike, isFetching: isFetchingBranch } = useCurrentBranchQuery(component);
+ const { data: branchLike, isFetching: isFetchingBranch } = useCurrentBranchQuery(
+ component,
+ StaleTime.LONG,
+ );
const enabled =
component?.key !== undefined &&
(isPortfolioLike(component?.qualifier) || (Boolean(branchLike) && !isFetchingBranch));
- const { data: appLeaks } = useApplicationLeakQuery(
- component?.key ?? '',
- isApplication(component?.qualifier),
- );
+ const { data: appLeaks } = useApplicationLeakQuery(component?.key ?? '', {
+ enabled: isApplication(component?.qualifier),
+ });
- const { data: analysesData, isLoading: isLoadingAnalyses } = useAllProjectAnalysesQuery(enabled);
- const { data: isStandardMode, isLoading: isLoadingStandardMode } = useStandardExperienceMode();
+ const { data: analysesData, isLoading: isLoadingAnalyses } = useAllProjectAnalysesQuery({
+ enabled,
+ staleTime: StaleTime.LONG,
+ });
const { data: historyData, isLoading: isLoadingHistory } = useAllMeasuresHistoryQuery(
{
@@ -82,16 +87,14 @@ export function ProjectActivityApp() {
branchParams: getBranchLikeQuery(branchLike),
metrics: getHistoryMetrics(query.graph || DEFAULT_GRAPH, parsedQuery.customMetrics).join(','),
},
- { enabled },
+ { enabled, staleTime: StaleTime.LONG },
);
const analyses = React.useMemo(() => analysesData ?? [], [analysesData]);
const measuresHistory = React.useMemo(
() =>
- isLoadingStandardMode
- ? []
- : mergeRatingMeasureHistory(historyData, parseDate, isStandardMode),
+ isLoadingStandardMode ? [] : mergeMeasureHistory(historyData, parseDate, isStandardMode),
[historyData, isStandardMode, isLoadingStandardMode],
);
@@ -122,10 +125,13 @@ export function ProjectActivityApp() {
}, [component?.qualifier, metrics]);
const handleUpdateQuery = (newQuery: Query) => {
- const q = serializeUrlQuery({
- ...parsedQuery,
- ...newQuery,
- });
+ const q = serializeUrlQuery(
+ {
+ ...parsedQuery,
+ ...newQuery,
+ },
+ isStandardMode,
+ );
router.push({
pathname,
@@ -137,20 +143,12 @@ export function ProjectActivityApp() {
});
};
- const firstSoftwareQualityRatingMetric = historyData?.measures.find((m) =>
- SOFTWARE_QUALITY_RATING_METRICS.includes(m.metric),
- );
-
return (
component && (
<Spinner isLoading={isLoadingStandardMode}>
<ProjectActivityAppRenderer
analyses={analyses}
- isStandardMode={
- isStandardMode ||
- !firstSoftwareQualityRatingMetric ||
- firstSoftwareQualityRatingMetric.history.every((h) => h.value === undefined)
- }
+ isStandardMode={isStandardMode}
analysesLoading={isLoadingAnalyses}
graphLoading={isLoadingHistory}
leakPeriodDate={leakPeriodDate}
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 ebf076627bb..10523c0eb92 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
@@ -35,10 +35,6 @@ import {
splitSeriesInGraphs,
} from '../../../components/activity-graph/utils';
import DocumentationLink from '../../../components/common/DocumentationLink';
-import {
- CCT_SOFTWARE_QUALITY_METRICS,
- SOFTWARE_QUALITY_RATING_METRICS_MAP,
-} from '../../../helpers/constants';
import { DocLink } from '../../../helpers/doc-links';
import { translate } from '../../../helpers/l10n';
import { MetricKey } from '../../../sonar-aligned/types/metrics';
@@ -50,6 +46,7 @@ import {
Serie,
} from '../../../types/project-activity';
import { Metric } from '../../../types/types';
+import { MQR_CONDITIONS_MAP } from '../../quality-gates/utils';
import { Query, datesQueryChanged, historyQueryChanged } from '../utils';
import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityApp';
@@ -221,17 +218,11 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
renderQualitiesMetricInfoMessage = () => {
const { measuresHistory, isStandardMode } = this.props;
- const qualityMeasuresHistory = measuresHistory.find((history) =>
- CCT_SOFTWARE_QUALITY_METRICS.includes(history.metric),
- );
- const ratingQualityMeasuresHistory = measuresHistory.find((history) =>
- (Object.keys(SOFTWARE_QUALITY_RATING_METRICS_MAP) as MetricKey[]).includes(history.metric),
+ const mqrMeasuresHistory = measuresHistory.find(
+ (history) => MQR_CONDITIONS_MAP[history.metric as MetricKey] !== undefined,
);
- if (
- this.hasGaps(qualityMeasuresHistory) ||
- (!isStandardMode && this.hasGaps(ratingQualityMeasuresHistory))
- ) {
+ if (!isStandardMode && this.hasGaps(mqrMeasuresHistory)) {
return (
<FlagMessage variant="info">
<FormattedMessage
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 63deee154e3..b74a5f34e96 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
@@ -253,7 +253,17 @@ describe('rendering', () => {
it('should render graph gap info message', async () => {
timeMachineHandler.setMeasureHistory([
mockMeasureHistory({
- metric: MetricKey.maintainability_issues,
+ metric: MetricKey.code_smells,
+ history: projectActivityHandler.getAnalysesList().map(({ date }) =>
+ mockHistoryItem({
+ // eslint-disable-next-line jest/no-conditional-in-test
+ value: '2',
+ date: parseDate(date),
+ }),
+ ),
+ }),
+ mockMeasureHistory({
+ metric: MetricKey.software_quality_maintainability_issues,
history: projectActivityHandler.getAnalysesList().map(({ date }, index) =>
mockHistoryItem({
// eslint-disable-next-line jest/no-conditional-in-test
@@ -274,7 +284,7 @@ describe('rendering', () => {
await ui.changeGraphType(GraphType.custom);
await ui.openMetricsDropdown();
- await ui.toggleMetric(MetricKey.maintainability_issues);
+ await ui.toggleMetric(MetricKey.software_quality_maintainability_issues);
expect(ui.gapInfoMessage.get()).toBeInTheDocument();
});
@@ -290,7 +300,7 @@ describe('rendering', () => {
await ui.changeGraphType(GraphType.custom);
await ui.openMetricsDropdown();
- await ui.toggleMetric(MetricKey.maintainability_issues);
+ await ui.toggleMetric(MetricKey.software_quality_maintainability_issues);
expect(ui.gapInfoMessage.query()).not.toBeInTheDocument();
});
});
@@ -522,7 +532,7 @@ describe('graph interactions', () => {
});
it.each([
- ['MQR', 'true', MetricKey.maintainability_issues],
+ ['MQR', 'true', MetricKey.software_quality_maintainability_issues],
['Standard', 'false', MetricKey.code_smells],
])('should correctly handle customizing the graph in %s mode', async (_, mode, metric) => {
settingsHandler.set(SettingsKey.MQRMode, mode);
@@ -607,7 +617,7 @@ describe('ratings', () => {
await ui.changeGraphType(GraphType.custom);
await ui.openMetricsDropdown();
- await ui.toggleMetric(MetricKey.reliability_rating);
+ await ui.toggleMetric(MetricKey.software_quality_reliability_rating);
await ui.closeMetricsDropdown();
expect(await ui.graphs.findAll()).toHaveLength(1);
@@ -649,7 +659,7 @@ describe('ratings', () => {
await ui.changeGraphType(GraphType.custom);
await ui.openMetricsDropdown();
- await ui.toggleMetric(MetricKey.reliability_rating);
+ await ui.toggleMetric(MetricKey.software_quality_reliability_rating);
await ui.closeMetricsDropdown();
expect(await ui.graphs.findAll()).toHaveLength(1);
@@ -682,7 +692,7 @@ describe('ratings', () => {
await ui.changeGraphType(GraphType.custom);
await ui.openMetricsDropdown();
- await ui.toggleMetric(MetricKey.reliability_rating);
+ await ui.toggleMetric(MetricKey.software_quality_reliability_rating);
await ui.closeMetricsDropdown();
expect(await ui.graphs.findAll()).toHaveLength(1);
@@ -956,12 +966,19 @@ function renderProjectActivityAppContainer(
{
metrics: keyBy(
[
- mockMetric({ key: MetricKey.maintainability_issues, type: MetricType.Data }),
+ mockMetric({
+ key: MetricKey.software_quality_maintainability_issues,
+ type: MetricType.Integer,
+ }),
mockMetric({ key: MetricKey.bugs, type: MetricType.Integer }),
mockMetric({ key: MetricKey.code_smells, type: MetricType.Integer }),
mockMetric({ key: MetricKey.security_hotspots_reviewed }),
mockMetric({ key: MetricKey.security_review_rating, type: MetricType.Rating }),
mockMetric({ key: MetricKey.reliability_rating, type: MetricType.Rating }),
+ mockMetric({
+ key: MetricKey.software_quality_reliability_rating,
+ type: MetricType.Rating,
+ }),
],
'key',
),
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts
index a93df4ef785..9ec0145af44 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts
+++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts
@@ -23,7 +23,6 @@ import { isEqual, uniq } from 'lodash';
import { MetricKey } from '~sonar-aligned/types/metrics';
import { RawQuery } from '~sonar-aligned/types/router';
import { DEFAULT_GRAPH } from '../../components/activity-graph/utils';
-import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from '../../helpers/constants';
import { parseDate } from '../../helpers/dates';
import { MEASURES_REDIRECTION } from '../../helpers/measures';
import {
@@ -37,6 +36,7 @@ import {
} from '../../helpers/query';
import { GraphType, ParsedAnalysis } from '../../types/project-activity';
import { Dict } from '../../types/types';
+import { MQR_CONDITIONS_MAP, STANDARD_CONDITIONS_MAP } from '../quality-gates/utils';
export interface Query {
category: string;
@@ -113,21 +113,17 @@ export function getAnalysesByVersionByDay(
}, []);
}
-export function parseQuery(urlQuery: RawQuery): Query {
+export function parseQuery(urlQuery: RawQuery, isStandardMode = false): Query {
const parsedMetrics = parseAsArray(urlQuery['custom_metrics'], parseAsString<MetricKey>);
let customMetrics = uniq(parsedMetrics.map((metric) => MEASURES_REDIRECTION[metric] ?? metric));
- const reversedMetricMap = Object.fromEntries(
- Object.entries(SOFTWARE_QUALITY_RATING_METRICS_MAP).map(
- ([k, v]) => [v, k] as [MetricKey, MetricKey],
+ customMetrics = uniq(
+ customMetrics.map((metric) =>
+ !isStandardMode ? (STANDARD_CONDITIONS_MAP[metric] ?? metric) : metric,
),
- );
-
- customMetrics = uniq(customMetrics.map((metric) => reversedMetricMap[metric] ?? metric))
+ )
.map((metric) =>
- SOFTWARE_QUALITY_RATING_METRICS_MAP[metric]
- ? [metric, SOFTWARE_QUALITY_RATING_METRICS_MAP[metric]]
- : metric,
+ !isStandardMode && MQR_CONDITIONS_MAP[metric] ? [metric, MQR_CONDITIONS_MAP[metric]] : metric,
)
.flat();
@@ -149,13 +145,12 @@ export function serializeQuery(query: Query): RawQuery {
});
}
-export function serializeUrlQuery(query: Query): RawQuery {
+export function serializeUrlQuery(query: Query, isStandardMode = false): RawQuery {
return cleanQuery({
category: serializeString(query.category),
custom_metrics: serializeStringArray(
query.customMetrics.filter(
- (metric) =>
- !Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric as MetricKey),
+ (metric) => isStandardMode || !STANDARD_CONDITIONS_MAP[metric as MetricKey],
),
),
from: serializeDate(query.from),
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 1b4ef643205..ad331f5cfcf 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
@@ -23,24 +23,15 @@ import { sortBy } from 'lodash';
import * as React from 'react';
import { Dropdown } from '~design-system';
import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
-import {
- CCT_SOFTWARE_QUALITY_METRICS,
- DEPRECATED_ACTIVITY_METRICS,
- HIDDEN_METRICS,
- LEAK_CCT_SOFTWARE_QUALITY_METRICS,
- LEAK_OLD_TAXONOMY_METRICS,
- LEAK_OLD_TAXONOMY_RATINGS,
- OLD_TAXONOMY_METRICS,
- SOFTWARE_QUALITY_RATING_METRICS,
- SOFTWARE_QUALITY_RATING_METRICS_MAP,
-} from '../../helpers/constants';
+import { MQR_CONDITIONS_MAP, STANDARD_CONDITIONS_MAP } from '../../apps/quality-gates/utils';
+import { HIDDEN_METRICS } from '../../helpers/constants';
import { getLocalizedMetricName, translate } from '../../helpers/l10n';
import { isDiffMetric } from '../../helpers/measures';
+import { useStandardExperienceMode } from '../../queries/settings';
import { Metric } from '../../types/types';
import AddGraphMetricPopup from './AddGraphMetricPopup';
interface Props {
- isStandardMode?: boolean;
metrics: Metric[];
metricsTypeFilter?: string[];
onAddMetric: (metric: string) => void;
@@ -48,27 +39,22 @@ interface Props {
selectedMetrics: string[];
}
-interface State {
- metrics: string[];
- query: string;
- selectedMetrics: string[];
-}
+export default function AddGraphMetric(props: Readonly<Props>) {
+ const [metrics, setMetrics] = React.useState<string[]>([]);
+ const [query, setQuery] = React.useState<string>('');
+ const [selectedMetrics, setSelectedMetrics] = React.useState<string[]>([]);
-export default class AddGraphMetric extends React.PureComponent<Props, State> {
- state: State = {
- metrics: [],
- query: '',
- selectedMetrics: [],
- };
+ const { data: isStandardMode } = useStandardExperienceMode();
- filterSelected = (query: string, selectedElements: string[]) => {
+ const filterSelected = (query: string, selectedElements: string[]) => {
return selectedElements.filter((element) =>
- this.getLocalizedMetricNameFromKey(element).toLowerCase().includes(query.toLowerCase()),
+ getLocalizedMetricNameFromKey(element).toLowerCase().includes(query.toLowerCase()),
);
};
- filterMetricsElements = (query: string) => {
- const { metrics, selectedMetrics, metricsTypeFilter, isStandardMode } = this.props;
+ const filterMetricsElements = () => {
+ const { metricsTypeFilter, metrics, selectedMetrics } = props;
+
return metrics
.filter((metric) => {
if (metric.hidden) {
@@ -77,40 +63,20 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
if (isDiffMetric(metric.key)) {
return false;
}
- if (
- [MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType) &&
- !CCT_SOFTWARE_QUALITY_METRICS.includes(metric.key as MetricKey)
- ) {
+ if ([MetricType.Data, MetricType.Distribution].includes(metric.type as MetricType)) {
return false;
}
if (HIDDEN_METRICS.includes(metric.key as MetricKey)) {
return false;
}
if (
- isStandardMode &&
- [
- ...LEAK_CCT_SOFTWARE_QUALITY_METRICS,
- ...CCT_SOFTWARE_QUALITY_METRICS,
- ...SOFTWARE_QUALITY_RATING_METRICS,
- ].includes(metric.key as MetricKey)
+ isStandardMode
+ ? MQR_CONDITIONS_MAP[metric.key as MetricKey]
+ : STANDARD_CONDITIONS_MAP[metric.key as MetricKey]
) {
return false;
}
if (
- !isStandardMode &&
- [
- ...LEAK_OLD_TAXONOMY_METRICS,
- ...OLD_TAXONOMY_METRICS,
- ...LEAK_OLD_TAXONOMY_RATINGS,
- ...DEPRECATED_ACTIVITY_METRICS,
- ].includes(metric.key as MetricKey)
- ) {
- return false;
- }
- if (Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric.key as MetricKey)) {
- return false;
- }
- if (
selectedMetrics.includes(metric.key) ||
!getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase())
) {
@@ -124,78 +90,64 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
.map((metric) => metric.key);
};
- getSelectedMetricsElements = (metrics: Metric[], selectedMetrics: string[]) => {
+ const getSelectedMetricsElements = (metrics: Metric[], selectedMetrics: string[]) => {
return metrics
.filter(
(metric) =>
selectedMetrics.includes(metric.key) &&
- !Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric.key as MetricKey),
+ (isStandardMode || !STANDARD_CONDITIONS_MAP[metric.key as MetricKey]),
)
.map((metric) => metric.key);
};
- getLocalizedMetricNameFromKey = (key: string) => {
- const metric = this.props.metrics.find((m) => m.key === key);
+ const getLocalizedMetricNameFromKey = (key: string) => {
+ const metric = props.metrics.find((m) => m.key === key);
return metric === undefined ? key : getLocalizedMetricName(metric);
};
- onSearch = (query: string) => {
- this.setState({ query });
+ const onSearch = (query: string) => {
+ setQuery(query);
return Promise.resolve();
};
- onSelect = (metric: string) => {
- this.props.onAddMetric(metric);
- this.setState((state) => {
- return {
- selectedMetrics: sortBy([...state.selectedMetrics, metric]),
- metrics: this.filterMetricsElements(state.query),
- };
- });
+ const onSelect = (metric: string) => {
+ props.onAddMetric(metric);
+ setSelectedMetrics(sortBy([...selectedMetrics, metric]));
+ setMetrics(filterMetricsElements());
};
- onUnselect = (metric: string) => {
- this.props.onRemoveMetric(metric);
- this.setState((state) => {
- return {
- metrics: sortBy([...state.metrics, metric]),
- selectedMetrics: state.selectedMetrics.filter((selected) => selected !== metric),
- };
- });
+ const onUnselect = (metric: string) => {
+ props.onRemoveMetric(metric);
+ setSelectedMetrics(selectedMetrics.filter((selected) => selected !== metric));
+ setMetrics(sortBy(metrics, metric));
};
- render() {
- const { query } = this.state;
- const filteredMetrics = this.filterMetricsElements(query);
- const selectedMetrics = this.getSelectedMetricsElements(
- this.props.metrics,
- this.props.selectedMetrics,
- );
+ const filteredMetrics = filterMetricsElements();
+ const selectedElements = getSelectedMetricsElements(props.metrics, props.selectedMetrics);
- return (
- <Dropdown
- allowResizing
- size="large"
- closeOnClick={false}
- id="activity-graph-custom-metric-selector"
- overlay={
- <AddGraphMetricPopup
- elements={filteredMetrics}
- filterSelected={this.filterSelected}
- metricsTypeFilter={this.props.metricsTypeFilter}
- onSearch={this.onSearch}
- onSelect={this.onSelect}
- onUnselect={this.onUnselect}
- selectedElements={selectedMetrics}
- />
- }
- >
- <Button suffix={<IconChevronDown />}>
- <span className="sw-typo-default sw-flex">
- {translate('project_activity.graphs.custom.add')}
- </span>
- </Button>
- </Dropdown>
- );
- }
+ return (
+ <Dropdown
+ allowResizing
+ size="large"
+ closeOnClick={false}
+ id="activity-graph-custom-metric-selector"
+ overlay={
+ <AddGraphMetricPopup
+ elements={filteredMetrics}
+ filterSelected={filterSelected}
+ metricsTypeFilter={props.metricsTypeFilter}
+ onSearch={onSearch}
+ onSelect={onSelect}
+ onUnselect={onUnselect}
+ selectedElements={selectedElements}
+ />
+ }
+ >
+ <Button suffix={<IconChevronDown />}>
+ <span className="sw-typo-default sw-flex">
+ {translate('project_activity.graphs.custom.add')}
+ </span>
+ </Button>
+ </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 07d00062654..5cd379f1e2e 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
@@ -18,10 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ReactNode, useCallback } from 'react';
-import { FlagMessage, MultiSelectMenu } from '~design-system';
+import { ReactNode } from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { Badge, FlagMessage, MultiSelectMenu } from '~design-system';
import { MetricKey } from '~sonar-aligned/types/metrics';
+import { DEPRECATED_ACTIVITY_METRICS } from '../../helpers/constants';
import { getLocalizedMetricName, translate, translateWithParameters } from '../../helpers/l10n';
+import { getDeprecatedTranslationKeyForTooltip } from './utils';
export interface AddGraphMetricPopupProps {
elements: string[];
@@ -39,6 +42,7 @@ export default function AddGraphMetricPopup({
metricsTypeFilter,
...props
}: Readonly<AddGraphMetricPopupProps>) {
+ const intl = useIntl();
let footerNode: ReactNode = '';
if (props.selectedElements.length >= 6) {
@@ -61,10 +65,37 @@ export default function AddGraphMetricPopup({
);
}
- const renderLabel = useCallback((key: MetricKey) => {
- return getLocalizedMetricName({ key });
- }, []);
+ const renderAriaLabel = (key: MetricKey) => {
+ const metricName = getLocalizedMetricName({ key });
+ const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key);
+ return isDeprecated
+ ? `${metricName} (${intl.formatMessage({ id: 'deprecated' })})`
+ : metricName;
+ };
+
+ const renderLabel = (key: MetricKey) => {
+ const metricName = getLocalizedMetricName({ key });
+ const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key);
+
+ return (
+ <>
+ {metricName}
+ {isDeprecated && (
+ <Badge className="sw-ml-1">{intl.formatMessage({ id: 'deprecated' })}</Badge>
+ )}
+ </>
+ );
+ };
+
+ const renderTooltip = (key: MetricKey) => {
+ const isDeprecated = DEPRECATED_ACTIVITY_METRICS.includes(key);
+ if (isDeprecated) {
+ return <FormattedMessage id={getDeprecatedTranslationKeyForTooltip(key)} tagName="div" />;
+ }
+
+ return null;
+ };
return (
<MultiSelectMenu
createElementLabel=""
@@ -79,7 +110,8 @@ export default function AddGraphMetricPopup({
onSelect={(item: string) => elements.includes(item) && props.onSelect(item)}
onUnselect={props.onUnselect}
placeholder={translate('search.search_for_metrics')}
- renderAriaLabel={renderLabel}
+ renderAriaLabel={renderAriaLabel}
+ renderTooltip={renderTooltip}
renderLabel={renderLabel}
selectedElements={props.selectedElements}
listSize={0}
diff --git a/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx b/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx
index 2604c4f5725..050fac489ad 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/EventInner.tsx
@@ -23,6 +23,7 @@ import { Note } from '~design-system';
import { ComponentContext } from '../../app/components/componentContext/ComponentContext';
import { translate } from '../../helpers/l10n';
import { useCurrentBranchQuery } from '../../queries/branch';
+import { StaleTime } from '../../queries/common';
import { AnalysisEvent, ProjectAnalysisEventCategory } from '../../types/project-activity';
import Tooltip from '../controls/Tooltip';
import { DefinitionChangeEventInner, isDefinitionChangeEvent } from './DefinitionChangeEventInner';
@@ -40,7 +41,7 @@ export interface EventInnerProps {
export default function EventInner({ event, readonly }: EventInnerProps) {
const { component } = React.useContext(ComponentContext);
- const { data: branchLike } = useCurrentBranchQuery(component);
+ const { data: branchLike } = useCurrentBranchQuery(component, StaleTime.LONG);
if (isRichQualityGateEvent(event)) {
return <RichQualityGateEventInner event={event} readonly={readonly} />;
} else if (isDefinitionChangeEvent(event)) {
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 88785de65ce..35d1150b9cd 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
@@ -21,7 +21,6 @@
import { ButtonGroup, InputSize, Select } from '@sonarsource/echoes-react';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
-import { useStandardExperienceMode } from '../../queries/settings';
import { GraphType } from '../../types/project-activity';
import { Metric } from '../../types/types';
import AddGraphMetric from './AddGraphMetric';
@@ -48,8 +47,6 @@ export default function GraphsHeader(props: Readonly<Props>) {
selectedMetrics = [],
} = props;
- const { data: isStandardMode } = useStandardExperienceMode();
-
const handleGraphChange = React.useCallback(
(value: GraphType) => {
if (value !== graph) {
@@ -90,7 +87,6 @@ export default function GraphsHeader(props: Readonly<Props>) {
metricsTypeFilter={metricsTypeFilter}
onRemoveMetric={props.onRemoveCustomMetric}
selectedMetrics={selectedMetrics}
- isStandardMode={isStandardMode}
/>
)}
</ButtonGroup>
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 dbd3729e914..d9ae9ae663b 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
@@ -50,7 +50,7 @@ export function GraphsLegendItem({
isActionable={isActionable}
>
{showWarning ? (
- <IconWarning className="sw-mr-2" />
+ <IconWarning color="echoes-color-icon-warning" className="sw-mr-2" />
) : (
<ChartLegend className="sw-mr-2" index={index} />
)}
diff --git a/server/sonar-web/src/main/js/helpers/activity-graph.ts b/server/sonar-web/src/main/js/helpers/activity-graph.ts
index d2273460103..ea0ccc4dd03 100644
--- a/server/sonar-web/src/main/js/helpers/activity-graph.ts
+++ b/server/sonar-web/src/main/js/helpers/activity-graph.ts
@@ -20,39 +20,36 @@
import { ScaleTime } from 'd3-scale';
import { TimeMachineResponse } from '../api/time-machine';
-import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from './constants';
+import { MQR_CONDITIONS_MAP, STANDARD_CONDITIONS_MAP } from '../apps/quality-gates/utils';
-export const mergeRatingMeasureHistory = (
+export const mergeMeasureHistory = (
historyData: TimeMachineResponse | undefined,
parseDateFn: (date: string) => Date,
isStandardMode = false,
) => {
- const softwareQualityMeasures = Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP);
- const softwareQualityMeasuresMap = new Map<
+ const standardMeasuresMap = new Map<
string,
{ history: { date: string; value?: string }[]; index: number; splitDate?: Date }
>();
if (isStandardMode) {
return (
- historyData?.measures
- ?.filter((m) => !softwareQualityMeasures.includes(m.metric))
- .map((measure) => ({
- metric: measure.metric,
- history: measure.history.map((historyItem) => ({
- date: parseDateFn(historyItem.date),
- value: historyItem.value,
- })),
- })) ?? []
+ historyData?.measures.map((measure) => ({
+ metric: measure.metric,
+ history: measure.history.map((historyItem) => ({
+ date: parseDateFn(historyItem.date),
+ value: historyItem.value,
+ })),
+ })) ?? []
);
}
const historyDataFiltered =
historyData?.measures?.filter((measure) => {
- if (softwareQualityMeasures.includes(measure.metric)) {
+ if (MQR_CONDITIONS_MAP[measure.metric]) {
const splitPointIndex = measure.history.findIndex(
(historyItem) => historyItem.value != null,
);
- softwareQualityMeasuresMap.set(measure.metric, {
+ standardMeasuresMap.set(measure.metric, {
history: measure.history,
index: measure.history.findIndex((historyItem) => historyItem.value != null),
splitDate:
@@ -72,20 +69,24 @@ export const mergeRatingMeasureHistory = (
});
return historyDataFiltered.map((measure) => {
- const softwareQualityMetric = softwareQualityMeasuresMap.get(
- SOFTWARE_QUALITY_RATING_METRICS_MAP[measure.metric],
- );
+ const metric = STANDARD_CONDITIONS_MAP[measure.metric];
+ const softwareQualityMetric = standardMeasuresMap.get(metric as string);
+ if (softwareQualityMetric !== undefined && metric) {
+ return {
+ metric,
+ splitPointDate: softwareQualityMetric.splitDate,
+ history: measure.history
+ .slice(0, softwareQualityMetric.index)
+ .map(historyMapper)
+ .concat(
+ softwareQualityMetric.history.slice(softwareQualityMetric.index).map(historyMapper),
+ ),
+ };
+ }
+
return {
metric: measure.metric,
- splitPointDate: softwareQualityMetric ? softwareQualityMetric.splitDate : undefined,
- history: softwareQualityMetric
- ? measure.history
- .slice(0, softwareQualityMetric.index)
- .map(historyMapper)
- .concat(
- softwareQualityMetric.history.slice(softwareQualityMetric.index).map(historyMapper),
- )
- : measure.history.map(historyMapper),
+ history: measure.history.map(historyMapper),
};
});
};
diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts
index cf169fbcf94..2afd6152403 100644
--- a/server/sonar-web/src/main/js/helpers/constants.ts
+++ b/server/sonar-web/src/main/js/helpers/constants.ts
@@ -199,14 +199,7 @@ export const HIDDEN_METRICS = [
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.confirmed_issues,
-];
+export const DEPRECATED_ACTIVITY_METRICS = [MetricKey.confirmed_issues];
export const SOFTWARE_QUALITY_RATING_METRICS_MAP: Record<string, MetricKey> = {
[MetricKey.sqale_rating]: MetricKey.software_quality_maintainability_rating,
@@ -267,11 +260,6 @@ export const SOFTWARE_QUALITY_RATING_METRICS = [
MetricKey.new_software_quality_reliability_remediation_effort,
MetricKey.new_software_quality_security_remediation_effort,
MetricKey.new_software_quality_maintainability_debt_ratio,
- MetricKey.software_quality_blocker_issues,
- MetricKey.software_quality_high_issues,
- MetricKey.software_quality_medium_issues,
- MetricKey.software_quality_low_issues,
- MetricKey.software_quality_info_issues,
];
export const PROJECT_KEY_MAX_LEN = 400;
diff --git a/server/sonar-web/src/main/js/queries/applications.ts b/server/sonar-web/src/main/js/queries/applications.ts
index c08095b30e2..13b20809900 100644
--- a/server/sonar-web/src/main/js/queries/applications.ts
+++ b/server/sonar-web/src/main/js/queries/applications.ts
@@ -18,17 +18,17 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query';
import { deleteApplication, getApplicationLeak } from '../api/application';
+import { createQueryHook } from './common';
import { invalidateMeasuresByComponentKey } from './measures';
-export default function useApplicationLeakQuery(application: string, enabled = true) {
- return useQuery({
+export const useApplicationLeakQuery = createQueryHook((application: string) => {
+ return queryOptions({
queryKey: ['application', 'leak', application],
queryFn: () => getApplicationLeak(application),
- enabled,
});
-}
+});
export function useDeleteApplicationMutation() {
const queryClient = useQueryClient();
diff --git a/server/sonar-web/src/main/js/queries/branch.tsx b/server/sonar-web/src/main/js/queries/branch.tsx
index ada7254d613..8acc97b5e68 100644
--- a/server/sonar-web/src/main/js/queries/branch.tsx
+++ b/server/sonar-web/src/main/js/queries/branch.tsx
@@ -168,7 +168,10 @@ export function useBranchesQuery(component: LightComponent | undefined) {
});
}
-export function useCurrentBranchQuery(component: LightComponent | undefined) {
+export function useCurrentBranchQuery(
+ component: LightComponent | undefined,
+ staleTime = StaleTime.LIVE,
+) {
const features = useContext(AvailableFeaturesContext);
const { search } = useLocation();
@@ -196,7 +199,7 @@ export function useCurrentBranchQuery(component: LightComponent | undefined) {
return useQuery({
...branchesQuery(component, features.includes(Feature.BranchSupport)),
select,
- staleTime: StaleTime.LIVE,
+ staleTime,
});
}
diff --git a/server/sonar-web/src/main/js/queries/project-analyses.ts b/server/sonar-web/src/main/js/queries/project-analyses.ts
index ee6fe2a0e73..c7edf3eb132 100644
--- a/server/sonar-web/src/main/js/queries/project-analyses.ts
+++ b/server/sonar-web/src/main/js/queries/project-analyses.ts
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { BranchParameters } from '~sonar-aligned/types/branch-like';
import {
@@ -38,6 +38,7 @@ import { parseDate } from '../helpers/dates';
import { serializeStringArray } from '../helpers/query';
import { ParsedAnalysis } from '../types/project-activity';
import { useCurrentBranchQuery } from './branch';
+import { createQueryHook } from './common';
const ACTIVITY_PAGE_SIZE = 500;
@@ -55,9 +56,10 @@ function useProjectActivityQueryKey() {
];
}
-export function useAllProjectAnalysesQuery(enabled = true) {
+export const useAllProjectAnalysesQuery = createQueryHook(() => {
const queryKey = useProjectActivityQueryKey();
- return useQuery({
+
+ return queryOptions({
queryKey,
queryFn: ({ queryKey: [_0, _1, project, branchParams] }) =>
getAllTimeProjectActivity({
@@ -75,9 +77,8 @@ export function useAllProjectAnalysesQuery(enabled = true) {
date: parseDate(analysis.date),
})),
),
- enabled,
});
-}
+});
export function useDeleteAnalysisMutation(successCb?: () => void) {
const queryClient = useQueryClient();