Commit 56f34933 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Migrate template content to vue

Moves the content contained in the the
show.html.haml for project cycle analytics
to the base.vue component
parent 4aba7a0f
<script>
import { GlIcon, GlEmptyState, GlLoadingIcon } 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,
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: false,
startDate: 30,
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
};
},
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
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 });
},
},
};
</script>
<template>
<!-- TODO: add error message if hasError = true -->
<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">
{{ n__('Last %d day', 'Last %d days', 30) }}
<gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" />
</span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="#" data-value="7">{{ n__('Last %d day', 'Last %d days', 7) }}</a>
</li>
<li>
<a href="#" data-value="30">{{ n__('Last %d day', 'Last %d days', 30) }}</a>
</li>
<li>
<a href="#" data-value="90">{{ n__('Last %d day', 'Last %d days', 90) }}</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="`ca-stage-title-${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>
// 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 Vue from 'vue';
import { __ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '../flash';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import banner from './components/banner.vue'; import CycleAnalytics from './components/base.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 CycleAnalyticsService from './cycle_analytics_service';
import CycleAnalyticsStore from './cycle_analytics_store'; import CycleAnalyticsStore from './cycle_analytics_store';
Vue.use(Translate); Vue.use(Translate);
const createCycleAnalyticsService = (requestPath) =>
new CycleAnalyticsService({
requestPath,
});
export default () => { export default () => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
const cycleAnalyticsEl = document.querySelector('#cycle-analytics'); const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
const { noAccessSvgPath, noDataSvgPath } = cycleAnalyticsEl.dataset; const { noAccessSvgPath, noDataSvgPath } = cycleAnalyticsEl.dataset;
...@@ -30,130 +19,14 @@ export default () => { ...@@ -30,130 +19,14 @@ export default () => {
new Vue({ new Vue({
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
components: { render: (createElement) =>
GlEmptyState, createElement(CycleAnalytics, {
GlLoadingIcon, props: {
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),
noAccessSvgPath,
noDataSvgPath, noDataSvgPath,
}; noAccessSvgPath,
}, store: CycleAnalyticsStore,
computed: { service: createCycleAnalyticsService(cycleAnalyticsEl.dataset.requestPath),
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,
});
},
}, },
}),
}); });
}; };
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment