diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-18 16:16:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-18 16:16:36 +0300 |
commit | 311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch) | |
tree | 07e7870bca8aed6d61fdcc810731c50d2c40af47 /app/assets/javascripts/cycle_analytics | |
parent | 27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff) |
Add latest changes from gitlab-org/gitlab@14-5-stable-eev14.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/cycle_analytics')
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), + }; +}; |