From 798b17a35311d60fe18440bfc53dba3aadd7b099 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 15 Sep 2016 00:03:42 +0300 Subject: [PATCH] Implement Cycle Analytics frontend. --- app/assets/javascripts/cycle-analytics.js.es6 | 70 +++++++++++ app/assets/javascripts/dispatcher.js | 3 + .../stylesheets/pages/cycle_analytics.scss | 115 ++++++++++++++++++ app/helpers/gitlab_routing_helper.rb | 4 + app/views/layouts/nav/_project.html.haml | 2 +- .../projects/cycle_analytics/show.html.haml | 107 ++++++++-------- app/views/projects/pipelines/_head.html.haml | 5 + .../icons/_icon_cycle_analytics_splash.svg | 1 + 8 files changed, 255 insertions(+), 52 deletions(-) create mode 100644 app/assets/javascripts/cycle-analytics.js.es6 create mode 100644 app/assets/stylesheets/pages/cycle_analytics.scss create mode 100644 app/views/shared/icons/_icon_cycle_analytics_splash.svg diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle-analytics.js.es6 new file mode 100644 index 00000000000..1bcd516c18a --- /dev/null +++ b/app/assets/javascripts/cycle-analytics.js.es6 @@ -0,0 +1,70 @@ +((global) => { + + gl.CycleAnalytics = class CycleAnalytics { + constructor() { + this.vue = new Vue({ + el: '#cycle-analytics', + name: 'CycleAnalytics', + created: this.fetchData(), + data: this.getData({ isLoading: true }) + }); + } + + 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; + }) + } + + 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 || '-' } + ] + } + } + } + + handleError(data) { + // TODO: Make sure that this is the proper error handling + new Flash('There was an error while fetching cycyle analytics data.', 'alert'); + } + + initDropdown() { + const $dropdown = $('.js-ca-dropdown'); + const $label = $dropdown.find('.dropdown-label'); + + $dropdown.find('li a').on('click', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + $label.text($target.text().trim()); + value = $target.data('value'); + + this.vue.isLoading = true; + }) + } + + } + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 38cdc7b9fba..46734761e86 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -186,6 +186,9 @@ new gl.ProtectedBranchCreate(); new gl.ProtectedBranchEditList(); break; + case 'projects:cycle_analytics:show': + window.ca = new gl.CycleAnalytics(); + break; } switch (path.first()) { case 'admin': diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss new file mode 100644 index 00000000000..306212990e1 --- /dev/null +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -0,0 +1,115 @@ +#cycle-analytics { + margin-top: 24px; + + .panel { + + .content-block { + padding: 24px 0; + border-bottom: none; + position: relative; + } + + .column { + text-align: center; + + .header { + font-size: 30px; + line-height: 38px; + font-weight: normal; + margin: 0; + } + + .text { + color: $layout-link-gray; + } + } + + .dropdown { + position: relative; + top: 10px; + } + } + + .bordered-box { + border: 1px solid $border-color; + @include border-radius($border-radius-default); + position: relative; + } + + .content-list { + li { + padding: 18px $gl-padding $gl-padding; + } + + .col-md-10 { + span { + &:first-child { + line-height: 19px; + font-size: 15px; + font-weight: 600; + } + &:last-child { + color: #8C8C8C; + } + } + } + + .col-md-2 span { + line-height: 42px; + } + } + + .inner-content { + width: 450px; + text-align: center; + margin: 0 auto; + padding: 62px 0; + + .btn-block { + max-width: 130px; + margin: 0 auto; + } + + h4 { + color: $gl-text-color; + font-size: 17px; + } + + p { + color: #8C8C8C; + 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; + left: 50%; + top: 50%; + margin: -16px 0 0 -16px; + } + } +} diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index a322a90cc4e..5b71113feb9 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -46,6 +46,10 @@ module GitlabRoutingHelper namespace_project_environments_path(project.namespace, project, *args) end + def project_cycle_analytics_path(project, *args) + namespace_project_cycle_analytics_path(project.namespace, project, *args) + end + def project_builds_path(project, *args) namespace_project_builds_path(project.namespace, project, *args) end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index f7012595a5a..1fb34841d87 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -47,7 +47,7 @@ Repository - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :environments]) do + = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span Pipelines diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 50ac9904445..4dd9847b42d 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,51 +1,56 @@ -%h2 Cycle Analytics from #{@cycle_analytics.from} to Today - -%ul.list-group - %li.list-group-item - Issue: - - if issue = @cycle_analytics.issue - = distance_of_time_in_words issue - - else - = "" - - %li.list-group-item - Plan: - - if plan = @cycle_analytics.plan - = distance_of_time_in_words plan - - else - = "" - - %li.list-group-item - Code: - - if code = @cycle_analytics.code.presence - = distance_of_time_in_words code - - else - = "" - - %li.list-group-item - Test: - - if test = @cycle_analytics.test.presence - = distance_of_time_in_words test - - else - = "" - - %li.list-group-item - Review: - - if review = @cycle_analytics.review.presence - = distance_of_time_in_words review - - else - = "" - - %li.list-group-item - Staging: - - if staging = @cycle_analytics.staging.presence - = distance_of_time_in_words staging - - else - = "" - - %li.list-group-item - Production: - - if production = @cycle_analytics.production.presence - = distance_of_time_in_words production - - else - = "" += render 'projects/pipelines/head' + +#cycle-analytics{"v-cloak" => "true", ":class" => "{ 'waiting': isLoading }"} + .panel.panel-default + .panel-heading + Pipeline Health + + .content-block + = icon("spinner spin", "v-if" => "isLoading") + + .row + %template{"v-for" => "info in analytics.summary"} + .col-md-3.column + %span.header {{info.value}} + %br + %span.text {{info.desc}} + + .col-md-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"} + .row + .col-md-10 + %span + {{info.title}} + %br + %span + {{info.desc}} + .col-md-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' diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index f611ddc8f5f..fa1470f5fbc 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -19,3 +19,8 @@ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do %span Environments + + = nav_link(controller: %w(cycle_analytics)) do + = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do + %span + Cycle Analytics diff --git a/app/views/shared/icons/_icon_cycle_analytics_splash.svg b/app/views/shared/icons/_icon_cycle_analytics_splash.svg new file mode 100644 index 00000000000..f7af1b6fea6 --- /dev/null +++ b/app/views/shared/icons/_icon_cycle_analytics_splash.svg @@ -0,0 +1 @@ + \ No newline at end of file -- GitLab