Finalise cycle analytics frontend.
This commit is contained in:
parent
3f3bdeced1
commit
e49c6f8666
5 changed files with 165 additions and 140 deletions
|
@ -1,19 +1,36 @@
|
||||||
((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 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 @@
|
||||||
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 @@
|
||||||
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()) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#cycle-analytics {
|
#cycle-analytics {
|
||||||
margin-top: 24px;
|
margin: 24px auto 0;
|
||||||
|
width: 800px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
|
|
||||||
|
@ -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 @@
|
||||||
.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,21 +64,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-md-2 span {
|
.value-col {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
span {
|
||||||
line-height: 42px;
|
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 {
|
.inner-content {
|
||||||
width: 450px;
|
width: 480px;
|
||||||
text-align: center;
|
float: left;
|
||||||
margin: 0 auto;
|
|
||||||
padding: 62px 0;
|
|
||||||
|
|
||||||
.btn-block {
|
|
||||||
max-width: 130px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
color: $gl-text-color;
|
color: $gl-text-color;
|
||||||
|
@ -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
|
||||||
[: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)|
|
||||||
|
|
|
@ -1,58 +1,58 @@
|
||||||
= 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'
|
|
||||||
|
|
Loading…
Reference in a new issue