summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/cycle-analytics.js.es670
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss115
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml107
-rw-r--r--app/views/projects/pipelines/_head.html.haml5
-rw-r--r--app/views/shared/icons/_icon_cycle_analytics_splash.svg1
8 files changed, 255 insertions, 52 deletions
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
- = "<Not enough data>"
-
- %li.list-group-item
- Plan:
- - if plan = @cycle_analytics.plan
- = distance_of_time_in_words plan
- - else
- = "<Not enough data>"
-
- %li.list-group-item
- Code:
- - if code = @cycle_analytics.code.presence
- = distance_of_time_in_words code
- - else
- = "<Not enough data>"
-
- %li.list-group-item
- Test:
- - if test = @cycle_analytics.test.presence
- = distance_of_time_in_words test
- - else
- = "<Not enough data>"
-
- %li.list-group-item
- Review:
- - if review = @cycle_analytics.review.presence
- = distance_of_time_in_words review
- - else
- = "<Not enough data>"
-
- %li.list-group-item
- Staging:
- - if staging = @cycle_analytics.staging.presence
- = distance_of_time_in_words staging
- - else
- = "<Not enough data>"
-
- %li.list-group-item
- Production:
- - if production = @cycle_analytics.production.presence
- = distance_of_time_in_words production
- - else
- = "<Not enough data>"
+= 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 @@
+<svg width="167" height="166" viewBox="261 77 167 166" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M18 84h34v9H18z"/><mask id="h" x="0" y="0" width="34" height="9" fill="#fff"><use xlink:href="#a"/></mask><path d="M26 95c0 4.97 4.03 9 9 9s9-4.03 9-9" id="b"/><mask id="i" x="0" y="0" width="18" height="9" fill="#fff"><use xlink:href="#b"/></mask><path d="M42 48l25.564 18.26C68.91 67.22 70 69.34 70 70.994v22.004c0 2.21-1.456 2.962-3.253 1.678L42 77V48z" id="c"/><mask id="j" x="0" y="0" width="28" height="47.303" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 48l25.564 18.26C26.91 67.22 28 69.34 28 70.994v22.004c0 2.21-1.456 2.962-3.253 1.678L0 77V48z" id="d"/><mask id="k" x="0" y="0" width="28" height="47.303" fill="#fff"><use xlink:href="#d"/></mask><path d="M52.994 86C59.156 79.382 62 67.656 62 49.725 62 19.235 38.032 1.18 38.032 1.18c-1.675-1.434-4.408-1.466-6.064 0C31.968 1.18 8 19.234 8 49.724 8 67.655 10.844 79.382 17.006 86h35.988z" id="e"/><mask id="l" x="0" y="0" width="54" height="85.908" fill="#fff"><use xlink:href="#e"/></mask><circle id="f" cx="35" cy="43" r="13"/><mask id="m" x="0" y="0" width="26" height="26" fill="#fff"><use xlink:href="#f"/></mask><circle id="g" cx="35" cy="43" r="8"/><mask id="n" x="0" y="0" width="16" height="16" fill="#fff"><use xlink:href="#g"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(262 77)"><g transform="translate(47 6)"><path d="M51 132c0 2.21 1.79 4 4 4s4-1.79 4-4M11 122c0 2.21 1.79 4 4 4s4-1.79 4-4" stroke="#E5E5E5" stroke-width="2" stroke-linecap="round"/><rect fill="#E5E5E5" x="50" y="107" width="2" height="6" rx="1"/><rect fill="#E5E5E5" x="50" y="117" width="2" height="6" rx="1"/><rect fill="#E5E5E5" x="34" y="107" width="2" height="6" rx="1"/><rect fill="#E5E5E5" x="42" y="107" width="2" height="6" rx="1"/><rect fill="#E5E5E5" x="34" y="117" width="2" height="6" rx="1"/><rect fill="#E5E5E5" x="34" y="127" width="2" height="6" rx="1"/><rect fill="#E5E5E5" x="42" y="117" width="2" height="30" rx="1"/><rect fill="#E5E5E5" x="26" y="107" width="2" height="27" rx="1"/><rect fill="#E5E5E5" x="50" y="127" width="2" height="6" rx="1"/><rect fill="#E5E5E5" x="18" y="107" width="2" height="6" rx="1"/><rect fill="#E5E5E5" x="18" y="117" width="2" height="6" rx="1"/><use stroke="#E5E5E5" mask="url(#h)" stroke-width="4" xlink:href="#a"/><use stroke="#E5E5E5" mask="url(#i)" stroke-width="4" xlink:href="#b"/><use stroke="#E5E5E5" mask="url(#j)" stroke-width="4" xlink:href="#c"/><use stroke="#E5E5E5" mask="url(#k)" stroke-width="4" transform="matrix(-1 0 0 1 28 0)" xlink:href="#d"/><use stroke="#E5E5E5" mask="url(#l)" stroke-width="4" fill="#FFF" xlink:href="#e"/><use stroke="#E5E5E5" mask="url(#m)" stroke-width="4" xlink:href="#f"/><use stroke="#E5E5E5" mask="url(#n)" stroke-width="4" xlink:href="#g"/><path d="M43 146c0 3.59 2.91 6.5 6.5 6.5s6.5-2.91 6.5-6.5M14 133c0 3.59 2.91 6.5 6.5 6.5s6.5-2.91 6.5-6.5" stroke="#E5E5E5" stroke-width="2" stroke-linecap="round"/></g><circle stroke="#E5E5E5" stroke-width="2" cx="138.5" cy="115.5" r="6.5"/><circle stroke="#E5E5E5" stroke-width="2" cx="33.5" cy="28.5" r="6.5"/><circle stroke="#E5E5E5" stroke-width="2" cx="142" cy="52" r="4"/><circle stroke="#E5E5E5" stroke-width="2" cx="4" cy="83" r="4"/></g></svg> \ No newline at end of file