diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index 800424b79b22adc26138a1213d4c80cab41e2a8b..254e181a9e8c191e6207ff8fe12690532fa57b21 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -166,6 +166,18 @@ export const VULNERABILITY_METRICS = {
HIGH: VULNERABILITY_HIGH_TYPE,
};
+export const PIPELINE_ANALYTICS_TYPE_COUNT = 'count';
+export const PIPELINE_ANALYTICS_TYPE_SUCCESS_RATE = 'success_rate';
+export const PIPELINE_ANALYTICS_TYPE_FAILED_RATE = 'failed_rate';
+export const PIPELINE_ANALYTICS_TYPE_MEDIAN = 'median';
+
+export const PIPELINE_ANALYTICS_METRICS = {
+ COUNT: PIPELINE_ANALYTICS_TYPE_COUNT,
+ SUCCESS_RATE: PIPELINE_ANALYTICS_TYPE_SUCCESS_RATE,
+ FAILED_RATE: PIPELINE_ANALYTICS_TYPE_FAILED_RATE,
+ MEDIAN: PIPELINE_ANALYTICS_TYPE_MEDIAN,
+};
+
export const MERGE_REQUEST_THROUGHPUT_TYPE = 'merge_request_throughput';
export const MERGE_REQUEST_METRICS = {
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator.stories.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..8d6631bd634443364db97fb7ac3b543ed6c92dc5
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator.stories.js
@@ -0,0 +1,42 @@
+import DataTable from './data_table.vue';
+import ChangePercentageIndicator from './change_percentage_indicator.vue';
+
+export default {
+ component: ChangePercentageIndicator,
+ title:
+ 'ee/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ChangePercentageIndicator },
+ props: Object.keys(argTypes),
+ template: ``,
+});
+
+const TableTemplate = (args, { argTypes }) => ({
+ components: { DataTable },
+ props: Object.keys(argTypes),
+ template: ``,
+});
+
+const tooltipLabel = 'Tooltip label is cool';
+
+export const Default = Template.bind({});
+Default.args = { data: 0.25, tooltipLabel, invertTrendColor: false };
+
+export const NegativeChange = Template.bind({});
+NegativeChange.args = { data: -0.125, tooltipLabel, invertTrendColor: false };
+
+export const NoChange = Template.bind({});
+NoChange.args = { tooltipLabel, invertTrendColor: false, data: 0 };
+
+export const InTable = TableTemplate.bind({});
+InTable.args = {
+ data: [{ change: { data: 0.15, tooltipLabel }, metric: 'Vulnerabilities' }],
+ options: {
+ fields: [
+ { key: 'metric', label: 'Title' },
+ { key: 'change', label: 'Change', component: 'ChangePercentageIndicator' },
+ ],
+ },
+};
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator.vue
new file mode 100644
index 0000000000000000000000000000000000000000..2504abbc1be776c896625de89b81d5d46661eab2
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+ {{ formatInvalidTrend(value) }}
+
+
+
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/data_table.stories.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/data_table.stories.js
index e93b31faee2f54864dd9531dea1aeb748120a813..26748cae7327c00bdc4e28ed46d3abe5b06a2f4e 100644
--- a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/data_table.stories.js
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/data_table.stories.js
@@ -22,38 +22,49 @@ const WithGridstack = (args, { argTypes }) => ({
`,
});
-const data = {
- nodes: [
- {
- title: 'MR 0',
- additions: 1,
- deletions: 0,
- commitCount: 1,
- userNotesCount: 1,
+const data = [
+ {
+ title: 'MR 0',
+ additions: 1,
+ deletions: 0,
+ commitCount: 1,
+ userNotesCount: 1,
+ change: {
+ data: 0,
},
- {
- title: 'MR 1',
- additions: 1,
- deletions: 0,
- commitCount: 1,
- userNotesCount: 1,
+ },
+ {
+ title: 'MR 1',
+ additions: 1,
+ deletions: 0,
+ commitCount: 1,
+ userNotesCount: 1,
+ change: {
+ data: -0.4,
},
- {
- title: 'MR 2',
- additions: 4,
- deletions: 3,
- commitCount: 10,
- userNotesCount: 1,
+ },
+ {
+ title: 'MR 2',
+ additions: 4,
+ deletions: 3,
+ commitCount: 10,
+ userNotesCount: 1,
+ change: {
+ data: 0.12,
},
- {
- title: 'MR 3',
- additions: 20,
- deletions: 4,
- commitCount: 40,
- userNotesCount: 1,
+ },
+ {
+ title: 'MR 3',
+ additions: 20,
+ deletions: 4,
+ commitCount: 40,
+ userNotesCount: 1,
+ change: {
+ data: 0.14,
+ invertTrendColor: true,
},
- ],
-};
+ },
+];
const defaultArgs = { data };
@@ -98,30 +109,30 @@ CustomFields.parameters = {
},
};
CustomFields.args = {
- data: {
- nodes: data.nodes.map(({ title, additions, deletions }) => ({
- title,
- assignees: {
- nodes: [
- {
- name: 'Administrator',
- webUrl: 'https://gitlab.com',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- },
- ],
- },
- changes: {
- additions,
- deletions,
- },
- })),
- },
+ data: data.map(({ title, additions, deletions, change }) => ({
+ title,
+ assignees: {
+ nodes: [
+ {
+ name: 'Administrator',
+ webUrl: 'https://gitlab.com',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ ],
+ },
+ changes: {
+ additions,
+ deletions,
+ },
+ change,
+ })),
options: {
fields: [
{ key: 'title' },
{ key: 'assignees', label: 'Assignees', component: 'AssigneeAvatars' },
{ key: 'changes', label: 'Diff', component: 'DiffLineChanges' },
+ { key: 'change', label: '+/- %', component: 'ChangePercentageIndicator' },
],
},
};
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/data_table.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/data_table.vue
index 7c2f910051eea715a33b08ffa47331672166cb7b..0eaaf610aa2771624bb665c89728c5aa3a8d8078 100644
--- a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/data_table.vue
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/data_table.vue
@@ -16,6 +16,11 @@ export default {
AssigneeAvatars: () => import('./assignee_avatars.vue'),
DiffLineChanges: () => import('./diff_line_changes.vue'),
MergeRequestLink: () => import('./merge_request_link.vue'),
+ ChangePercentageIndicator: () => import('./change_percentage_indicator.vue'),
+ // TODO: give this a better name
+ MetricTableCell: () => import('./metric_table_cell.vue'),
+ SimpleValue: () => import('./simple_value.vue'),
+ TrendLine: () => import('./trend_line.vue'),
},
props: {
data: {
@@ -90,7 +95,8 @@ export default {
-
+
+
{{ formatVisualizationValue(value.text) }}
+
{{ formatVisualizationValue(value) }}
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/metric_table_cell.stories.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/metric_table_cell.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..2d53a26c3d6ab7e185c453ac10b382810fe23062
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/metric_table_cell.stories.js
@@ -0,0 +1,35 @@
+import DataTable from './data_table.vue';
+import MetricTableCell from './metric_table_cell.vue';
+
+export default {
+ component: MetricTableCell,
+ title: 'ee/analytics/analytics_dashboards/components/visualizations/data_table/metric_table_cell',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { MetricTableCell },
+ props: Object.keys(argTypes),
+ template: ``,
+});
+
+const TableTemplate = (args, { argTypes }) => ({
+ components: { DataTable },
+ props: Object.keys(argTypes),
+ template: ``,
+});
+
+const metric = { identifier: 'cycle_time', requestPath: 'some/path/for/request', isProject: false };
+
+export const Default = Template.bind({});
+Default.args = { ...metric };
+
+export const InTable = TableTemplate.bind({});
+InTable.args = {
+ data: [{ metric, value: '12.5/d' }],
+ options: {
+ fields: [
+ { key: 'metric', label: 'Metric', component: 'MetricTableCell' },
+ { key: 'value', label: 'Current value' },
+ ],
+ },
+};
diff --git a/ee/app/assets/javascripts/analytics/dashboards/components/metric_table_cell.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/metric_table_cell.vue
similarity index 66%
rename from ee/app/assets/javascripts/analytics/dashboards/components/metric_table_cell.vue
rename to ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/metric_table_cell.vue
index 6f8b88a72d4aed7d02957dbdfb285854702602d2..d0720950ae63e90664e5a6d86f9e97a536a18926 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/components/metric_table_cell.vue
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/metric_table_cell.vue
@@ -6,8 +6,8 @@ import { joinPaths, mergeUrlParams } from '~/lib/utils/url_utility';
import { VALUE_STREAM_METRIC_METADATA, DORA_METRICS } from '~/analytics/shared/constants';
import { s__ } from '~/locale';
import { EVENT_LABEL_CLICK_METRIC_IN_DASHBOARD_TABLE } from 'ee/analytics/analytics_dashboards/constants';
-import { TABLE_METRICS } from '../constants';
-import { AI_IMPACT_TABLE_METRICS } from '../ai_impact/constants';
+import { PIPELINE_ANALYTICS_TABLE_METRICS, TABLE_METRICS } from '../../../../dashboards/constants';
+import { AI_IMPACT_TABLE_METRICS } from '../../../../dashboards/ai_impact/constants';
export default {
name: 'MetricTableCell',
@@ -22,13 +22,16 @@ export default {
type: String,
required: true,
},
+ // TODO: request path and is project are only used to construct the link, parametize link instead and compute this stuff earlier
requestPath: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
isProject: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
filterLabels: {
type: Array,
@@ -43,7 +46,15 @@ export default {
},
computed: {
metric() {
- return TABLE_METRICS[this.identifier] || AI_IMPACT_TABLE_METRICS[this.identifier];
+ // TODO: we should supply the full metric object, not just the identifer
+ // if (!TABLE_METRICS[this.identifier] || !AI_IMPACT_TABLE_METRICS[this.identifier]) {
+ // console.log('MetricTableCell::FAILED', this.identifier);
+ // }
+ return (
+ TABLE_METRICS[this.identifier] ||
+ AI_IMPACT_TABLE_METRICS[this.identifier] ||
+ PIPELINE_ANALYTICS_TABLE_METRICS[this.identifier]
+ );
},
isDoraMetric() {
return Object.values(DORA_METRICS).includes(this.identifier);
@@ -105,18 +116,20 @@ export default {
>{{ metric.label }}
{{ metric.label }}
-
-
- {{ tooltip.description }}
-
- {{ $options.i18n.docsLabel }}
-
-
-
+
+
+
+ {{ tooltip.description }}
+
+ {{ $options.i18n.docsLabel }}
+
+
+
+
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/simple_value.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/simple_value.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6ed71415b2f8dced260682bfa841428d959c56fb
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/simple_value.vue
@@ -0,0 +1,21 @@
+
+
+
+ {{ formatVisualizationValue(value) }}
+
+
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/trend_line.stories.js b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/trend_line.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..5828686c512f5767fa30a288b8759202e5c1a39b
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/trend_line.stories.js
@@ -0,0 +1,50 @@
+import DataTable from './data_table.vue';
+import TrendLine from './trend_line.vue';
+
+export default {
+ component: TrendLine,
+ title: 'ee/analytics/analytics_dashboards/components/visualizations/data_table/trend_line',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { TrendLine },
+ props: Object.keys(argTypes),
+ template: ``,
+});
+
+const TableTemplate = (args, { argTypes }) => ({
+ components: { DataTable },
+ props: Object.keys(argTypes),
+ template: ``,
+});
+
+const data = [
+ ['Jan', 20],
+ ['Feb', 5],
+ ['Mar', 4],
+ ['Apr', 11],
+ ['May', 13],
+ ['Jun', 21],
+];
+
+const tooltipLabel = 'Tooltip label is cool';
+
+export const Default = Template.bind({});
+Default.args = { data, tooltipLabel, invertTrendColor: false };
+
+export const WithInvertTrendColor = Template.bind({});
+WithInvertTrendColor.args = { data, tooltipLabel, invertTrendColor: true };
+
+export const IsLoading = Template.bind({});
+IsLoading.args = { tooltipLabel, invertTrendColor: false, data: [] };
+
+export const InTable = TableTemplate.bind({});
+InTable.args = {
+ data: [{ trend: { data, tooltipLabel, invertTrendColor: false }, metric: 'Vulnerabilities' }],
+ options: {
+ fields: [
+ { key: 'metric', label: 'Title' },
+ { key: 'trend', label: 'Trend', component: 'TrendLine' },
+ ],
+ },
+};
diff --git a/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/trend_line.vue b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/trend_line.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d7b02310343943bfcfa2cbf046f8e470b0582ce3
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/trend_line.vue
@@ -0,0 +1,56 @@
+
+
+
+
diff --git a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/components/metrics_data_table.vue b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/components/metrics_data_table.vue
new file mode 100644
index 0000000000000000000000000000000000000000..19575bdcdae5dd704dcadc4e6eaa4c54c3719aa4
--- /dev/null
+++ b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/components/metrics_data_table.vue
@@ -0,0 +1,135 @@
+
+
+
+
diff --git a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/constants.js b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/constants.js
index 6b96378100800308c79297dc60ade7e81e932782..73fbc12f32de8d56294b0c1305a80589357127d6 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/constants.js
+++ b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/constants.js
@@ -7,6 +7,7 @@ import {
VULNERABILITY_METRICS,
CONTRIBUTOR_METRICS,
AI_METRICS,
+ PIPELINE_ANALYTICS_METRICS,
UNITS,
} from '~/analytics/shared/constants';
@@ -28,6 +29,13 @@ export const SUPPORTED_MERGE_REQUEST_METRICS = [MERGE_REQUEST_METRICS.THROUGHPUT
export const SUPPORTED_VULNERABILITY_METRICS = [VULNERABILITY_METRICS.CRITICAL];
export const SUPPORTED_CONTRIBUTOR_METRICS = [CONTRIBUTOR_METRICS.COUNT];
+export const SUPPORTED_PIPELINE_ANALYTICS_METRICS = [
+ PIPELINE_ANALYTICS_METRICS.COUNT,
+ PIPELINE_ANALYTICS_METRICS.SUCCESS_RATE,
+ PIPELINE_ANALYTICS_METRICS.FAILED_RATE,
+ PIPELINE_ANALYTICS_METRICS.MEDIAN,
+];
+
export const SUPPORTED_AI_METRICS = [
AI_METRICS.CODE_SUGGESTIONS_USAGE_RATE,
AI_METRICS.CODE_SUGGESTIONS_ACCEPTANCE_RATE,
diff --git a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/utils.js b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/utils.js
index a9e89e93467427665c68fd0487d5be5a69768f56..9f99c741beafc49b03ea9f74140cdc5165cf3de4 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/ai_impact/utils.js
+++ b/ee/app/assets/javascripts/analytics/dashboards/ai_impact/utils.js
@@ -25,15 +25,18 @@ const getStartOfMonth = (now) => dateAtFirstDayOfMonth(getStartOfDay(now));
* @param {Date} now Current date
* @returns {Array} Tuple of time periods
*/
-export const generateDateRanges = (now) => {
+export const generateDateRanges = (now, component = null) => {
const formatDateHeader = (date) => formatDate(date, 'mmm yyyy');
+ const defaultFields = { thClass: 'gl-w-1/10' };
+ const extraFields = component ? { component, ...defaultFields } : defaultFields;
+
const currentMonth = {
key: 'this-month',
label: formatDateHeader(now),
start: getStartOfMonth(now),
end: now,
- thClass: 'gl-w-1/10',
+ ...extraFields,
};
return [1, 2, 3, 4, 5].reduce(
@@ -47,7 +50,7 @@ export const generateDateRanges = (now) => {
label: formatDateHeader(start),
start,
end,
- thClass: 'gl-w-1/10',
+ ...extraFields,
},
...acc,
];
@@ -85,6 +88,39 @@ export const generateTableColumns = (now) => [
},
];
+/**
+ * Generates all the table columns based on the given date.
+ *
+ * @param {Date} now
+ * @returns {Array} The list of columns
+ */
+export const generateTableColumnsWithComponents = (now) => [
+ {
+ key: 'metric',
+ label: __('Metric'),
+ thClass: 'gl-w-3/20',
+ component: 'MetricTableCell',
+ },
+ ...generateDateRanges(now, 'SimpleValue'),
+ {
+ key: 'change',
+ label: sprintf(__('Change (%%)')),
+ description: __('Past 6 Months'),
+ thClass: 'gl-w-3/20',
+ component: 'ChangePercentageIndicator',
+ },
+ {
+ key: 'chart',
+ label: __('Trend'),
+ start: nMonthsBefore(now, 6),
+ end: now,
+ thClass: 'gl-w-1/10',
+ tdClass: '!gl-py-2',
+ component: 'TrendLine',
+ },
+];
+
+// TODO: parameterize table metrics and dedupe method
/**
* Creates the table rows filled with blank data. Once the data has loaded,
* it can be filled into the returned skeleton using `mergeTableData`.
@@ -92,8 +128,11 @@ export const generateTableColumns = (now) => [
* @param {Array} excludeMetrics - Array of metric identifiers to remove from the table
* @returns {Array} array of data-less table rows
*/
-export const generateSkeletonTableData = (excludeMetrics = []) =>
- Object.entries(AI_IMPACT_TABLE_METRICS)
+export const generateSkeletonTableData = (
+ excludeMetrics = [],
+ tableMetrics = AI_IMPACT_TABLE_METRICS,
+) =>
+ Object.entries(tableMetrics)
.filter(([identifier]) => !excludeMetrics.includes(identifier))
.map(([identifier, { label, invertTrendColor }]) => ({
metric: { identifier, value: label },
@@ -164,14 +203,15 @@ const buildTableRow = ({ identifier, units, timePeriods }) => {
return { ...row, change };
};
+// TODO: parameterize table metrics and dedupe method
/**
* Takes N time periods of metrics and formats the data to be displayed in the table.
*
* @param {Array} timePeriods - Array of metrics for different time periods
* @returns {Object} object containing the same data, formatted for the table
*/
-export const generateTableRows = (timePeriods) =>
- Object.entries(AI_IMPACT_TABLE_METRICS).reduce((acc, [identifier, { units }]) => {
+export const generateTableRows = (timePeriods, tableMetrics = AI_IMPACT_TABLE_METRICS) => {
+ return Object.entries(tableMetrics).reduce((acc, [identifier, { units }]) => {
if (!isMetricInTimePeriods(identifier, timePeriods)) return acc;
return Object.assign(acc, {
@@ -182,6 +222,7 @@ export const generateTableRows = (timePeriods) =>
}),
});
}, {});
+};
/**
* Calculates a rate, given a numerator and a denominator
@@ -241,6 +282,7 @@ export const getRestrictedTableMetrics = (
* @typedef {Array<[String, MetricIds]>} AlertGroup
*/
+// TODO: parameterize TABLE_METRICS, dedupe and remove
/**
* Creates a list of panel alerts to be rendered for the metric table.
*
diff --git a/ee/app/assets/javascripts/analytics/dashboards/api.js b/ee/app/assets/javascripts/analytics/dashboards/api.js
index adeb97334c6b60177fbbc38981db03a4c4f8484e..877944964f753d3696083aa262cc655f58e4a4de 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/api.js
+++ b/ee/app/assets/javascripts/analytics/dashboards/api.js
@@ -1,6 +1,11 @@
-import { CONTRIBUTOR_METRICS, VULNERABILITY_METRICS } from '~/analytics/shared/constants';
+import {
+ CONTRIBUTOR_METRICS,
+ VULNERABILITY_METRICS,
+ PIPELINE_ANALYTICS_METRICS,
+} from '~/analytics/shared/constants';
import { scaledValueForDisplay } from '~/analytics/shared/utils';
import { TABLE_METRICS } from './constants';
+import { calculateRate } from './ai_impact/utils';
/**
* @typedef {Object} ValueStreamDashboardTableMetric
@@ -44,6 +49,49 @@ export const extractGraphqlVulnerabilitiesData = (rawVulnerabilityData = []) =>
};
};
+/**
+ * Takes the raw Query.pipelineAnalytics graphql response and prepares the data for display
+ * in the Value streams dashboard.
+ *
+ * An array is returned, but we only want the first value (the latest date) if there are multiple
+ *
+ * @param {AggregatedPipelineMetricsDataItem} data
+ * @returns {AggregatedPipelineMetricsDataResponseItem} Vulnerability metric data ready for rendering in the value stream dashboard
+ */
+export const extractAggregatedPipelineMetricsData = (rawAggregatedPipelineMetricsData = []) => {
+ // NOTE: the fromTime/toTime ends up spreading over 2 months, we take the latest
+ // TODO: can the date/times be adjusted / converted before sending the query to properly align with each month?
+ const metricData = rawAggregatedPipelineMetricsData.length
+ ? rawAggregatedPipelineMetricsData[0]
+ : {};
+
+ const {
+ count,
+ successCount,
+ failedCount,
+ durationStatistics: { p50: median = null } = {},
+ } = metricData;
+
+ return {
+ [PIPELINE_ANALYTICS_METRICS.COUNT]: {
+ identifier: PIPELINE_ANALYTICS_METRICS.COUNT,
+ value: count || '-',
+ },
+ [PIPELINE_ANALYTICS_METRICS.SUCCESS_RATE]: {
+ identifier: PIPELINE_ANALYTICS_METRICS.SUCCESS_RATE,
+ value: calculateRate({ numerator: successCount, denominator: count }) ?? '-',
+ },
+ [PIPELINE_ANALYTICS_METRICS.FAILED_RATE]: {
+ identifier: PIPELINE_ANALYTICS_METRICS.FAILED_RATE,
+ value: calculateRate({ numerator: failedCount, denominator: count }) ?? '-',
+ },
+ [PIPELINE_ANALYTICS_METRICS.MEDIAN]: {
+ identifier: PIPELINE_ANALYTICS_METRICS.MEDIAN,
+ value: median || '-',
+ },
+ };
+};
+
/**
* @typedef {Object} DoraMetricItem
* @property {String} date - ISO 8601 date
diff --git a/ee/app/assets/javascripts/analytics/dashboards/components/comparison_table.vue b/ee/app/assets/javascripts/analytics/dashboards/components/comparison_table.vue
index ff7ea041f339d9f3c08caafbd1fc0f91e2d13ec8..1c1c30df9badeeb12721e8f24106dbe47b7da1f7 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/components/comparison_table.vue
+++ b/ee/app/assets/javascripts/analytics/dashboards/components/comparison_table.vue
@@ -6,7 +6,7 @@ import { formatNumber } from '~/locale';
import { VSD_COMPARISON_TABLE_TRACKING_PROPERTY } from 'ee/analytics/analytics_dashboards/constants';
import { CHART_GRADIENT, CHART_GRADIENT_INVERTED } from '../constants';
import { generateDashboardTableFields } from '../utils';
-import MetricTableCell from './metric_table_cell.vue';
+import MetricTableCell from '../../analytics_dashboards/components/visualizations/data_table/metric_table_cell.vue';
import TrendIndicator from './trend_indicator.vue';
export default {
diff --git a/ee/app/assets/javascripts/analytics/dashboards/constants.js b/ee/app/assets/javascripts/analytics/dashboards/constants.js
index 0b7dd49f6966fb3c86a5af843a76186a1119f422..6218e16bd1863d886c9883cbe0db81fa7a22758b 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/constants.js
+++ b/ee/app/assets/javascripts/analytics/dashboards/constants.js
@@ -6,6 +6,7 @@ import {
VULNERABILITY_METRICS,
MERGE_REQUEST_METRICS,
CONTRIBUTOR_METRICS,
+ PIPELINE_ANALYTICS_METRICS,
UNITS,
} from '~/analytics/shared/constants';
@@ -112,6 +113,30 @@ export const TABLE_METRICS = {
},
};
+export const PIPELINE_ANALYTICS_TABLE_METRICS = {
+ [PIPELINE_ANALYTICS_METRICS.COUNT]: {
+ label: s__('DORA4Metrics|Total pipeline runs'),
+ // TODO: double check the `invertTrendColor` value for each
+ invertTrendColor: true,
+ units: UNITS.COUNT,
+ },
+ [PIPELINE_ANALYTICS_METRICS.MEDIAN]: {
+ label: s__('DORA4Metrics|Median duration'),
+ invertTrendColor: true,
+ units: UNITS.COUNT,
+ },
+ [PIPELINE_ANALYTICS_METRICS.SUCCESS_RATE]: {
+ label: s__('DORA4Metrics|Success rate'),
+ invertTrendColor: true,
+ units: UNITS.PERCENT,
+ },
+ [PIPELINE_ANALYTICS_METRICS.FAILED_RATE]: {
+ label: s__('DORA4Metrics|Failure rate'),
+ invertTrendColor: true,
+ units: UNITS.PERCENT,
+ },
+};
+
export const METRICS_WITH_NO_TREND = [VULNERABILITY_METRICS.CRITICAL, VULNERABILITY_METRICS.HIGH];
export const METRICS_WITH_LABEL_FILTERING = [
FLOW_METRICS.ISSUES,
diff --git a/ee/app/assets/javascripts/analytics/dashboards/utils.js b/ee/app/assets/javascripts/analytics/dashboards/utils.js
index 902ad9c7bc5aced410e14d4654977667e4bc0f3f..e122332e3f4d079fc5a6c6509f5de6487e187122 100644
--- a/ee/app/assets/javascripts/analytics/dashboards/utils.js
+++ b/ee/app/assets/javascripts/analytics/dashboards/utils.js
@@ -93,6 +93,7 @@ export const formatMetric = (value, units) => {
export const percentChange = ({ current, previous }) =>
previous > 0 && current > 0 ? (current - previous) / previous : 0;
+// TODO: parameterize TABLE_METRICS, dedupe and remove
/**
* Creates the table rows filled with blank data for the comparison table. Once the data
* has loaded, it can be filled into the returned skeleton using `mergeTableData`.
@@ -100,8 +101,8 @@ export const percentChange = ({ current, previous }) =>
* @param {Array} excludeMetrics - Array of DORA metric identifiers to remove from the table
* @returns {Array} array of data-less table rows
*/
-export const generateSkeletonTableData = (excludeMetrics = []) =>
- Object.entries(TABLE_METRICS)
+export const generateSkeletonTableData = (excludeMetrics = [], metrics = TABLE_METRICS) =>
+ Object.entries(metrics)
.filter(([identifier]) => !excludeMetrics.includes(identifier))
.map(([identifier, { label, invertTrendColor, valueLimit }]) => ({
invertTrendColor,
@@ -384,10 +385,10 @@ export const getRestrictedTableMetrics = (
* potential alerts to show, if there are any metrics present.
* @returns {Array} The list of alerts to be rendered for the comparison chart.
*/
-export const generateTableAlerts = (alertGroups) =>
+export const generateTableAlerts = (alertGroups, tableMetrics = TABLE_METRICS) =>
alertGroups.reduce((acc, [message, metrics]) => {
if (metrics.length === 0) return acc;
- const formattedMetrics = metrics.map((metric) => TABLE_METRICS[metric].label).join(', ');
+ const formattedMetrics = metrics.map((metric) => tableMetrics[metric].label).join(', ');
return [...acc, `${message}: ${formattedMetrics}`];
}, []);
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 00202bd4f49e8be534eaf5f8eb0c10f9b04bf1dd..d2ca109b3bfd5709f165aeab17afd54c89e07c72 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -20047,6 +20047,9 @@ msgstr ""
msgid "DORA4Metrics|Failed to load labels matching the filter: %{labels}"
msgstr ""
+msgid "DORA4Metrics|Failure rate"
+msgstr ""
+
msgid "DORA4Metrics|Filtered by"
msgstr ""
@@ -20112,6 +20115,9 @@ msgstr ""
msgid "DORA4Metrics|Median (last %{days}d)"
msgstr ""
+msgid "DORA4Metrics|Median duration"
+msgstr ""
+
msgid "DORA4Metrics|Median time (last %{days}d)"
msgstr ""
@@ -20184,6 +20190,9 @@ msgstr ""
msgid "DORA4Metrics|Something went wrong while getting time to restore service data."
msgstr ""
+msgid "DORA4Metrics|Success rate"
+msgstr ""
+
msgid "DORA4Metrics|The chart displays the frequency of deployments to production environment(s) that are based on the %{linkStart}deployment_tier%{linkEnd} value."
msgstr ""
@@ -20235,6 +20244,9 @@ msgstr ""
msgid "DORA4Metrics|Took more than 7 days to restore service when a service incident or a defect that impacts users occurs."
msgstr ""
+msgid "DORA4Metrics|Total pipeline runs"
+msgstr ""
+
msgid "DORA4Metrics|Total projects (%{count}) with DORA metrics"
msgstr ""