Commit e49c6f86 authored by Fatih Acet's avatar Fatih Acet

Finalise cycle analytics frontend.

parent 3f3bdece
((global) => { ((global) => {
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
gl.CycleAnalytics = class CycleAnalytics { gl.CycleAnalytics = class CycleAnalytics {
constructor() { constructor() {
const that = this;
this.isHelpDismissed = $.cookie(COOKIE_NAME);
this.vue = new Vue({ this.vue = new Vue({
el: '#cycle-analytics', el: '#cycle-analytics',
name: 'CycleAnalytics', name: 'CycleAnalytics',
created: this.fetchData(), created: this.fetchData(),
data: this.getData({ isLoading: true }) data: this.decorateData({ isLoading: true }),
methods: {
dismissLanding() {
that.dismissLanding();
}
}
}); });
} }
fetchData() { fetchData(options) {
$.get('cycle_analytics.json') options = options || { startDate: 30 };
.done((data) => {
this.vue.$data = this.getData(data); $.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(); this.initDropdown();
}) })
.error((data) => { .error((data) => {
...@@ -24,46 +41,52 @@ ...@@ -24,46 +41,52 @@
}) })
} }
getData(data) { decorateData(data) {
return { data.summary = data.summary || [];
notAvailable: data.notAvailable || false, data.stats = data.stats || [];
isLoading: data.isLoading || false, data.isHelpDismissed = this.isHelpDismissed;
analytics: { data.isLoading = data.isLoading || false;
summary: [
{ desc: 'New Issues', value: data.issues || '-' }, data.summary.forEach((item) => {
{ desc: 'Commits', value: data.commits || '-' }, item.value = item.value || '-';
{ desc: 'Deploys', value: data.deploys || '-' } });
],
data: [ data.stats.forEach((item) => {
{ title: 'Issue', desc: 'Time before an issue get scheduled', value: data.issue || '-' }, item.value = item.value || '-';
{ 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 || '-' }, return data;
{ 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 || '-' }
]
}
}
} }
handleError(data) { handleError(data) {
// TODO: Make sure that this is the proper error handling this.vue.$data = {
new Flash('There was an error while fetching cycyle analytics data.', 'alert'); 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() { initDropdown() {
const $dropdown = $('.js-ca-dropdown'); const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label'); const $label = $dropdown.find('.dropdown-label');
$dropdown.find('li a').on('click', (e) => { $dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault(); e.preventDefault();
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const value = $target.data('value'); const value = $target.data('value');
$label.text($target.text().trim()); $label.text($target.text().trim());
this.vue.isLoading = true; this.vue.isLoading = true;
this.fetchData({ startDate: value });
}) })
} }
} }
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
new MergedButtons(); new MergedButtons();
break; break;
case "projects:merge_requests:conflicts": case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver() new MergeConflictResolver()
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
Issuable.init(); Issuable.init();
...@@ -187,7 +187,7 @@ ...@@ -187,7 +187,7 @@
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
break; break;
case 'projects:cycle_analytics:show': case 'projects:cycle_analytics:show':
window.ca = new gl.CycleAnalytics(); new gl.CycleAnalytics();
break; break;
} }
switch (path.first()) { switch (path.first()) {
......
#cycle-analytics { #cycle-analytics {
margin-top: 24px; margin: 24px auto 0;
width: 800px;
position: relative;
.panel { .panel {
...@@ -22,6 +24,10 @@ ...@@ -22,6 +24,10 @@
.text { .text {
color: $layout-link-gray; color: $layout-link-gray;
} }
&:last-child {
text-align: right;
}
} }
.dropdown { .dropdown {
...@@ -39,9 +45,13 @@ ...@@ -39,9 +45,13 @@
.content-list { .content-list {
li { li {
padding: 18px $gl-padding $gl-padding; padding: 18px $gl-padding $gl-padding;
.container-fluid {
padding: 0;
}
} }
.col-md-10 { .title-col {
span { span {
&:first-child { &:first-child {
line-height: 19px; line-height: 19px;
...@@ -54,22 +64,36 @@ ...@@ -54,22 +64,36 @@
} }
} }
.col-md-2 span { .value-col {
text-align: right;
span {
line-height: 42px; line-height: 42px;
} }
} }
}
.inner-content { .landing {
width: 450px; margin-bottom: $gl-padding;
text-align: center; overflow: hidden;
margin: 0 auto;
padding: 62px 0;
.btn-block { .dismiss-icon {
max-width: 130px; position: absolute;
margin: 0 auto; right: $gl-padding;
cursor: pointer;
}
svg {
margin: 0 20px;
float: left;
width: 136px;
height: 136px;
} }
.inner-content {
width: 480px;
float: left;
h4 { h4 {
color: $gl-text-color; color: $gl-text-color;
font-size: 17px; font-size: 17px;
...@@ -80,36 +104,14 @@ ...@@ -80,36 +104,14 @@
margin-bottom: $gl-padding; 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 { .fa-spinner {
font-size: 32px; font-size: 28px;
position: absolute; position: relative;
margin-left: -20px;
left: 50%; left: 50%;
top: 50%; margin-top: 36px;
margin: -16px 0 0 -16px;
}
} }
} }
...@@ -6,8 +6,8 @@ module CycleAnalyticsHelper ...@@ -6,8 +6,8 @@ module CycleAnalyticsHelper
[:plan, "Plan", "Time before an issue starts implementation"], [:plan, "Plan", "Time before an issue starts implementation"],
[:code, "Code", "Time until first merge request"], [:code, "Code", "Time until first merge request"],
[:test, "Test", "Total test time for all commits/merges"], [:test, "Test", "Total test time for all commits/merges"],
[:review, "Review", "Time between MR creation and merge/close"], [:review, "Review", "Time between merge request creation and merge/close"],
[:staging, "Staging", "From MR merge until deploy to production"], [:staging, "Staging", "From merge request merge until deploy to production"],
[:production, "Production", "From issue creation 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)| stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
......
= render 'projects/pipelines/head' = 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.panel-default
.panel-heading .panel-heading
Pipeline Health Pipeline Health
.content-block .content-block
= icon("spinner spin", "v-if" => "isLoading")
.container-fluid .container-fluid
.row .row
%template{"v-for" => "info in analytics.summary"} %template{"v-for" => "item in summary"}
.col-xs-3.column .col-xs-3.column
%span.header {{info.value}} %span.header {{item.value}}
%br %br
%span.text {{info.desc}} %span.text {{item.title}}
.col-xs-3.column .col-xs-3.column
.dropdown.inline.js-ca-dropdown .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 %span.dropdown-label Last 30 days
%i.fa.fa-chevron-down %i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
%a{'href' => "#", 'data-value' => '30days'} %a{'href' => "#", 'data-value' => '30'}
Last 30 days Last 30 days
%li %li
%a{'href' => "#", 'data-value' => '90days'} %a{'href' => "#", 'data-value' => '90'}
Last 90 days Last 90 days
.bordered-box .bordered-box
= icon("spinner spin", "v-if" => "isLoading") %ul.content-list
%li{"v-for" => "item in stats"}
%ul.content-list{{"v-if" => "!notAvailable"}}
%li{"v-for" => "info in analytics.data"}
.container-fluid .container-fluid
.row .row
.col-xs-10 .col-xs-10.title-col
%span %span
{{info.title}} {{item.title}}
%br %br
%span %span
{{info.desc}} {{item.description}}
.col-xs-2 .col-xs-2.value-col
%span %span
{{info.value}} {{item.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'
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