From 37c79a8a5d4faf5266265bda66caea667e46dbcd Mon Sep 17 00:00:00 2001 From: Ezekiel <3397881-ekigbo@users.noreply.gitlab.com> Date: Fri, 18 Jul 2025 22:27:38 +1000 Subject: [PATCH 1/3] Add additional datatable column rendering components Adds datatable columns to support rendering the DORA/AI Impact comparison table. --- .../change_percentage_indicator.stories.js | 42 +++++++ .../change_percentage_indicator.vue | 58 ++++++++++ .../data_table/data_table.stories.js | 105 ++++++++++-------- .../visualizations/data_table/data_table.vue | 9 +- .../data_table/metric_table_cell.stories.js | 35 ++++++ .../data_table}/metric_table_cell.vue | 49 +++++--- .../data_table/simple_value.vue | 21 ++++ .../data_table/trend_line.stories.js | 50 +++++++++ .../visualizations/data_table/trend_line.vue | 56 ++++++++++ 9 files changed, 359 insertions(+), 66 deletions(-) create mode 100644 ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator.stories.js create mode 100644 ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator.vue create mode 100644 ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/metric_table_cell.stories.js rename ee/app/assets/javascripts/analytics/{dashboards/components => analytics_dashboards/components/visualizations/data_table}/metric_table_cell.vue (66%) create mode 100644 ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/simple_value.vue create mode 100644 ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/trend_line.stories.js create mode 100644 ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/trend_line.vue 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 00000000000000..8d6631bd634443 --- /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 00000000000000..2504abbc1be776 --- /dev/null +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/change_percentage_indicator.vue @@ -0,0 +1,58 @@ + + 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 e93b31faee2f54..26748cae7327c0 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 7c2f910051eea7..0eaaf610aa2771 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 {
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 00000000000000..2d53a26c3d6ab7 --- /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 6f8b88a72d4aed..d0720950ae63e9 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 }} - - - +
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 00000000000000..6ed71415b2f8dc --- /dev/null +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/simple_value.vue @@ -0,0 +1,21 @@ + + 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 00000000000000..5828686c512f57 --- /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 00000000000000..d7b02310343943 --- /dev/null +++ b/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table/trend_line.vue @@ -0,0 +1,56 @@ + + -- GitLab From 238a583db381b4648e40d4986f77b38e0cd6269d Mon Sep 17 00:00:00 2001 From: Ezekiel <3397881-ekigbo@users.noreply.gitlab.com> Date: Fri, 18 Jul 2025 22:49:37 +1000 Subject: [PATCH 2/3] Add missing constants Fixes some missing imports and adds constants for the pipeline metrics --- .../javascripts/analytics/shared/constants.js | 12 +++++++++ .../components/comparison_table.vue | 2 +- .../analytics/dashboards/constants.js | 25 +++++++++++++++++++ locale/gitlab.pot | 12 +++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js index 800424b79b22ad..254e181a9e8c19 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/dashboards/components/comparison_table.vue b/ee/app/assets/javascripts/analytics/dashboards/components/comparison_table.vue index ff7ea041f339d9..1c1c30df9badee 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 0b7dd49f6966fb..6218e16bd1863d 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/locale/gitlab.pot b/locale/gitlab.pot index 00202bd4f49e8b..d2ca109b3bfd57 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 "" -- GitLab From 3aa68cdfe897d10eebb569fcbb1f131aea9e1369 Mon Sep 17 00:00:00 2001 From: Ezekiel <3397881-ekigbo@users.noreply.gitlab.com> Date: Fri, 18 Jul 2025 23:06:42 +1000 Subject: [PATCH 3/3] [skip ci] wip - add metrics data table component --- .../components/metrics_data_table.vue | 135 ++++++++++++++++++ .../dashboards/ai_impact/constants.js | 8 ++ .../analytics/dashboards/ai_impact/utils.js | 56 +++++++- .../javascripts/analytics/dashboards/api.js | 50 ++++++- .../javascripts/analytics/dashboards/utils.js | 9 +- 5 files changed, 246 insertions(+), 12 deletions(-) create mode 100644 ee/app/assets/javascripts/analytics/dashboards/ai_impact/components/metrics_data_table.vue 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 00000000000000..19575bdcdae5dd --- /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 6b963781008003..73fbc12f32de8d 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 a9e89e93467427..9f99c741beafc4 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 adeb97334c6b60..877944964f753d 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/utils.js b/ee/app/assets/javascripts/analytics/dashboards/utils.js index 902ad9c7bc5ace..e122332e3f4d07 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}`]; }, []); -- GitLab