summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/analytics
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/analytics')
-rw-r--r--app/assets/javascripts/analytics/shared/components/daterange.vue33
-rw-r--r--app/assets/javascripts/analytics/shared/components/metric_popover.vue61
-rw-r--r--app/assets/javascripts/analytics/shared/components/metric_tile.vue51
-rw-r--r--app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue99
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js45
-rw-r--r--app/assets/javascripts/analytics/shared/utils.js27
-rw-r--r--app/assets/javascripts/analytics/usage_trends/graphql/fragments/count.fragment.graphql1
7 files changed, 292 insertions, 25 deletions
diff --git a/app/assets/javascripts/analytics/shared/components/daterange.vue b/app/assets/javascripts/analytics/shared/components/daterange.vue
index a5b9c40b9c9..7df66d1b2be 100644
--- a/app/assets/javascripts/analytics/shared/components/daterange.vue
+++ b/app/assets/javascripts/analytics/shared/components/daterange.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDaterangePicker, GlSprintf, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlDaterangePicker, GlSprintf } from '@gitlab/ui';
import { getDayDifference } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
import { OFFSET_DATE_BY_ONE } from '../constants';
@@ -8,10 +8,6 @@ export default {
components: {
GlDaterangePicker,
GlSprintf,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
},
props: {
show: {
@@ -56,7 +52,7 @@ export default {
return {
maxDateRangeTooltip: sprintf(
__(
- 'Showing data for workflow items created in this date range. Date range cannot exceed %{maxDateRange} days.',
+ 'Showing data for workflow items created in this date range. Date range limited to %{maxDateRange} days.',
),
{
maxDateRange: this.maxDateRange,
@@ -94,28 +90,15 @@ export default {
:max-date-range="maxDateRange"
:default-max-date="maxDate"
:same-day-selection="includeSelectedDate"
+ :tooltip="maxDateRangeTooltip"
theme="animate-picker"
start-picker-class="js-daterange-picker-from gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-lg-align-items-center gl-lg-mr-3 gl-mb-2 gl-lg-mb-0"
- end-picker-class="js-daterange-picker-to d-flex flex-column flex-lg-row align-items-lg-center"
+ end-picker-class="js-daterange-picker-to d-flex flex-column flex-lg-row align-items-lg-center gl-mb-2 gl-lg-mb-0"
label-class="gl-mb-2 gl-lg-mb-0"
- />
- <div
- v-if="maxDateRange"
- class="daterange-indicator d-flex flex-row flex-lg-row align-items-flex-start align-items-lg-center"
>
- <span class="number-of-days pl-2 pr-1">
- <gl-sprintf :message="n__('1 day selected', '%d days selected', numberOfDays)">
- <template #numberOfDays>{{ numberOfDays }}</template>
- </gl-sprintf>
- </span>
- <gl-icon
- v-gl-tooltip
- data-testid="helper-icon"
- :title="maxDateRangeTooltip"
- name="question"
- :size="14"
- class="text-secondary"
- />
- </div>
+ <gl-sprintf :message="n__('1 day selected', '%d days selected', numberOfDays)">
+ <template #numberOfDays>{{ numberOfDays }}</template>
+ </gl-sprintf>
+ </gl-daterange-picker>
</div>
</template>
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 || '',
+ };
+ });
diff --git a/app/assets/javascripts/analytics/usage_trends/graphql/fragments/count.fragment.graphql b/app/assets/javascripts/analytics/usage_trends/graphql/fragments/count.fragment.graphql
index 2bde5973600..b353bcdfd0e 100644
--- a/app/assets/javascripts/analytics/usage_trends/graphql/fragments/count.fragment.graphql
+++ b/app/assets/javascripts/analytics/usage_trends/graphql/fragments/count.fragment.graphql
@@ -1,4 +1,5 @@
fragment Count on UsageTrendsMeasurement {
+ __typename
count
recordedAt
}