diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle-analytics.js.es6 index 727ae12b4fc5dbeac40ea3524399b36f6442de90..acae5439fa29098f85d102f16cef3931e0853780 100644 --- a/app/assets/javascripts/cycle-analytics.js.es6 +++ b/app/assets/javascripts/cycle-analytics.js.es6 @@ -1,69 +1,92 @@ ((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); - this.initDropdown(); - }) - .error((data) => { - this.handleError(data); - }) - .always(() => { - this.vue.isLoading = false; - }) + 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) => { + this.handleError(data); + }) + .always(() => { + this.vue.isLoading = false; + }) } - 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 = {})); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 46734761e8642b2aa141ff982b02acfac9735ce5..6e620e9976c1c52d890b20795c7c168c152e168f 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -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()) { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 16a9b0eebfcbfcfc96af64be30b6aeece75732d8..4bbd942112ac3f53fd519a6493ddffe5ad68f30e 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -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,62 +64,54 @@ } } - .col-md-2 span { - line-height: 42px; - } - } - - .inner-content { - width: 450px; - text-align: center; - margin: 0 auto; - padding: 62px 0; + .value-col { + text-align: right; - .btn-block { - max-width: 130px; - margin: 0 auto; + span { + line-height: 42px; + } } + } - h4 { - color: $gl-text-color; - font-size: 17px; - } + .landing { + margin-bottom: $gl-padding; + overflow: hidden; - p { - color: #8C8C8C; - margin-bottom: $gl-padding; + .dismiss-icon { + position: absolute; + right: $gl-padding; + cursor: pointer; } - } - &.waiting { - .panel .header { - width: 35px; - height: 35px; - margin-bottom: 3px; + svg { + margin: 0 20px; + float: left; + width: 136px; + height: 136px; } - span { - background-color: #F8F8F8; - color: #F8F8F8 !important; - display: inline-block; - line-height: 13px !important; - } + .inner-content { + width: 480px; + float: left; - .dropdown { - opacity: .33; - } + h4 { + color: $gl-text-color; + font-size: 17px; + } - .col-md-2 span { - position: relative; - top: 11px; + p { + color: #8C8C8C; + margin-bottom: $gl-padding; + } } + } - .fa-spinner { - font-size: 32px; - position: absolute; - left: 50%; - top: 50%; - margin: -16px 0 0 -16px; - } + .fa-spinner { + font-size: 28px; + position: relative; + margin-left: -20px; + left: 50%; + margin-top: 36px; } + } diff --git a/app/helpers/cycle_analytics_helper.rb b/app/helpers/cycle_analytics_helper.rb index c0d1eb56fd319c83ec297dc7247d4420ef739e12..5caf7254c5c1268f622d264c042f098c8245b887 100644 --- a/app/helpers/cycle_analytics_helper.rb +++ b/app/helpers/cycle_analytics_helper.rb @@ -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)| diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 33c403118c1419d490a860995a5c106f70fc2ec3..b9439014fb5edb8ea8f77a6d4e900147e4aacd97 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,58 +1,58 @@ = render 'projects/pipelines/head' -#cycle-analytics{"v-cloak" => "true", ":class" => "{ 'waiting': isLoading }"} - .panel.panel-default - .panel-heading - Pipeline Health +#cycle-analytics{"v-cloak" => "true", data: { request_path: "#{project_cycle_analytics_path(@project)}"}} - .content-block - = icon("spinner spin", "v-if" => "isLoading") + .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. - .container-fluid - .row - %template{"v-for" => "info in analytics.summary"} - .col-xs-3.column - %span.header {{info.value}} - %br - %span.text {{info.desc}} - - .col-xs-3.column - .dropdown.inline.js-ca-dropdown - %button.dropdown-menu-toggle{"aria-expanded" => "false", "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'} - Last 30 days - %li - %a{'href' => "#", 'data-value' => '90days'} - Last 90 days - - .bordered-box - = icon("spinner spin", "v-if" => "isLoading") - - %ul.content-list{{"v-if" => "!notAvailable"}} - %li{"v-for" => "info in analytics.data"} + = 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 .container-fluid .row - .col-xs-10 - %span - {{info.title}} - %br - %span - {{info.desc}} - .col-xs-2 - %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' + %template{"v-for" => "item in summary"} + .col-xs-3.column + %span.header {{item.value}} + %br + %span.text {{item.title}} + + .col-xs-3.column + .dropdown.inline.js-ca-dropdown + %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' => '30'} + Last 30 days + %li + %a{'href' => "#", 'data-value' => '90'} + Last 90 days + + .bordered-box + %ul.content-list + %li{"v-for" => "item in stats"} + .container-fluid + .row + .col-xs-10.title-col + %span + {{item.title}} + %br + %span + {{item.description}} + .col-xs-2.value-col + %span + {{item.value}}