diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-04 15:17:40 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-04 15:17:40 +0300 |
commit | 9486811b62db7f35906bae75f912aa89804e721b (patch) | |
tree | 92da5045f0554bf89a60a96eb4ac1ffa96bfa427 /app/assets/javascripts/analytics | |
parent | aa7870a90b5925412a38dd6a27522f83517b917e (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/analytics')
5 files changed, 283 insertions, 0 deletions
diff --git a/app/assets/javascripts/analytics/shared/components/metric_popover.vue b/app/assets/javascripts/analytics/shared/components/metric_popover.vue new file mode 100644 index 00000000000..8d90e7b2392 --- /dev/null +++ b/app/assets/javascripts/analytics/shared/components/metric_popover.vue @@ -0,0 +1,61 @@ +<script> +import { GlPopover, GlLink, GlIcon } from '@gitlab/ui'; + +export default { + name: 'MetricPopover', + components: { + GlPopover, + GlLink, + GlIcon, + }, + props: { + metric: { + type: Object, + required: true, + }, + target: { + type: String, + required: true, + }, + }, + computed: { + metricLinks() { + return this.metric.links?.filter((link) => !link.docs_link) || []; + }, + docsLink() { + return this.metric.links?.find((link) => link.docs_link); + }, + }, +}; +</script> + +<template> + <gl-popover :target="target" placement="bottom"> + <template #title> + <span class="gl-display-block gl-text-left" data-testid="metric-label">{{ + metric.label + }}</span> + </template> + <div + v-for="(link, idx) in metricLinks" + :key="`link-${idx}`" + class="gl-display-flex gl-justify-content-space-between gl-text-right gl-py-1" + data-testid="metric-link" + > + <span>{{ link.label }}</span> + <gl-link :href="link.url" class="gl-font-sm"> + {{ link.name }} + </gl-link> + </div> + <span v-if="metric.description" data-testid="metric-description">{{ metric.description }}</span> + <gl-link + v-if="docsLink" + :href="docsLink.url" + class="gl-font-sm" + target="_blank" + data-testid="metric-docs-link" + >{{ docsLink.label }} + <gl-icon name="external-link" class="gl-vertical-align-middle" /> + </gl-link> + </gl-popover> +</template> diff --git a/app/assets/javascripts/analytics/shared/components/metric_tile.vue b/app/assets/javascripts/analytics/shared/components/metric_tile.vue new file mode 100644 index 00000000000..845a3386f6c --- /dev/null +++ b/app/assets/javascripts/analytics/shared/components/metric_tile.vue @@ -0,0 +1,51 @@ +<script> +import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { redirectTo } from '~/lib/utils/url_utility'; +import MetricPopover from './metric_popover.vue'; + +export default { + name: 'MetricTile', + components: { + GlSingleStat, + MetricPopover, + }, + props: { + metric: { + type: Object, + required: true, + }, + }, + computed: { + decimalPlaces() { + const parsedFloat = parseFloat(this.metric.value); + return Number.isNaN(parsedFloat) || Number.isInteger(parsedFloat) ? 0 : 1; + }, + hasLinks() { + return this.metric.links?.length && this.metric.links[0].url; + }, + }, + methods: { + clickHandler({ links }) { + if (this.hasLinks) { + redirectTo(links[0].url); + } + }, + }, +}; +</script> +<template> + <div v-bind="$attrs"> + <gl-single-stat + :id="metric.identifier" + :value="`${metric.value}`" + :title="metric.label" + :unit="metric.unit || ''" + :should-animate="true" + :animation-decimal-places="decimalPlaces" + :class="{ 'gl-hover-cursor-pointer': hasLinks }" + tabindex="0" + @click="clickHandler(metric)" + /> + <metric-popover :metric="metric" :target="metric.identifier" /> + </div> +</template> diff --git a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue new file mode 100644 index 00000000000..1a3544e7677 --- /dev/null +++ b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue @@ -0,0 +1,99 @@ +<script> +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; +import { flatten, isEqual } from 'lodash'; +import createFlash from '~/flash'; +import { sprintf, s__ } from '~/locale'; +import { METRICS_POPOVER_CONTENT } from '../constants'; +import { removeFlash, prepareTimeMetricsData } from '../utils'; +import MetricTile from './metric_tile.vue'; + +const requestData = ({ request, endpoint, path, params, name }) => { + return request({ endpoint, params, requestPath: path }) + .then(({ data }) => data) + .catch(() => { + const message = sprintf( + s__( + 'ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data.', + ), + { requestTypeName: name }, + ); + createFlash({ message }); + }); +}; + +const fetchMetricsData = (reqs = [], path, params) => { + const promises = reqs.map((r) => requestData({ ...r, path, params })); + return Promise.all(promises).then((responses) => + prepareTimeMetricsData(flatten(responses), METRICS_POPOVER_CONTENT), + ); +}; + +export default { + name: 'ValueStreamMetrics', + components: { + GlSkeletonLoading, + MetricTile, + }, + props: { + requestPath: { + type: String, + required: true, + }, + requestParams: { + type: Object, + required: true, + }, + requests: { + type: Array, + required: true, + }, + filterFn: { + type: Function, + required: false, + default: null, + }, + }, + data() { + return { + metrics: [], + isLoading: false, + }; + }, + watch: { + requestParams(newVal, oldVal) { + if (!isEqual(newVal, oldVal)) { + this.fetchData(); + } + }, + }, + mounted() { + this.fetchData(); + }, + methods: { + fetchData() { + removeFlash(); + this.isLoading = true; + return fetchMetricsData(this.requests, this.requestPath, this.requestParams) + .then((data) => { + this.metrics = this.filterFn ? this.filterFn(data) : data; + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + }); + }, + }, +}; +</script> +<template> + <div class="gl-display-flex gl-flex-wrap" data-testid="vsa-metrics"> + <gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" /> + <metric-tile + v-for="metric in metrics" + v-show="!isLoading" + :key="metric.identifier" + :metric="metric" + class="gl-my-6 gl-pr-9" + /> + </div> +</template> diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js index c06bd34f86f..2ac144ceb5e 100644 --- a/app/assets/javascripts/analytics/shared/constants.js +++ b/app/assets/javascripts/analytics/shared/constants.js @@ -1,4 +1,5 @@ import { masks } from 'dateformat'; +import { s__ } from '~/locale'; export const DATE_RANGE_LIMIT = 180; export const OFFSET_DATE_BY_ONE = 1; @@ -11,3 +12,47 @@ export const dateFormats = { defaultDateTime: 'mmm d, yyyy h:MMtt', month: 'mmmm', }; + +// Some content is duplicated due to backward compatibility. +// It will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/350614 in 14.9 +export const METRICS_POPOVER_CONTENT = { + 'lead-time': { + description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'), + }, + lead_time: { + description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'), + }, + 'cycle-time': { + description: s__( + "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.", + ), + }, + cycle_time: { + description: s__( + "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.", + ), + }, + 'lead-time-for-changes': { + description: s__( + 'ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period.', + ), + }, + lead_time_for_changes: { + description: s__( + 'ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period.', + ), + }, + issues: { description: s__('ValueStreamAnalytics|Number of new issues created.') }, + 'new-issue': { description: s__('ValueStreamAnalytics|Number of new issues created.') }, + 'new-issues': { description: s__('ValueStreamAnalytics|Number of new issues created.') }, + deploys: { description: s__('ValueStreamAnalytics|Total number of deploys to production.') }, + 'deployment-frequency': { + description: s__('ValueStreamAnalytics|Average number of deployments to production per day.'), + }, + deployment_frequency: { + description: s__('ValueStreamAnalytics|Average number of deployments to production per day.'), + }, + commits: { + description: s__('ValueStreamAnalytics|Number of commits pushed to the default branch'), + }, +}; diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js index f55ef99964e..dde429ab278 100644 --- a/app/assets/javascripts/analytics/shared/utils.js +++ b/app/assets/javascripts/analytics/shared/utils.js @@ -1,4 +1,6 @@ import dateFormat from 'dateformat'; +import { hideFlash } from '~/flash'; +import { slugify } from '~/lib/utils/text_utility'; import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { dateFormats } from './constants'; @@ -69,3 +71,28 @@ export const getDataZoomOption = ({ }; }); }; + +export const removeFlash = (type = 'alert') => { + const flashEl = document.querySelector(`.flash-${type}`); + if (flashEl) { + hideFlash(flashEl); + } +}; + +/** + * Prepares metric data to be rendered in the metric_card component + * + * @param {MetricData[]} data - The metric data to be rendered + * @param {Object} popoverContent - Key value pair of data to display in the popover + * @returns {TransformedMetricData[]} An array of metrics ready to render in the metric_card + */ +export const prepareTimeMetricsData = (data = [], popoverContent = {}) => + data.map(({ title: label, identifier, ...rest }) => { + const metricIdentifier = identifier || slugify(label); + return { + ...rest, + label, + identifier: metricIdentifier, + description: popoverContent[metricIdentifier]?.description || '', + }; + }); |