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-03-16 21:18:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-16 21:18:33 +0300
commitf64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch)
treea2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/assets/javascripts/cycle_analytics
parentbfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff)
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/assets/javascripts/cycle_analytics')
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue288
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js156
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js32
3 files changed, 320 insertions, 156 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
new file mode 100644
index 00000000000..df77d641e21
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -0,0 +1,288 @@
+<script>
+import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import Cookies from 'js-cookie';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import { __ } from '~/locale';
+import banner from './banner.vue';
+import stageCodeComponent from './stage_code_component.vue';
+import stageComponent from './stage_component.vue';
+import stageNavItem from './stage_nav_item.vue';
+import stageReviewComponent from './stage_review_component.vue';
+import stageStagingComponent from './stage_staging_component.vue';
+import stageTestComponent from './stage_test_component.vue';
+
+const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
+
+export default {
+ name: 'CycleAnalytics',
+ components: {
+ GlIcon,
+ GlEmptyState,
+ GlLoadingIcon,
+ GlSprintf,
+ banner,
+ 'stage-issue-component': stageComponent,
+ 'stage-plan-component': stageComponent,
+ 'stage-code-component': stageCodeComponent,
+ 'stage-test-component': stageTestComponent,
+ 'stage-review-component': stageReviewComponent,
+ 'stage-staging-component': stageStagingComponent,
+ 'stage-production-component': stageComponent,
+ 'stage-nav-item': stageNavItem,
+ },
+ props: {
+ noDataSvgPath: {
+ type: String,
+ required: true,
+ },
+ noAccessSvgPath: {
+ type: String,
+ required: true,
+ },
+ store: {
+ type: Object,
+ required: true,
+ },
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ state: this.store.state,
+ isLoading: false,
+ isLoadingStage: false,
+ isEmptyStage: false,
+ hasError: true,
+ startDate: 30,
+ isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
+ };
+ },
+ computed: {
+ currentStage() {
+ return this.store.currentActiveStage();
+ },
+ },
+ created() {
+ this.fetchCycleAnalyticsData();
+ },
+ methods: {
+ handleError() {
+ this.store.setErrorState(true);
+ return new Flash(__('There was an error while fetching value stream analytics data.'));
+ },
+ handleDateSelect(startDate) {
+ this.startDate = startDate;
+ this.fetchCycleAnalyticsData({ startDate: this.startDate });
+ },
+ fetchCycleAnalyticsData(options) {
+ const fetchOptions = options || { startDate: this.startDate };
+
+ this.isLoading = true;
+
+ this.service
+ .fetchCycleAnalyticsData(fetchOptions)
+ .then((response) => {
+ this.store.setCycleAnalyticsData(response);
+ this.selectDefaultStage();
+ })
+ .catch(() => {
+ this.handleError();
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ selectDefaultStage() {
+ const stage = this.state.stages[0];
+ this.selectStage(stage);
+ },
+ selectStage(stage) {
+ if (this.isLoadingStage) return;
+ if (this.currentStage === stage) return;
+
+ if (!stage.isUserAllowed) {
+ this.store.setActiveStage(stage);
+ return;
+ }
+
+ this.isLoadingStage = true;
+ this.store.setStageEvents([], stage);
+ this.store.setActiveStage(stage);
+
+ this.service
+ .fetchStageData({
+ stage,
+ startDate: this.startDate,
+ projectIds: this.selectedProjectIds,
+ })
+ .then((response) => {
+ this.isEmptyStage = !response.events.length;
+ this.store.setStageEvents(response.events, stage);
+ })
+ .catch(() => {
+ this.isEmptyStage = true;
+ })
+ .finally(() => {
+ this.isLoadingStage = false;
+ });
+ },
+ dismissOverviewDialog() {
+ this.isOverviewDialogDismissed = true;
+ Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
+ },
+ },
+ dayRangeOptions: [7, 30, 90],
+ i18n: {
+ dropdownText: __('Last %{days} days'),
+ },
+};
+</script>
+<template>
+ <div class="cycle-analytics">
+ <gl-loading-icon v-if="isLoading" size="lg" />
+ <div v-else class="wrapper">
+ <div class="card">
+ <div class="card-header">{{ __('Recent Project Activity') }}</div>
+ <div class="d-flex justify-content-between">
+ <div v-for="item in state.summary" :key="item.title" class="flex-grow text-center">
+ <h3 class="header">{{ item.value }}</h3>
+ <p class="text">{{ item.title }}</p>
+ </div>
+ <div class="flex-grow align-self-center text-center">
+ <div class="js-ca-dropdown dropdown inline">
+ <button class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
+ <span class="dropdown-label">
+ <gl-sprintf :message="$options.i18n.dropdownText">
+ <template #days>{{ startDate }}</template>
+ </gl-sprintf>
+ <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" />
+ </span>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right">
+ <li v-for="days in $options.dayRangeOptions" :key="`day-range-${days}`">
+ <a href="#" @click.prevent="handleDateSelect(days)">
+ <gl-sprintf :message="$options.i18n.dropdownText">
+ <template #days>{{ days }}</template>
+ </gl-sprintf>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="stage-panel-container">
+ <div class="card stage-panel">
+ <div class="card-header border-bottom-0">
+ <nav class="col-headers">
+ <ul>
+ <li class="stage-header pl-5">
+ <span class="stage-name font-weight-bold">{{
+ s__('ProjectLifecycle|Stage')
+ }}</span>
+ <span
+ class="has-tooltip"
+ data-placement="top"
+ :title="__('The phase of the development lifecycle.')"
+ aria-hidden="true"
+ >
+ <gl-icon name="question-o" class="gl-text-gray-500" />
+ </span>
+ </li>
+ <li class="median-header">
+ <span class="stage-name font-weight-bold">{{ __('Median') }}</span>
+ <span
+ class="has-tooltip"
+ data-placement="top"
+ :title="
+ __(
+ 'The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.',
+ )
+ "
+ aria-hidden="true"
+ >
+ <gl-icon name="question-o" class="gl-text-gray-500" />
+ </span>
+ </li>
+ <li class="event-header pl-3">
+ <span
+ v-if="currentStage && currentStage.legend"
+ class="stage-name font-weight-bold"
+ >{{ currentStage ? __(currentStage.legend) : __('Related Issues') }}</span
+ >
+ <span
+ class="has-tooltip"
+ data-placement="top"
+ :title="
+ __('The collection of events added to the data gathered for that stage.')
+ "
+ aria-hidden="true"
+ >
+ <gl-icon name="question-o" class="gl-text-gray-500" />
+ </span>
+ </li>
+ <li class="total-time-header pr-5 text-right">
+ <span class="stage-name font-weight-bold">{{ __('Time') }}</span>
+ <span
+ class="has-tooltip"
+ data-placement="top"
+ :title="__('The time taken by each data entry gathered by that stage.')"
+ aria-hidden="true"
+ >
+ <gl-icon name="question-o" class="gl-text-gray-500" />
+ </span>
+ </li>
+ </ul>
+ </nav>
+ </div>
+
+ <div class="stage-panel-body">
+ <nav class="stage-nav">
+ <ul>
+ <stage-nav-item
+ v-for="stage in state.stages"
+ :key="stage.title"
+ :title="stage.title"
+ :is-user-allowed="stage.isUserAllowed"
+ :value="stage.value"
+ :is-active="stage.active"
+ @select="selectStage(stage)"
+ />
+ </ul>
+ </nav>
+ <section class="stage-events overflow-auto">
+ <gl-loading-icon v-show="isLoadingStage" size="lg" />
+ <template v-if="currentStage && !currentStage.isUserAllowed">
+ <gl-empty-state
+ class="js-empty-state"
+ :title="__('You need permission.')"
+ :svg-path="noAccessSvgPath"
+ :description="__('Want to see the data? Please ask an administrator for access.')"
+ />
+ </template>
+ <template v-else>
+ <template v-if="currentStage && isEmptyStage && !isLoadingStage">
+ <gl-empty-state
+ class="js-empty-state"
+ :description="currentStage.emptyStageText"
+ :svg-path="noDataSvgPath"
+ :title="__('We don\'t have enough data to show this stage.')"
+ />
+ </template>
+ <template v-if="state.events.length && !isLoadingStage && !isEmptyStage">
+ <component
+ :is="currentStage.component"
+ :stage="currentStage"
+ :items="state.events"
+ />
+ </template>
+ </template>
+ </section>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
deleted file mode 100644
index 847820c965f..00000000000
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ /dev/null
@@ -1,156 +0,0 @@
-// This is a true violation of @gitlab/no-runtime-template-compiler, as it
-// relies on app/views/projects/cycle_analytics/show.html.haml for its
-// template.
-/* eslint-disable @gitlab/no-runtime-template-compiler */
-import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
-import $ from 'jquery';
-import Cookies from 'js-cookie';
-import Vue from 'vue';
-import { __ } from '~/locale';
-import { deprecatedCreateFlash as Flash } from '../flash';
-import Translate from '../vue_shared/translate';
-import banner from './components/banner.vue';
-import stageCodeComponent from './components/stage_code_component.vue';
-import stageComponent from './components/stage_component.vue';
-import stageNavItem from './components/stage_nav_item.vue';
-import stageReviewComponent from './components/stage_review_component.vue';
-import stageStagingComponent from './components/stage_staging_component.vue';
-import stageTestComponent from './components/stage_test_component.vue';
-import CycleAnalyticsService from './cycle_analytics_service';
-import CycleAnalyticsStore from './cycle_analytics_store';
-
-Vue.use(Translate);
-
-export default () => {
- const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
- const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
-
- // eslint-disable-next-line no-new
- new Vue({
- el: '#cycle-analytics',
- name: 'CycleAnalytics',
- components: {
- GlEmptyState,
- GlLoadingIcon,
- banner,
- 'stage-issue-component': stageComponent,
- 'stage-plan-component': stageComponent,
- 'stage-code-component': stageCodeComponent,
- 'stage-test-component': stageTestComponent,
- 'stage-review-component': stageReviewComponent,
- 'stage-staging-component': stageStagingComponent,
- 'stage-production-component': stageComponent,
- 'stage-nav-item': stageNavItem,
- },
- data() {
- return {
- store: CycleAnalyticsStore,
- state: CycleAnalyticsStore.state,
- isLoading: false,
- isLoadingStage: false,
- isEmptyStage: false,
- hasError: false,
- startDate: 30,
- isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
- service: this.createCycleAnalyticsService(cycleAnalyticsEl.dataset.requestPath),
- };
- },
- computed: {
- currentStage() {
- return this.store.currentActiveStage();
- },
- },
- created() {
- // Conditional check placed here to prevent this method from being called on the
- // new Value Stream Analytics page (i.e. the new page will be initialized blank and only
- // after a group is selected the cycle analyitcs data will be fetched). Once the
- // old (current) page has been removed this entire created method as well as the
- // variable itself can be completely removed.
- // Follow up issue: https://gitlab.com/gitlab-org/gitlab-foss/issues/64490
- if (cycleAnalyticsEl.dataset.requestPath) this.fetchCycleAnalyticsData();
- },
- methods: {
- handleError() {
- this.store.setErrorState(true);
- return new Flash(__('There was an error while fetching value stream analytics data.'));
- },
- initDropdown() {
- const $dropdown = $('.js-ca-dropdown');
- const $label = $dropdown.find('.dropdown-label');
-
- // eslint-disable-next-line @gitlab/no-global-event-off
- $dropdown
- .find('li a')
- .off('click')
- .on('click', (e) => {
- e.preventDefault();
- const $target = $(e.currentTarget);
- this.startDate = $target.data('value');
-
- $label.text($target.text().trim());
- this.fetchCycleAnalyticsData({ startDate: this.startDate });
- });
- },
- fetchCycleAnalyticsData(options) {
- const fetchOptions = options || { startDate: this.startDate };
-
- this.isLoading = true;
-
- this.service
- .fetchCycleAnalyticsData(fetchOptions)
- .then((response) => {
- this.store.setCycleAnalyticsData(response);
- this.selectDefaultStage();
- this.initDropdown();
- this.isLoading = false;
- })
- .catch(() => {
- this.handleError();
- this.isLoading = false;
- });
- },
- selectDefaultStage() {
- const stage = this.state.stages[0];
- this.selectStage(stage);
- },
- selectStage(stage) {
- if (this.isLoadingStage) return;
- if (this.currentStage === stage) return;
-
- if (!stage.isUserAllowed) {
- this.store.setActiveStage(stage);
- return;
- }
-
- this.isLoadingStage = true;
- this.store.setStageEvents([], stage);
- this.store.setActiveStage(stage);
-
- this.service
- .fetchStageData({
- stage,
- startDate: this.startDate,
- projectIds: this.selectedProjectIds,
- })
- .then((response) => {
- this.isEmptyStage = !response.events.length;
- this.store.setStageEvents(response.events, stage);
- this.isLoadingStage = false;
- })
- .catch(() => {
- this.isEmptyStage = true;
- this.isLoadingStage = false;
- });
- },
- dismissOverviewDialog() {
- this.isOverviewDialogDismissed = true;
- Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
- },
- createCycleAnalyticsService(requestPath) {
- return new CycleAnalyticsService({
- requestPath,
- });
- },
- },
- });
-};
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
new file mode 100644
index 00000000000..42d6700fae1
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import Translate from '../vue_shared/translate';
+import CycleAnalytics from './components/base.vue';
+import CycleAnalyticsService from './cycle_analytics_service';
+import CycleAnalyticsStore from './cycle_analytics_store';
+
+Vue.use(Translate);
+
+const createCycleAnalyticsService = (requestPath) =>
+ new CycleAnalyticsService({
+ requestPath,
+ });
+
+export default () => {
+ const el = document.querySelector('#js-cycle-analytics');
+ const { noAccessSvgPath, noDataSvgPath } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ name: 'CycleAnalytics',
+ render: (createElement) =>
+ createElement(CycleAnalytics, {
+ props: {
+ noDataSvgPath,
+ noAccessSvgPath,
+ store: CycleAnalyticsStore,
+ service: createCycleAnalyticsService(el.dataset.requestPath),
+ },
+ }),
+ });
+};