Commit 905ed5e9 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Kushal Pandya

FE for group level Cycle Analytics

This MR adds the code which displays cycle analytics on a group level.
It reuses the cycle_analytics_bundle file which will later be removed.

This feature is currently behind the analytics feature flag.
parent edb42a28
This diff is collapsed.
import $ from 'jquery';
import Vue from 'vue';
import Cookies from 'js-cookie';
import { GlEmptyState } from '@gitlab/ui';
import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins';
import Flash from '../flash';
import { __ } from '~/locale';
import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue';
......@@ -11,7 +14,6 @@ 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';
import { __ } from '~/locale';
Vue.use(Translate);
......@@ -24,6 +26,7 @@ export default () => {
el: '#cycle-analytics',
name: 'CycleAnalytics',
components: {
GlEmptyState,
banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stageComponent,
......@@ -32,12 +35,15 @@ export default () => {
'stage-review-component': stageReviewComponent,
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
GroupsDropdownFilter: () =>
import('ee_component/analytics/shared/components/groups_dropdown_filter.vue'),
ProjectsDropdownFilter: () =>
import('ee_component/analytics/shared/components/projects_dropdown_filter.vue'),
DateRangeDropdown: () =>
import('ee_component/analytics/shared/components/date_range_dropdown.vue'),
},
mixins: [filterMixins],
data() {
const cycleAnalyticsService = new CycleAnalyticsService({
requestPath: cycleAnalyticsEl.dataset.requestPath,
});
return {
store: CycleAnalyticsStore,
state: CycleAnalyticsStore.state,
......@@ -47,7 +53,7 @@ export default () => {
hasError: false,
startDate: 30,
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
service: cycleAnalyticsService,
service: this.createCycleAnalyticsService(cycleAnalyticsEl.dataset.requestPath),
};
},
computed: {
......@@ -124,6 +130,7 @@ export default () => {
.fetchStageData({
stage,
startDate: this.startDate,
projectIds: this.selectedProjectIds,
})
.then(response => {
this.isEmptyStage = !response.events.length;
......@@ -139,6 +146,11 @@ export default () => {
this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
},
createCycleAnalyticsService(requestPath) {
return new CycleAnalyticsService({
requestPath,
});
},
},
});
};
<script>
export default {
name: 'CycleAnalytics',
};
</script>
<template>
<div>Hello World!</div>
</template>
import Vue from 'vue';
import CycleAnalytics from './components/base.vue';
export default () => {
// eslint-disable-next-line no-new
new Vue({
el: '#js-cycle-analytics-app',
name: 'CycleAnalyticsApp',
components: {
CycleAnalytics,
},
render: createElement => createElement('cycle-analytics', {}),
});
};
export default {
data() {
return {
dateOptions: [7, 30, 90],
selectedGroup: null,
selectedProjectIds: [],
multiProjectSelect: true,
};
},
methods: {
renderSelectedGroup(selectedItemURL) {
this.service = this.createCycleAnalyticsService(selectedItemURL);
this.loadAnalyticsData();
},
setSelectedGroup(selectedGroup) {
this.selectedGroup = selectedGroup;
this.renderSelectedGroup(`/groups/${selectedGroup.path}/-/cycle_analytics`);
},
setSelectedProjects(selectedProjects) {
this.selectedProjectIds = selectedProjects.map(value => value.id);
this.loadAnalyticsData();
},
setSelectedDate(days) {
if (this.startDate !== days) {
this.startDate = days;
this.loadAnalyticsData();
}
},
loadAnalyticsData() {
this.fetchCycleAnalyticsData({
startDate: this.startDate,
projectIds: this.selectedProjectIds,
});
},
},
};
import initCycleAnalytics from '~/cycle_analytics/cycle_analytics_bundle';
import initCycleAnalyticsApp from 'ee/analytics/cycle_analytics/cycle_analytics_app';
import { parseBoolean } from '~/lib/utils/common_utils';
import Cookies from 'js-cookie';
if (parseBoolean(Cookies.get('cycle_analytics_app'))) {
document.addEventListener('DOMContentLoaded', initCycleAnalyticsApp);
} else {
document.addEventListener('DOMContentLoaded', initCycleAnalytics);
}
- page_title _('Cycle Analytics')
- if cookies[:cycle_analytics_app] == 'true'
#js-cycle-analytics-app
- else
.page-title-holder.d-flex.align-items-center
%h1.page-title= _('Cycle Analytics')
#cycle-analytics.m-0.mw-100
.mt-3.py-2.px-3.d-flex.bg-gray-light.border-top.border-bottom.flex-column.flex-md-row.justify-content-between
%groups-dropdown-filter.dropdown-select{ "@selected" => "setSelectedGroup" }
%projects-dropdown-filter.ml-md-1.mt-1.mt-md-0.dropdown-select{ "v-if" => "selectedGroup",
":group-id" => "selectedGroup.id",
":key" => "selectedGroup.id",
"@selected" => "setSelectedProjects",
":multi-select" => 'multiProjectSelect' } }
.ml-0.ml-md-auto.mt-2.mt-md-0.d-flex.flex-column.flex-md-row.align-items-md-center.justify-content-md-end{ "v-if" => "selectedGroup" }
%label.text-bold.mb-0.mr-1
{{ __('Timeframe') }}
%date-range-dropdown.js-timeframe-filter{ "@selected" => "setSelectedDate",
":available-days-in-past" => "dateOptions",
":default-selected" => "startDate" }
%gl-empty-state{ "v-show" => "!selectedGroup",
"title" => _("Cycle Analytics can help you determine your team’s velocity"),
"svg-path" => image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"),
"description" => _("Start by choosing a group to see how your team is spending time. You can then drill down to the project level.") }
.js-cycle-analytics{ "v-show" => "selectedGroup" }
.wrapper.mt-3
.card
.card-header.font-weight-bold
{{ __('Recent Activity') }}
.content-block
.container-fluid
.row
.col-sm-2
.col-sm-4.col-12.column{ "v-for" => "item in state.summary" }
%h3.header {{ item.value }}
%p.text {{ item.title }}
.stage-panel-container
.card.stage-panel
.card-header
%nav.col-headers
%ul
%li.stage-header
%span.stage-name
{{ s__('ProjectLifecycle|Stage') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" }
%li.median-header
%span.stage-name
{{ __('Median') }}
%i.has-tooltip.fa.fa-question-circle{ "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" }
%li.event-header
%span.stage-name
{{ currentStage ? __(currentStage.legend) : __('Related Issues') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" }
%li.total-time-header
%span.stage-name
{{ __('Total Time') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" }
.stage-panel-body
%nav.stage-nav
%ul
%li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" }
.stage-nav-item-cell.stage-name
{{ stage.title }}
.stage-nav-item-cell.stage-median
%template{ "v-if" => "stage.isUserAllowed" }
%span{ "v-if" => "stage.value" }
{{ stage.value }}
%span.stage-empty{ "v-else" => true }
{{ __('Not enough data') }}
%template{ "v-else" => true }
%span.not-available
{{ __('Not available') }}
.section.stage-events
%template{ "v-if" => "isLoadingStage" }
= icon("spinner spin")
%template{ "v-if" => "currentStage && !currentStage.isUserAllowed" }
= render partial: "projects/cycle_analytics/no_access"
%template{ "v-else" => true }
%template{ "v-if" => "isEmptyStage && !isLoadingStage" }
= render partial: "projects/cycle_analytics/empty_stage"
%template{ "v-if" => "state.events.length && !isLoadingStage && !isEmptyStage" }
%component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.events" }
---
title: Add cycle analytics on a group level - FE
merge_request: 14891
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe 'Group Cycle Analytics', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, group: group) }
before do
group.add_owner(user)
project.add_maintainer(user)
sign_in(user)
visit analytics_cycle_analytics_path
end
it 'displays an empty state before a group is selected' do
element = page.find('.row.empty-state')
expect(element).to have_content("Cycle Analytics can help you determine your team’s velocity")
expect(element.find('.svg-content img')['src']).to have_content('illustrations/analytics/cycle-analytics-empty-chart')
end
context 'displays correct fields after group selection' do
before do
dropdown = page.find('.dropdown-groups')
dropdown.click
dropdown.find('a').click
end
it 'hides the empty state' do
expect(page).to have_selector('.row.empty-state', visible: false)
end
it 'shows the projects filter' do
expect(page).to have_selector('.dropdown-projects', visible: true)
end
it 'shows the date filter' do
expect(page).to have_selector('.js-timeframe-filter', visible: true)
end
end
end
......@@ -4360,6 +4360,9 @@ msgstr ""
msgid "Cycle Analytics"
msgstr ""
msgid "Cycle Analytics can help you determine your team’s velocity"
msgstr ""
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr ""
......@@ -11860,6 +11863,9 @@ msgstr ""
msgid "Recent"
msgstr ""
msgid "Recent Activity"
msgstr ""
msgid "Recent Project Activity"
msgstr ""
......@@ -13775,6 +13781,9 @@ msgstr ""
msgid "Start and due date"
msgstr ""
msgid "Start by choosing a group to see how your team is spending time. You can then drill down to the project level."
msgstr ""
msgid "Start cleanup"
msgstr ""
......@@ -15187,6 +15196,9 @@ msgstr ""
msgid "Timeago|right now"
msgstr ""
msgid "Timeframe"
msgstr ""
msgid "Timeout"
msgstr ""
......
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