Finalise cycle analytics frontend.

This commit is contained in:
Fatih Acet 2016-09-19 23:41:36 +03:00
parent 3f3bdeced1
commit e49c6f8666
5 changed files with 165 additions and 140 deletions

View file

@ -1,19 +1,36 @@
((global) => {
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
gl.CycleAnalytics = class CycleAnalytics {
constructor() {
const that = this;
this.isHelpDismissed = $.cookie(COOKIE_NAME);
this.vue = new Vue({
el: '#cycle-analytics',
name: 'CycleAnalytics',
created: this.fetchData(),
data: this.getData({ isLoading: true })
data: this.decorateData({ isLoading: true }),
methods: {
dismissLanding() {
that.dismissLanding();
}
}
});
}
fetchData() {
$.get('cycle_analytics.json')
.done((data) => {
this.vue.$data = this.getData(data);
fetchData(options) {
options = options || { startDate: 30 };
$.ajax({
url: $('#cycle-analytics').data('request-path'),
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: { start_date: options.startDate }
}).done((data) => {
this.vue.$data = this.decorateData(data);
this.initDropdown();
})
.error((data) => {
@ -24,46 +41,52 @@
})
}
getData(data) {
return {
notAvailable: data.notAvailable || false,
isLoading: data.isLoading || false,
analytics: {
summary: [
{ desc: 'New Issues', value: data.issues || '-' },
{ desc: 'Commits', value: data.commits || '-' },
{ desc: 'Deploys', value: data.deploys || '-' }
],
data: [
{ title: 'Issue', desc: 'Time before an issue get scheduled', value: data.issue || '-' },
{ title: 'Plan', desc: 'Time before an issue starts implementation', value: data.plan || '-' },
{ title: 'Code', desc: 'Time until first merge request', value: data.code || '-' },
{ title: 'Test', desc: 'CI test time of the default branch', value: data.test || '-' },
{ title: 'Review', desc: 'Time between MR creation and merge/close', value: data.review || '-' },
{ title: 'Deploy', desc: 'Time for a new commit to land in one of the environments', value: data.deploy || '-' }
]
}
}
decorateData(data) {
data.summary = data.summary || [];
data.stats = data.stats || [];
data.isHelpDismissed = this.isHelpDismissed;
data.isLoading = data.isLoading || false;
data.summary.forEach((item) => {
item.value = item.value || '-';
});
data.stats.forEach((item) => {
item.value = item.value || '-';
})
return data;
}
handleError(data) {
// TODO: Make sure that this is the proper error handling
new Flash('There was an error while fetching cycyle analytics data.', 'alert');
this.vue.$data = {
hasError: true,
isHelpDismissed: this.isHelpDismissed
};
new Flash('There was an error while fetching cycle analytics data.', 'alert');
}
dismissLanding() {
this.vue.isHelpDismissed = true;
$.cookie(COOKIE_NAME, true);
}
initDropdown() {
const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label');
$dropdown.find('li a').on('click', (e) => {
$dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
const value = $target.data('value');
$label.text($target.text().trim());
this.vue.isLoading = true;
this.fetchData({ startDate: value });
})
}
}
})(window.gl || (window.gl = {}));

View file

@ -92,7 +92,7 @@
new MergedButtons();
break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
new MergeConflictResolver()
case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation();
Issuable.init();
@ -187,7 +187,7 @@
new gl.ProtectedBranchEditList();
break;
case 'projects:cycle_analytics:show':
window.ca = new gl.CycleAnalytics();
new gl.CycleAnalytics();
break;
}
switch (path.first()) {

View file

@ -1,5 +1,7 @@
#cycle-analytics {
margin-top: 24px;
margin: 24px auto 0;
width: 800px;
position: relative;
.panel {
@ -22,6 +24,10 @@
.text {
color: $layout-link-gray;
}
&:last-child {
text-align: right;
}
}
.dropdown {
@ -39,9 +45,13 @@
.content-list {
li {
padding: 18px $gl-padding $gl-padding;
.container-fluid {
padding: 0;
}
}
.col-md-10 {
.title-col {
span {
&:first-child {
line-height: 19px;
@ -54,21 +64,35 @@
}
}
.col-md-2 span {
.value-col {
text-align: right;
span {
line-height: 42px;
}
}
}
.landing {
margin-bottom: $gl-padding;
overflow: hidden;
.dismiss-icon {
position: absolute;
right: $gl-padding;
cursor: pointer;
}
svg {
margin: 0 20px;
float: left;
width: 136px;
height: 136px;
}
.inner-content {
width: 450px;
text-align: center;
margin: 0 auto;
padding: 62px 0;
.btn-block {
max-width: 130px;
margin: 0 auto;
}
width: 480px;
float: left;
h4 {
color: $gl-text-color;
@ -80,36 +104,14 @@
margin-bottom: $gl-padding;
}
}
&.waiting {
.panel .header {
width: 35px;
height: 35px;
margin-bottom: 3px;
}
span {
background-color: #F8F8F8;
color: #F8F8F8 !important;
display: inline-block;
line-height: 13px !important;
}
.dropdown {
opacity: .33;
}
.col-md-2 span {
position: relative;
top: 11px;
}
.fa-spinner {
font-size: 32px;
position: absolute;
font-size: 28px;
position: relative;
margin-left: -20px;
left: 50%;
top: 50%;
margin: -16px 0 0 -16px;
}
margin-top: 36px;
}
}

View file

@ -6,8 +6,8 @@ module CycleAnalyticsHelper
[:plan, "Plan", "Time before an issue starts implementation"],
[:code, "Code", "Time until first merge request"],
[:test, "Test", "Total test time for all commits/merges"],
[:review, "Review", "Time between MR creation and merge/close"],
[:staging, "Staging", "From MR merge until deploy to production"],
[:review, "Review", "Time between merge request creation and merge/close"],
[:staging, "Staging", "From merge request merge until deploy to production"],
[:production, "Production", "From issue creation until deploy to production"]]
stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|

View file

@ -1,58 +1,58 @@
= render 'projects/pipelines/head'
#cycle-analytics{"v-cloak" => "true", ":class" => "{ 'waiting': isLoading }"}
#cycle-analytics{"v-cloak" => "true", data: { request_path: "#{project_cycle_analytics_path(@project)}"}}
.bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
= icon('times', class: 'dismiss-icon', "@click": "dismissLanding()")
= custom_icon('icon_cycle_analytics_splash')
.inner-content
%h4
Introducing Cycle Analytics
%p
Cycle Analytics gives an overview on how much time it takes to go from idea to production in your project.
= button_tag 'Read more', class: 'btn'
= icon("spinner spin", "v-show" => "isLoading")
.wrapper{"v-show" => "!isLoading && !hasError"}
.panel.panel-default
.panel-heading
Pipeline Health
.content-block
= icon("spinner spin", "v-if" => "isLoading")
.container-fluid
.row
%template{"v-for" => "info in analytics.summary"}
%template{"v-for" => "item in summary"}
.col-xs-3.column
%span.header {{info.value}}
%span.header {{item.value}}
%br
%span.text {{info.desc}}
%span.text {{item.title}}
.col-xs-3.column
.dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{"aria-expanded" => "false", "data-toggle" => "dropdown", :type => "button"}
%button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
%span.dropdown-label Last 30 days
%i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right
%li
%a{'href' => "#", 'data-value' => '30days'}
%a{'href' => "#", 'data-value' => '30'}
Last 30 days
%li
%a{'href' => "#", 'data-value' => '90days'}
%a{'href' => "#", 'data-value' => '90'}
Last 90 days
.bordered-box
= icon("spinner spin", "v-if" => "isLoading")
%ul.content-list{{"v-if" => "!notAvailable"}}
%li{"v-for" => "info in analytics.data"}
%ul.content-list
%li{"v-for" => "item in stats"}
.container-fluid
.row
.col-xs-10
.col-xs-10.title-col
%span
{{info.title}}
{{item.title}}
%br
%span
{{info.desc}}
.col-xs-2
{{item.description}}
.col-xs-2.value-col
%span
{{info.value}}
.content-block{{"v-if" => "notAvailable"}}
.inner-content
= custom_icon('icon_cycle_analytics_splash')
%h4
Set up your deploys to environment!
%p
Cycle Analytics will give an overview on how much time it takes to go from an idea to production in your project.
= button_tag 'Set up', class: 'btn btn-create btn-block'
{{item.value}}