Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 16:16:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 16:16:36 +0300
commit311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch)
tree07e7870bca8aed6d61fdcc810731c50d2c40af47 /app/assets/javascripts/cycle_analytics
parent27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff)
Add latest changes from gitlab-org/gitlab@14-5-stable-eev14.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/cycle_analytics')
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue17
-rw-r--r--app/assets/javascripts/cycle_analytics/components/metric_popover.vue61
-rw-r--r--app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue49
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js7
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js49
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js69
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js60
7 files changed, 211 insertions, 101 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 1d98a42ce58..36430e51dd2 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -2,10 +2,12 @@
import { GlLoadingIcon } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex';
+import { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
import { __ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
@@ -19,6 +21,7 @@ export default {
StageTable,
ValueStreamFilters,
ValueStreamMetrics,
+ UrlSync,
},
props: {
noDataSvgPath: {
@@ -54,6 +57,9 @@ export default {
'pagination',
]),
...mapGetters(['pathNavigationData', 'filterParams']),
+ isLoaded() {
+ return !this.isLoading && !this.isLoadingStage;
+ },
displayStageEvents() {
const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
@@ -98,6 +104,16 @@ export default {
metricsRequests() {
return this.features?.cycleAnalyticsForGroups ? METRICS_REQUESTS : SUMMARY_METRICS_REQUEST;
},
+ query() {
+ return {
+ created_after: toYmd(this.createdAfter),
+ created_before: toYmd(this.createdBefore),
+ stage_id: this.selectedStage?.id || null,
+ sort: this.pagination?.sort || null,
+ direction: this.pagination?.direction || null,
+ page: this.pagination?.page || null,
+ };
+ },
},
methods: {
...mapActions([
@@ -176,5 +192,6 @@ export default {
:pagination="pagination"
@handleUpdatePagination="onHandleUpdatePagination"
/>
+ <url-sync v-if="isLoaded" :query="query" />
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/metric_popover.vue b/app/assets/javascripts/cycle_analytics/components/metric_popover.vue
new file mode 100644
index 00000000000..8d90e7b2392
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/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/cycle_analytics/components/value_stream_metrics.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
index 7371ffd2c7c..9671742e564 100644
--- a/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/cycle_analytics/components/value_stream_metrics.vue
@@ -1,11 +1,13 @@
<script>
-import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlPopover } from '@gitlab/ui';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { flatten } from 'lodash';
import createFlash from '~/flash';
import { sprintf, s__ } from '~/locale';
+import { redirectTo } from '~/lib/utils/url_utility';
import { METRICS_POPOVER_CONTENT } from '../constants';
import { removeFlash, prepareTimeMetricsData } from '../utils';
+import MetricPopover from './metric_popover.vue';
const requestData = ({ request, endpoint, path, params, name }) => {
return request({ endpoint, params, requestPath: path })
@@ -31,9 +33,9 @@ const fetchMetricsData = (reqs = [], path, params) => {
export default {
name: 'ValueStreamMetrics',
components: {
- GlPopover,
GlSingleStat,
GlSkeletonLoading,
+ MetricPopover,
},
props: {
requestPath: {
@@ -76,32 +78,33 @@ export default {
this.isLoading = false;
});
},
+ hasLinks(links) {
+ return links?.length && links[0].url;
+ },
+ clickHandler({ links }) {
+ if (this.hasLinks(links)) {
+ redirectTo(links[0].url);
+ }
+ },
},
};
</script>
<template>
<div class="gl-display-flex gl-flex-wrap" data-testid="vsa-time-metrics">
- <div v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6">
- <gl-skeleton-loading />
+ <gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" />
+ <div v-for="metric in metrics" v-show="!isLoading" :key="metric.key" class="gl-my-6 gl-pr-9">
+ <gl-single-stat
+ :id="metric.key"
+ :value="`${metric.value}`"
+ :title="metric.label"
+ :unit="metric.unit || ''"
+ :should-animate="true"
+ :animation-decimal-places="1"
+ :class="{ 'gl-hover-cursor-pointer': hasLinks(metric.links) }"
+ tabindex="0"
+ @click="clickHandler(metric)"
+ />
+ <metric-popover :metric="metric" :target="metric.key" />
</div>
- <template v-else>
- <div v-for="metric in metrics" :key="metric.key" class="gl-my-6 gl-pr-9">
- <gl-single-stat
- :id="metric.key"
- :value="`${metric.value}`"
- :title="metric.label"
- :unit="metric.unit || ''"
- :should-animate="true"
- :animation-decimal-places="1"
- tabindex="0"
- />
- <gl-popover :target="metric.key" placement="bottom">
- <template #title>
- <span class="gl-display-block gl-text-left">{{ metric.label }}</span>
- </template>
- <span v-if="metric.description">{{ metric.description }}</span>
- </gl-popover>
- </div>
- </template>
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index c205aa1e831..7d5822b0824 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -5,8 +5,6 @@ import {
} from '~/api/analytics_api';
import { __, s__ } from '~/locale';
-export const DEFAULT_DAYS_IN_PAST = 30;
-export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
export const DEFAULT_VALUE_STREAM = {
@@ -47,6 +45,11 @@ export const METRICS_POPOVER_CONTENT = {
"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.',
+ ),
+ },
'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.') },
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 34ef03409b8..3da8696edeb 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -1,44 +1,36 @@
import Vue from 'vue';
+import {
+ extractFilterQueryParameters,
+ extractPaginationQueryParameters,
+} from '~/analytics/shared/utils';
import Translate from '../vue_shared/translate';
import CycleAnalytics from './components/base.vue';
-import { DEFAULT_DAYS_TO_DISPLAY } from './constants';
import createStore from './store';
-import { calculateFormattedDayInPast } from './utils';
+import { buildCycleAnalyticsInitialData } from './utils';
Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
- const {
- noAccessSvgPath,
- noDataSvgPath,
- requestPath,
- fullPath,
- projectId,
- groupId,
- groupPath,
- labelsPath,
- milestonesPath,
- } = el.dataset;
+ const { noAccessSvgPath, noDataSvgPath } = el.dataset;
+ const initialData = buildCycleAnalyticsInitialData({ ...el.dataset, gon });
- const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
+ const pagination = extractPaginationQueryParameters(window.location.search);
+ const {
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ } = extractFilterQueryParameters(window.location.search);
store.dispatch('initializeVsa', {
- projectId: parseInt(projectId, 10),
- endpoints: {
- requestPath,
- fullPath,
- labelsPath,
- milestonesPath,
- groupId: parseInt(groupId, 10),
- groupPath,
- },
- features: {
- cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
- },
- createdBefore: new Date(now),
- createdAfter: new Date(past),
+ ...initialData,
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ pagination,
});
// eslint-disable-next-line no-new
@@ -52,7 +44,6 @@ export default () => {
props: {
noDataSvgPath,
noAccessSvgPath,
- fullPath,
},
}),
});
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index 24b62849db7..e0156b24f9d 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -14,7 +14,7 @@ import * as types from './mutation_types';
export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => {
commit(types.SET_SELECTED_VALUE_STREAM, valueStream);
- return Promise.all([dispatch('fetchValueStreamStages'), dispatch('fetchCycleAnalyticsData')]);
+ return dispatch('fetchValueStreamStages');
};
export const fetchValueStreamStages = ({ commit, state }) => {
@@ -46,10 +46,8 @@ export const fetchValueStreams = ({ commit, dispatch, state }) => {
} = state;
commit(types.REQUEST_VALUE_STREAMS);
- const stageRequests = ['setSelectedStage', 'fetchStageMedians', 'fetchStageCountValues'];
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
- .then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
@@ -153,33 +151,36 @@ export const fetchStageCountValues = ({
});
};
-export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
- const stage = selectedStage || stages[0];
- commit(types.SET_SELECTED_STAGE, stage);
- return dispatch('fetchStageData');
+export const fetchValueStreamStageData = ({ dispatch }) =>
+ Promise.all([
+ dispatch('fetchCycleAnalyticsData'),
+ dispatch('fetchStageData'),
+ dispatch('fetchStageMedians'),
+ dispatch('fetchStageCountValues'),
+ ]);
+
+export const refetchStageData = async ({ dispatch, commit }) => {
+ commit(types.SET_LOADING, true);
+ await dispatch('fetchValueStreamStageData');
+ commit(types.SET_LOADING, false);
};
-export const setLoading = ({ commit }, value) => commit(types.SET_LOADING, value);
-
-const refetchStageData = (dispatch) => {
- return Promise.resolve()
- .then(() => dispatch('setLoading', true))
- .then(() =>
- Promise.all([
- dispatch('fetchCycleAnalyticsData'),
- dispatch('fetchStageData'),
- dispatch('fetchStageMedians'),
- dispatch('fetchStageCountValues'),
- ]),
- )
- .finally(() => dispatch('setLoading', false));
+export const setSelectedStage = ({ dispatch, commit }, selectedStage = null) => {
+ commit(types.SET_SELECTED_STAGE, selectedStage);
+ return dispatch('refetchStageData');
};
-export const setFilters = ({ dispatch }) => refetchStageData(dispatch);
+export const setFilters = ({ dispatch }) => dispatch('refetchStageData');
export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => {
commit(types.SET_DATE_RANGE, { createdAfter, createdBefore });
- return refetchStageData(dispatch);
+ return dispatch('refetchStageData');
+};
+
+export const setInitialStage = ({ dispatch, commit, state: { stages } }, stage) => {
+ const selectedStage = stage || stages[0];
+ commit(types.SET_SELECTED_STAGE, selectedStage);
+ return dispatch('fetchValueStreamStageData');
};
export const updateStageTablePagination = (
@@ -190,12 +191,18 @@ export const updateStageTablePagination = (
return dispatch('fetchStageData', selectedStage.id);
};
-export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
+export const initializeVsa = async ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData);
const {
endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' },
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ selectedStage = null,
} = initialData;
+
dispatch('filters/setEndpoints', {
labelsEndpoint: labelsPath,
milestonesEndpoint: milestonesPath,
@@ -203,7 +210,15 @@ export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
projectEndpoint: fullPath,
});
- return dispatch('setLoading', true)
- .then(() => dispatch('fetchValueStreams'))
- .finally(() => dispatch('setLoading', false));
+ dispatch('filters/initialize', {
+ selectedAuthor,
+ selectedMilestone,
+ selectedAssigneeList,
+ selectedLabelList,
+ });
+
+ commit(types.SET_LOADING, true);
+ await dispatch('fetchValueStreams');
+ await dispatch('setInitialStage', selectedStage);
+ commit(types.SET_LOADING, false);
};
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
index 3c6267bac06..9af63f5f9cc 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -1,7 +1,4 @@
-import dateFormat from 'dateformat';
-import { dateFormats } from '~/analytics/shared/constants';
import { hideFlash } from '~/flash';
-import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { formatTimeAsSummary } from '~/lib/utils/datetime/date_format_utility';
import { slugify } from '~/lib/utils/text_utility';
@@ -74,23 +71,6 @@ export const formatMedianValues = (medians = []) =>
export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
-const toIsoFormat = (d) => dateFormat(d, dateFormats.isoDate);
-
-/**
- * Takes an integer specifying the number of days to subtract
- * from the date specified will return the 2 dates, formatted as ISO dates
- *
- * @param {Number} daysInPast - Number of days in the past to subtract
- * @param {Date} [today=new Date] - Date to subtract days from, defaults to today
- * @returns {Object} Returns 'now' and the 'past' date formatted as ISO dates
- */
-export const calculateFormattedDayInPast = (daysInPast, today = new Date()) => {
- return {
- now: toIsoFormat(today),
- past: toIsoFormat(getDateInPast(today, daysInPast)),
- };
-};
-
/**
* @typedef {Object} MetricData
* @property {String} title - Title of the metric measured
@@ -123,3 +103,43 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
description: popoverContent[key]?.description || '',
};
});
+
+const extractFeatures = (gon) => ({
+ cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
+});
+
+/**
+ * Builds the initial data object for Value Stream Analytics with data loaded from the backend
+ *
+ * @param {Object} dataset - dataset object paseed to the frontend via data-* properties
+ * @returns {Object} - The initial data to load the app with
+ */
+export const buildCycleAnalyticsInitialData = ({
+ fullPath,
+ requestPath,
+ projectId,
+ groupId,
+ groupPath,
+ labelsPath,
+ milestonesPath,
+ stage,
+ createdAfter,
+ createdBefore,
+ gon,
+} = {}) => {
+ return {
+ projectId: parseInt(projectId, 10),
+ endpoints: {
+ requestPath,
+ fullPath,
+ labelsPath,
+ milestonesPath,
+ groupId: parseInt(groupId, 10),
+ groupPath,
+ },
+ createdAfter: new Date(createdAfter),
+ createdBefore: new Date(createdBefore),
+ selectedStage: stage ? JSON.parse(stage) : null,
+ features: extractFeatures(gon),
+ };
+};