diff --git a/app/assets/images/i2p-step.svg b/app/assets/images/i2p-step.svg new file mode 100644 index 00000000000..8886092ed82 --- /dev/null +++ b/app/assets/images/i2p-step.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 53b25da18e5..baa20d0c34a 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -393,6 +393,9 @@ import ShortcutsBlob from './shortcuts_blob'; case 'users:show': new UserCallout(); break; + case 'admin:conversational_development_index:show': + new UserCallout(); + break; case 'snippets:show': new LineHighlighter(); new BlobViewer(); diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index b9d57cbcad4..ff2208baeab 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,11 +1,10 @@ import Cookies from 'js-cookie'; -const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; - export default class UserCallout { - constructor() { - this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); - this.userCalloutBody = $('.user-callout'); + constructor(className = 'user-callout') { + this.userCalloutBody = $(`.${className}`); + this.cookieName = this.userCalloutBody.data('uid'); + this.isCalloutDismissed = Cookies.get(this.cookieName); this.init(); } @@ -18,7 +17,7 @@ export default class UserCallout { dismissCallout(e) { const $currentTarget = $(e.currentTarget); - Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 }); + Cookies.set(this.cookieName, 'true', { expires: 365 }); if ($currentTarget.hasClass('close')) { this.userCalloutBody.remove(); diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 975a4b40383..b3a86b92d93 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -569,3 +569,10 @@ $filter-value-selected-color: #d7d7d7; Animation Functions */ $dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1); + +/* +Convdev Index +*/ +$color-high-score: $green-400; +$color-average-score: $orange-400; +$color-low-score: $red-400; diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss new file mode 100644 index 00000000000..0413114c279 --- /dev/null +++ b/app/assets/stylesheets/pages/convdev_index.scss @@ -0,0 +1,255 @@ +$space-between-cards: 8px; + +.convdev-empty svg { + margin: 64px auto 32px; + max-width: 420px; +} + +.convdev-header { + margin-top: $gl-padding; + margin-bottom: $gl-padding; + padding: 0 4px; + display: flex; + align-items: center; + + .convdev-header-title { + font-size: 48px; + line-height: 1; + margin: 0; + } + + .convdev-header-subtitle { + font-size: 22px; + line-height: 1; + color: $gl-text-color-secondary; + margin-left: 8px; + font-weight: 500; + + a { + font-size: 18px; + color: $gl-text-color-secondary; + + &:hover { + color: $blue-500; + } + } + } +} + +.convdev-cards { + display: flex; + justify-content: center; + flex-wrap: wrap; +} + +.convdev-card-wrapper { + display: flex; + flex-direction: column; + align-items: stretch; + text-align: center; + width: 50%; + border-color: $border-color; + margin: 0 0 32px; + padding: $space-between-cards / 2; + position: relative; + + @media (min-width: $screen-xs-min) { + width: percentage(1 / 4); + } + + @media (min-width: $screen-sm-min) { + width: percentage(1 / 5); + } + + @media (min-width: $screen-md-min) { + width: percentage(1 / 6); + } + + @media (min-width: $screen-lg-min) { + width: percentage(1 / 10); + } +} + +.convdev-card { + border: solid 1px $border-color; + border-radius: 3px; + border-top-width: 3px; + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.convdev-card-low { + border-top-color: $color-low-score; + + .card-score-big { + background-color: $red-25; + } +} + +.convdev-card-average { + border-top-color: $color-average-score; + + .card-score-big { + background-color: $orange-25; + } +} + +.convdev-card-high { + border-top-color: $color-high-score; + + .card-score-big { + background-color: $green-25; + } +} + +.convdev-card-title { + margin: $gl-padding auto auto; + max-width: 100px; + + h3 { + font-size: 14px; + margin: 0 0 2px; + } + + .text-light { + font-size: 13px; + line-height: 1.25; + color: $gl-text-color-secondary; + } +} + +.card-scores { + display: flex; + justify-content: space-around; + align-items: center; + margin: $gl-padding $gl-btn-padding; + line-height: 1; +} + +.card-score { + color: $gl-text-color-secondary; + + .card-score-name { + font-size: 13px; + margin-top: 4px; + } +} + +.card-score-value { + font-size: 16px; + color: $gl-text-color; + font-weight: 500; +} + +.card-score-big { + border-top: 2px solid $border-color; + border-bottom: 1px solid $border-color; + font-size: 22px; + padding: 10px 0; + font-weight: 500; +} + +.card-buttons { + display: flex; + + > * { + font-size: 16px; + color: $gl-text-color-secondary; + padding: 10px; + flex-grow: 1; + + &:hover { + background-color: $border-color; + color: $gl-text-color; + } + + + * { + border-left: solid 1px $border-color; + } + } +} + +.convdev-steps { + margin-top: $gl-padding; + height: 1px; + min-width: 100%; + justify-content: space-around; + position: relative; + background: $border-color; +} + +.convdev-step { + $step-positions: 5% 10% 30% 42% 48% 55% 60% 70% 75% 90%; + @each $pos in $step-positions { + $i: index($step-positions, $pos); + + &:nth-child(#{$i}) { + left: $pos; + } + } + + position: absolute; + transform-origin: 75% 50%; + padding: 8px; + height: 50px; + width: 50px; + border-radius: 3px; + display: flex; + flex-direction: column; + align-items: center; + border: solid 1px $border-color; + background: $white-light; + transform: translate(-50%, -50%); + color: $gl-text-color-secondary; + fill: $gl-text-color-secondary; + box-shadow: 0 2px 4px $dropdown-shadow-color; + + &:hover { + padding: 8px 10px; + fill: currentColor; + z-index: 100; + height: auto; + width: auto; + + .convdev-step-title { + max-height: 2em; + opacity: 1; + transition: opacity 0.2s; + } + + svg { + transform: scale(1.5); + margin: $gl-btn-padding; + } + } + + svg { + transition: transform 0.1s; + width: 30px; + height: 30px; + min-height: 30px; + min-width: 30px; + } +} + +.convdev-step-title { + max-height: 0; + opacity: 0; + text-transform: uppercase; + margin: $gl-vert-padding 0 0; + text-align: center; + font-size: 12px; +} + +.convdev-high-score { + color: $color-high-score; +} + +.convdev-average-score { + color: $color-average-score; +} + +.convdev-low-score { + color: $color-low-score; +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index fe084eb9397..c207159f606 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -287,6 +287,7 @@ table.u2f-registrations { .user-callout { margin: 0 auto; + max-width: $screen-lg-min; .bordered-box { border: 1px solid $blue-300; @@ -295,14 +296,15 @@ table.u2f-registrations { position: relative; display: flex; justify-content: center; + align-items: center; } .landing { - margin-top: $gl-padding; - margin-bottom: $gl-padding; + padding: 32px; .close { position: absolute; + top: 20px; right: 20px; opacity: 1; @@ -330,11 +332,20 @@ table.u2f-registrations { height: 110px; vertical-align: top; } + + &.convdev { + margin: 0 0 0 30px; + + svg { + height: 127px; + } + } } .user-callout-copy { display: inline-block; vertical-align: top; + max-width: 570px; } } @@ -348,12 +359,20 @@ table.u2f-registrations { .landing { .svg-container, .user-callout-copy { - margin: 0; + margin: 0 auto; display: block; svg { height: 75px; } + + &.convdev { + margin: $gl-padding auto 0; + + svg { + height: 120px; + } + } } } } diff --git a/app/controllers/admin/conversational_development_index_controller.rb b/app/controllers/admin/conversational_development_index_controller.rb new file mode 100644 index 00000000000..921169d3e2b --- /dev/null +++ b/app/controllers/admin/conversational_development_index_controller.rb @@ -0,0 +1,5 @@ +class Admin::ConversationalDevelopmentIndexController < Admin::ApplicationController + def show + @metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 36d9090b3ae..f422c48329c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -275,8 +275,8 @@ module ApplicationHelper 'active' if condition end - def show_user_callout? - cookies[:user_callout_dismissed].nil? + def show_callout?(name) + cookies[name] != 'true' end def linkedin_url(user) diff --git a/app/helpers/conversational_development_index_helper.rb b/app/helpers/conversational_development_index_helper.rb new file mode 100644 index 00000000000..1ff54415811 --- /dev/null +++ b/app/helpers/conversational_development_index_helper.rb @@ -0,0 +1,16 @@ +module ConversationalDevelopmentIndexHelper + def score_level(score) + if score < 33.33 + 'low' + elsif score < 66.66 + 'average' + else + 'high' + end + end + + def format_score(score) + precision = score < 1 ? 2 : 1 + number_with_precision(score, precision: precision) + end +end diff --git a/app/models/conversational_development_index/card.rb b/app/models/conversational_development_index/card.rb new file mode 100644 index 00000000000..e8f09dc9161 --- /dev/null +++ b/app/models/conversational_development_index/card.rb @@ -0,0 +1,26 @@ +module ConversationalDevelopmentIndex + class Card + attr_accessor :metric, :title, :description, :feature, :blog, :docs + + def initialize(metric:, title:, description:, feature:, blog:, docs: nil) + self.metric = metric + self.title = title + self.description = description + self.feature = feature + self.blog = blog + self.docs = docs + end + + def instance_score + metric.instance_score(feature) + end + + def leader_score + metric.leader_score(feature) + end + + def percentage_score + metric.percentage_score(feature) + end + end +end diff --git a/app/models/conversational_development_index/idea_to_production_step.rb b/app/models/conversational_development_index/idea_to_production_step.rb new file mode 100644 index 00000000000..6e1753c9f30 --- /dev/null +++ b/app/models/conversational_development_index/idea_to_production_step.rb @@ -0,0 +1,19 @@ +module ConversationalDevelopmentIndex + class IdeaToProductionStep + attr_accessor :metric, :title, :features + + def initialize(metric:, title:, features:) + self.metric = metric + self.title = title + self.features = features + end + + def percentage_score + sum = features.sum do |feature| + metric.percentage_score(feature) + end + + sum / features.size.to_f + end + end +end diff --git a/app/models/conversational_development_index/metric.rb b/app/models/conversational_development_index/metric.rb new file mode 100644 index 00000000000..f42f516f99a --- /dev/null +++ b/app/models/conversational_development_index/metric.rb @@ -0,0 +1,21 @@ +module ConversationalDevelopmentIndex + class Metric < ActiveRecord::Base + include Presentable + + self.table_name = 'conversational_development_index_metrics' + + def instance_score(feature) + self["instance_#{feature}"] + end + + def leader_score(feature) + self["leader_#{feature}"] + end + + def percentage_score(feature) + return 100 if leader_score(feature).zero? + + 100 * instance_score(feature) / leader_score(feature) + end + end +end diff --git a/app/presenters/conversational_development_index/metric_presenter.rb b/app/presenters/conversational_development_index/metric_presenter.rb new file mode 100644 index 00000000000..bb65ba2646b --- /dev/null +++ b/app/presenters/conversational_development_index/metric_presenter.rb @@ -0,0 +1,144 @@ +module ConversationalDevelopmentIndex + class MetricPresenter < Gitlab::View::Presenter::Simple + def cards + [ + Card.new( + metric: subject, + title: 'Issues', + description: 'created per active user', + feature: 'issues', + blog: 'https://www2.deloitte.com/content/dam/Deloitte/se/Documents/technology-media-telecommunications/deloitte-digital-collaboration.pdf' + ), + Card.new( + metric: subject, + title: 'Comments', + description: 'created per active user', + feature: 'notes', + blog: 'http://conversationaldevelopment.com/why/' + ), + Card.new( + metric: subject, + title: 'Milestones', + description: 'created per active user', + feature: 'milestones', + blog: 'http://conversationaldevelopment.com/shorten-cycle/', + docs: help_page_path('user/project/milestones/index') + ), + Card.new( + metric: subject, + title: 'Boards', + description: 'created per active user', + feature: 'boards', + blog: 'http://jpattonassociates.com/user-story-mapping/', + docs: help_page_path('user/project/issue_board') + ), + Card.new( + metric: subject, + title: 'Merge Requests', + description: 'per active user', + feature: 'merge_requests', + blog: 'https://8thlight.com/blog/uncle-bob/2013/02/01/The-Humble-Craftsman.html', + docs: help_page_path('user/project/merge_requests/index') + ), + Card.new( + metric: subject, + title: 'Pipelines', + description: 'created per active user', + feature: 'ci_pipelines', + blog: 'https://martinfowler.com/bliki/ContinuousDelivery.html', + docs: help_page_path('ci/README') + ), + Card.new( + metric: subject, + title: 'Environments', + description: 'created per active user', + feature: 'environments', + blog: 'https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/', + docs: help_page_path('ci/environments') + ), + Card.new( + metric: subject, + title: 'Deployments', + description: 'created per active user', + feature: 'deployments', + blog: 'https://puppet.com/blog/continuous-delivery-vs-continuous-deployment-what-s-diff' + ), + Card.new( + metric: subject, + title: 'Monitoring', + description: 'fraction of all projects', + feature: 'projects_prometheus_active', + blog: 'https://prometheus.io/docs/introduction/overview/', + docs: help_page_path('user/project/integrations/prometheus') + ), + Card.new( + metric: subject, + title: 'Service Desk', + description: 'issues created per active user', + feature: 'service_desk_issues', + blog: 'http://blogs.forrester.com/kate_leggett/17-01-30-top_trends_for_customer_service_in_2017_operations_become_smarter_and_more_strategic', + docs: 'https://docs.gitlab.com/ee/user/project/service_desk.html' + ) + ] + end + + def idea_to_production_steps + [ + IdeaToProductionStep.new( + metric: subject, + title: 'Idea', + features: %w(issues) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Issue', + features: %w(issues notes) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Plan', + features: %w(milestones boards) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Code', + features: %w(merge_requests) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Commit', + features: %w(merge_requests) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Test', + features: %w(ci_pipelines) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Review', + features: %w(ci_pipelines environments) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Staging', + features: %w(environments deployments) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Production', + features: %w(deployments) + ), + IdeaToProductionStep.new( + metric: subject, + title: 'Feedback', + features: %w(projects_prometheus_active service_desk_issues) + ) + ] + end + + def average_percentage_score + cards.sum(&:percentage_score) / cards.size.to_f + end + end +end diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb new file mode 100644 index 00000000000..17857ca62f2 --- /dev/null +++ b/app/services/submit_usage_ping_service.rb @@ -0,0 +1,41 @@ +class SubmitUsagePingService + URL = 'https://version.gitlab.com/usage_data'.freeze + + include Gitlab::CurrentSettings + + def execute + return false unless current_application_settings.usage_ping_enabled? + + response = HTTParty.post( + URL, + body: Gitlab::UsageData.to_json(force_refresh: true), + headers: { 'Content-type' => 'application/json' } + ) + + store_metrics(response) + + true + rescue HTTParty::Error => e + Rails.logger.info "Unable to contact GitLab, Inc.: #{e}" + + false + end + + private + + def store_metrics(response) + return unless response['conv_index'].present? + + ConversationalDevelopmentIndex::Metric.create!( + response['conv_index'].slice( + 'leader_issues', 'instance_issues', 'leader_notes', 'instance_notes', + 'leader_milestones', 'instance_milestones', 'leader_boards', 'instance_boards', + 'leader_merge_requests', 'instance_merge_requests', 'leader_ci_pipelines', + 'instance_ci_pipelines', 'leader_environments', 'instance_environments', + 'leader_deployments', 'instance_deployments', 'leader_projects_prometheus_active', + 'instance_projects_prometheus_active', 'leader_service_desk_issues', + 'instance_service_desk_issues' + ) + ) + end +end diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index ac36bb5bb17..e5842bd1ea0 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "Background Jobs" -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } %h3.page-title Background Jobs diff --git a/app/views/admin/conversational_development_index/_callout.html.haml b/app/views/admin/conversational_development_index/_callout.html.haml new file mode 100644 index 00000000000..33a4dab1e00 --- /dev/null +++ b/app/views/admin/conversational_development_index/_callout.html.haml @@ -0,0 +1,13 @@ +.prepend-top-default +.user-callout{ data: { uid: 'convdev_intro_callout_dismissed' } } + .bordered-box.landing.content-block + %button.btn.btn-default.close.js-close-callout{ type: 'button', + 'aria-label' => 'Dismiss ConvDev introduction' } + = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') + .user-callout-copy + %h4 + Introducing Your Conversational Development Index + %p + Your Conversational Development Index gives an overview of how you are using GitLab from a feature perspective. View how you compare with other organizations, discover features you are not using, and learn best practices through blog posts and white papers. + .svg-container.convdev + = custom_icon('convdev_overview') diff --git a/app/views/admin/conversational_development_index/_card.html.haml b/app/views/admin/conversational_development_index/_card.html.haml new file mode 100644 index 00000000000..6c8688e06ae --- /dev/null +++ b/app/views/admin/conversational_development_index/_card.html.haml @@ -0,0 +1,25 @@ +.convdev-card-wrapper + .convdev-card{ class: "convdev-card-#{score_level(card.percentage_score)}" } + .convdev-card-title + %h3 + = card.title + .text-light + = card.description + .card-scores + .card-score + .card-score-value + = format_score(card.instance_score) + .card-score-name You + .card-score + .card-score-value + = format_score(card.leader_score) + .card-score-name Lead + .card-score-big + = number_to_percentage(card.percentage_score, precision: 1) + .card-buttons + - if card.blog + %a{ href: card.blog } + = icon('info-circle', 'aria-hidden' => 'true') + - if card.docs + %a{ href: card.docs } + = icon('question-circle', 'aria-hidden' => 'true') diff --git a/app/views/admin/conversational_development_index/_disabled.html.haml b/app/views/admin/conversational_development_index/_disabled.html.haml new file mode 100644 index 00000000000..975d7df3da6 --- /dev/null +++ b/app/views/admin/conversational_development_index/_disabled.html.haml @@ -0,0 +1,9 @@ +.container.convdev-empty + .col-sm-6.col-sm-push-3.text-center + = custom_icon('convdev_no_index') + %h4 Usage ping is not enabled + %p + ConvDev is only shown when the + = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics'), target: '_blank' + is enabled. Enable usage ping to get an overview of how you are using GitLab from a feature perspective + = link_to 'Enable usage ping', admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' diff --git a/app/views/admin/conversational_development_index/_no_data.html.haml b/app/views/admin/conversational_development_index/_no_data.html.haml new file mode 100644 index 00000000000..b23d2b5ec3a --- /dev/null +++ b/app/views/admin/conversational_development_index/_no_data.html.haml @@ -0,0 +1,7 @@ +.container.convdev-empty + .col-sm-6.col-sm-push-3.text-center + = custom_icon('convdev_no_data') + %h4 Data is still calculating... + %p + In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index. + = link_to 'Learn more', help_page_path('user/admin_area/monitoring/convdev'), target: '_blank' diff --git a/app/views/admin/conversational_development_index/show.html.haml b/app/views/admin/conversational_development_index/show.html.haml new file mode 100644 index 00000000000..833d4c612f8 --- /dev/null +++ b/app/views/admin/conversational_development_index/show.html.haml @@ -0,0 +1,35 @@ +- @no_container = true +- page_title 'ConvDev Index' + += render 'admin/monitoring/head' + +.container + - if show_callout?('convdev_intro_callout_dismissed') + = render 'callout' + + .prepend-top-default + - if !current_application_settings.usage_ping_enabled + = render 'disabled' + - elsif @metric.blank? + = render 'no_data' + - else + .convdev + .convdev-header + %h2.convdev-header-title{ class: "convdev-#{score_level(@metric.average_percentage_score)}-score" } + = number_to_percentage(@metric.average_percentage_score, precision: 1) + .convdev-header-subtitle + index + %br + score + = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/admin_area/monitoring/convdev') + + .convdev-cards.card-container + - @metric.cards.each do |card| + = render 'card', card: card + + .convdev-steps.visible-lg + - @metric.idea_to_production_steps.each_with_index do |step, index| + .convdev-step{ class: "convdev-#{score_level(step.percentage_score)}-score" } + = custom_icon("i2p_step_#{index + 1}") + %h4.convdev-step-title + = step.title diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 4deccf4aa93..8adb966064c 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "Health Check" -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } %h3.page-title diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 5e585ce789b..487f1cf5c4f 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -3,7 +3,7 @@ - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, Gitlab::EnvironmentLogger, Gitlab::SidekiqLogger, Gitlab::RepositoryCheckLogger] -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } %ul.nav-links.log-tabs diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/monitoring/_head.html.haml similarity index 82% rename from app/views/admin/background_jobs/_head.html.haml rename to app/views/admin/monitoring/_head.html.haml index b3530915068..901e30275fd 100644 --- a/app/views/admin/background_jobs/_head.html.haml +++ b/app/views/admin/monitoring/_head.html.haml @@ -3,6 +3,10 @@ = render 'shared/nav_scroll' .nav-links.sub-nav.scrolling-tabs %ul{ class: (container_class) } + = nav_link(controller: :conversational_development_index) do + = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do + %span + ConvDev Index = nav_link(controller: :system_info) do = link_to admin_system_info_path, title: 'System Info' do %span diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml index c7b63d9de98..b7db18b2d32 100644 --- a/app/views/admin/requests_profiles/index.html.haml +++ b/app/views/admin/requests_profiles/index.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title 'Requests Profiles' -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } %h3.page-title diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml index 9b9559c7fe5..fd0281e4961 100644 --- a/app/views/admin/system_info/show.html.haml +++ b/app/views/admin/system_info/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "System Info" -= render 'admin/background_jobs/head' += render 'admin/monitoring/head' %div{ class: container_class } .prepend-top-default diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index 2890ae7173b..5e63a61e21b 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -9,7 +9,7 @@ = render "projects/last_push" %div{ class: container_class } - - if show_user_callout? + - if show_callout?('user_callout_dismissed') = render 'shared/user_callout' - if @projects.any? || params[:name] diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 86779eeaf15..6df0adfd742 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -9,8 +9,8 @@ = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do %span Overview - = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do - = link_to admin_system_info_path, title: 'Monitoring' do + = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do + = link_to admin_conversational_development_index_path, title: 'Monitoring' do %span Monitoring = nav_link(controller: :broadcast_messages) do diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml index 8308baa7829..17ffcba69d8 100644 --- a/app/views/shared/_user_callout.html.haml +++ b/app/views/shared/_user_callout.html.haml @@ -1,4 +1,4 @@ -.user-callout +.user-callout{ data: { uid: 'user_callout_dismissed' } } .bordered-box.landing.content-block %button.btn.btn-default.close.js-close-callout{ type: 'button', 'aria-label' => 'Dismiss customize experience box' } diff --git a/app/views/shared/icons/_convdev_no_data.svg b/app/views/shared/icons/_convdev_no_data.svg new file mode 100644 index 00000000000..ed32b2333e7 --- /dev/null +++ b/app/views/shared/icons/_convdev_no_data.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/views/shared/icons/_convdev_no_index.svg b/app/views/shared/icons/_convdev_no_index.svg new file mode 100644 index 00000000000..95c00e81d10 --- /dev/null +++ b/app/views/shared/icons/_convdev_no_index.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/views/shared/icons/_convdev_overview.svg b/app/views/shared/icons/_convdev_overview.svg new file mode 100644 index 00000000000..2f31113bad7 --- /dev/null +++ b/app/views/shared/icons/_convdev_overview.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/views/shared/icons/_i2p_step_1.svg b/app/views/shared/icons/_i2p_step_1.svg new file mode 100644 index 00000000000..9dedcd5291a --- /dev/null +++ b/app/views/shared/icons/_i2p_step_1.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/views/shared/icons/_i2p_step_10.svg b/app/views/shared/icons/_i2p_step_10.svg new file mode 100644 index 00000000000..dd6fd1457ff --- /dev/null +++ b/app/views/shared/icons/_i2p_step_10.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/views/shared/icons/_i2p_step_2.svg b/app/views/shared/icons/_i2p_step_2.svg new file mode 100644 index 00000000000..b8805b90275 --- /dev/null +++ b/app/views/shared/icons/_i2p_step_2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/views/shared/icons/_i2p_step_3.svg b/app/views/shared/icons/_i2p_step_3.svg new file mode 100644 index 00000000000..6c783ed8289 --- /dev/null +++ b/app/views/shared/icons/_i2p_step_3.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/views/shared/icons/_i2p_step_4.svg b/app/views/shared/icons/_i2p_step_4.svg new file mode 100644 index 00000000000..af804c838e0 --- /dev/null +++ b/app/views/shared/icons/_i2p_step_4.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/views/shared/icons/_i2p_step_5.svg b/app/views/shared/icons/_i2p_step_5.svg new file mode 100644 index 00000000000..e54f707019e --- /dev/null +++ b/app/views/shared/icons/_i2p_step_5.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/views/shared/icons/_i2p_step_6.svg b/app/views/shared/icons/_i2p_step_6.svg new file mode 100644 index 00000000000..c57baccc06b --- /dev/null +++ b/app/views/shared/icons/_i2p_step_6.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/views/shared/icons/_i2p_step_7.svg b/app/views/shared/icons/_i2p_step_7.svg new file mode 100644 index 00000000000..e9083de3afa --- /dev/null +++ b/app/views/shared/icons/_i2p_step_7.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/views/shared/icons/_i2p_step_8.svg b/app/views/shared/icons/_i2p_step_8.svg new file mode 100644 index 00000000000..62676b0e12e --- /dev/null +++ b/app/views/shared/icons/_i2p_step_8.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/views/shared/icons/_i2p_step_9.svg b/app/views/shared/icons/_i2p_step_9.svg new file mode 100644 index 00000000000..e4285a14425 --- /dev/null +++ b/app/views/shared/icons/_i2p_step_9.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c239253c8d5..f246bd7a586 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -100,7 +100,7 @@ Snippets %div{ class: container_class } - - if @user == current_user && show_user_callout? + - if @user == current_user && show_callout?('user_callout_dismissed') = render 'shared/user_callout' .tab-content #activity.tab-pane diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb index 2f02235b0ac..0a55aab63fd 100644 --- a/app/workers/gitlab_usage_ping_worker.rb +++ b/app/workers/gitlab_usage_ping_worker.rb @@ -3,29 +3,17 @@ class GitlabUsagePingWorker include Sidekiq::Worker include CronjobQueue - include HTTParty def perform - return unless current_application_settings.usage_ping_enabled - # Multiple Sidekiq workers could run this. We should only do this at most once a day. return unless try_obtain_lease - begin - HTTParty.post(url, - body: Gitlab::UsageData.to_json(force_refresh: true), - headers: { 'Content-type' => 'application/json' } - ) - rescue HTTParty::Error => e - Rails.logger.info "Unable to contact GitLab, Inc.: #{e}" - end + SubmitUsagePingService.new.execute end + private + def try_obtain_lease Gitlab::ExclusiveLease.new('gitlab_usage_ping_worker:ping', timeout: LEASE_TIMEOUT).try_obtain end - - def url - 'https://version.gitlab.com/usage_data' - end end diff --git a/changelogs/unreleased/30469-convdev-index.yml b/changelogs/unreleased/30469-convdev-index.yml new file mode 100644 index 00000000000..0bdd9c4a699 --- /dev/null +++ b/changelogs/unreleased/30469-convdev-index.yml @@ -0,0 +1,4 @@ +--- +title: Add ConvDev Index page to admin area +merge_request: 11377 +author: diff --git a/config/routes/admin.rb b/config/routes/admin.rb index ccfd85aed63..c7b639b7b3c 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -72,6 +72,8 @@ namespace :admin do resource :system_info, controller: 'system_info', only: [:show] resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } + get 'conversational_development_index' => 'conversational_development_index#show' + resources :projects, only: [:index] scope(path: 'projects/*namespace_id', diff --git a/db/fixtures/development/21_conversational_development_index_metrics.rb b/db/fixtures/development/21_conversational_development_index_metrics.rb new file mode 100644 index 00000000000..4cd0a82ed1a --- /dev/null +++ b/db/fixtures/development/21_conversational_development_index_metrics.rb @@ -0,0 +1,40 @@ +Gitlab::Seeder.quiet do + conversational_development_index_metric = ConversationalDevelopmentIndex::Metric.new( + leader_issues: 10.2, + instance_issues: 3.2, + + leader_notes: 25.3, + instance_notes: 23.2, + + leader_milestones: 16.2, + instance_milestones: 5.5, + + leader_boards: 5.2, + instance_boards: 3.2, + + leader_merge_requests: 5.2, + instance_merge_requests: 3.2, + + leader_ci_pipelines: 25.1, + instance_ci_pipelines: 21.3, + + leader_environments: 3.3, + instance_environments: 2.2, + + leader_deployments: 41.3, + instance_deployments: 15.2, + + leader_projects_prometheus_active: 0.31, + instance_projects_prometheus_active: 0.30, + + leader_service_desk_issues: 15.8, + instance_service_desk_issues: 15.1 + ) + + if conversational_development_index_metric.save + print '.' + else + puts conversational_development_index_metric.errors.full_messages + print 'F' + end +end diff --git a/db/migrate/20170523121229_create_conversational_development_index_metrics.rb b/db/migrate/20170523121229_create_conversational_development_index_metrics.rb new file mode 100644 index 00000000000..9f9ec526055 --- /dev/null +++ b/db/migrate/20170523121229_create_conversational_development_index_metrics.rb @@ -0,0 +1,39 @@ +class CreateConversationalDevelopmentIndexMetrics < ActiveRecord::Migration + DOWNTIME = false + + def change + create_table :conversational_development_index_metrics do |t| + t.float :leader_issues, null: false + t.float :instance_issues, null: false + + t.float :leader_notes, null: false + t.float :instance_notes, null: false + + t.float :leader_milestones, null: false + t.float :instance_milestones, null: false + + t.float :leader_boards, null: false + t.float :instance_boards, null: false + + t.float :leader_merge_requests, null: false + t.float :instance_merge_requests, null: false + + t.float :leader_ci_pipelines, null: false + t.float :instance_ci_pipelines, null: false + + t.float :leader_environments, null: false + t.float :instance_environments, null: false + + t.float :leader_deployments, null: false + t.float :instance_deployments, null: false + + t.float :leader_projects_prometheus_active, null: false + t.float :instance_projects_prometheus_active, null: false + + t.float :leader_service_desk_issues, null: false + t.float :instance_service_desk_issues, null: false + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fa1c5dc15c4..7966c732080 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -371,6 +371,31 @@ ActiveRecord::Schema.define(version: 20170525174156) do add_index "container_repositories", ["project_id", "name"], name: "index_container_repositories_on_project_id_and_name", unique: true, using: :btree add_index "container_repositories", ["project_id"], name: "index_container_repositories_on_project_id", using: :btree + create_table "conversational_development_index_metrics", force: :cascade do |t| + t.float "leader_issues", null: false + t.float "instance_issues", null: false + t.float "leader_notes", null: false + t.float "instance_notes", null: false + t.float "leader_milestones", null: false + t.float "instance_milestones", null: false + t.float "leader_boards", null: false + t.float "instance_boards", null: false + t.float "leader_merge_requests", null: false + t.float "instance_merge_requests", null: false + t.float "leader_ci_pipelines", null: false + t.float "instance_ci_pipelines", null: false + t.float "leader_environments", null: false + t.float "instance_environments", null: false + t.float "leader_deployments", null: false + t.float "instance_deployments", null: false + t.float "leader_projects_prometheus_active", null: false + t.float "instance_projects_prometheus_active", null: false + t.float "leader_service_desk_issues", null: false + t.float "instance_service_desk_issues", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false diff --git a/spec/factories/conversational_development_index_metrics.rb b/spec/factories/conversational_development_index_metrics.rb new file mode 100644 index 00000000000..a5412629195 --- /dev/null +++ b/spec/factories/conversational_development_index_metrics.rb @@ -0,0 +1,33 @@ +FactoryGirl.define do + factory :conversational_development_index_metric, class: ConversationalDevelopmentIndex::Metric do + leader_issues 9.256 + instance_issues 1.234 + + leader_notes 30.33333 + instance_notes 28.123 + + leader_milestones 16.2456 + instance_milestones 1.234 + + leader_boards 5.2123 + instance_boards 3.254 + + leader_merge_requests 1.2 + instance_merge_requests 0.6 + + leader_ci_pipelines 12.1234 + instance_ci_pipelines 2.344 + + leader_environments 3.3333 + instance_environments 2.2222 + + leader_deployments 1.200 + instance_deployments 0.771 + + leader_projects_prometheus_active 0.111 + instance_projects_prometheus_active 0.109 + + leader_service_desk_issues 15.891 + instance_service_desk_issues 13.345 + end +end diff --git a/spec/features/admin/admin_conversational_development_index_spec.rb b/spec/features/admin/admin_conversational_development_index_spec.rb new file mode 100644 index 00000000000..739ab907a29 --- /dev/null +++ b/spec/features/admin/admin_conversational_development_index_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'Admin Conversational Development Index' do + before do + login_as :admin + end + + context 'when usage ping is disabled' do + it 'shows empty state' do + stub_application_setting(usage_ping_enabled: false) + + visit admin_conversational_development_index_path + + expect(page).to have_content('Usage ping is not enabled') + end + end + + context 'when there is no data to display' do + it 'shows empty state' do + stub_application_setting(usage_ping_enabled: true) + + visit admin_conversational_development_index_path + + expect(page).to have_content('Data is still calculating') + end + end + + context 'when there is data to display' do + it 'shows numbers for each metric' do + stub_application_setting(usage_ping_enabled: true) + create(:conversational_development_index_metric) + + visit admin_conversational_development_index_path + + expect(page).to have_content( + 'Issues created per active user 1.2 You 9.3 Lead 13.3%' + ) + end + end +end diff --git a/spec/presenters/conversational_development_index/metric_presenter_spec.rb b/spec/presenters/conversational_development_index/metric_presenter_spec.rb new file mode 100644 index 00000000000..1e015c71f5b --- /dev/null +++ b/spec/presenters/conversational_development_index/metric_presenter_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe ConversationalDevelopmentIndex::MetricPresenter do + subject { described_class.new(metric) } + let(:metric) { build(:conversational_development_index_metric) } + + describe '#cards' do + it 'includes instance score, leader score and percentage score' do + issues_card = subject.cards.first + + expect(issues_card.instance_score).to eq 1.234 + expect(issues_card.leader_score).to eq 9.256 + expect(issues_card.percentage_score).to be_within(0.1).of(13.3) + end + end + + describe '#idea_to_production_steps' do + it 'returns percentage score when it depends on a single feature' do + code_step = subject.idea_to_production_steps.fourth + + expect(code_step.percentage_score).to be_within(0.1).of(50.0) + end + + it 'returns percentage score when it depends on two features' do + issue_step = subject.idea_to_production_steps.second + + expect(issue_step.percentage_score).to be_within(0.1).of(53.0) + end + end + + describe '#average_percentage_score' do + it 'calculates an average value across all the features' do + expect(subject.average_percentage_score).to be_within(0.1).of(55.8) + end + end +end diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb new file mode 100644 index 00000000000..63a1e78f274 --- /dev/null +++ b/spec/services/submit_usage_ping_service_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe SubmitUsagePingService do + context 'when usage ping is disabled' do + before do + stub_application_setting(usage_ping_enabled: false) + end + + it 'does not run' do + expect(HTTParty).not_to receive(:post) + + result = subject.execute + + expect(result).to eq false + end + end + + context 'when usage ping is enabled' do + before do + stub_application_setting(usage_ping_enabled: true) + end + + it 'sends a POST request' do + response = stub_response(without_conv_index_params) + + subject.execute + + expect(response).to have_been_requested + end + + it 'refreshes usage data statistics before submitting' do + stub_response(without_conv_index_params) + + expect(Gitlab::UsageData).to receive(:to_json) + .with(force_refresh: true) + .and_call_original + + subject.execute + end + + it 'saves conversational development index data from the response' do + stub_response(with_conv_index_params) + + expect { subject.execute } + .to change { ConversationalDevelopmentIndex::Metric.count } + .by(1) + + expect(ConversationalDevelopmentIndex::Metric.last.leader_issues).to eq 10.2 + end + end + + def without_conv_index_params + { + conv_index: {} + } + end + + def with_conv_index_params + { + conv_index: { + leader_issues: 10.2, + instance_issues: 3.2, + + leader_notes: 25.3, + instance_notes: 23.2, + + leader_milestones: 16.2, + instance_milestones: 5.5, + + leader_boards: 5.2, + instance_boards: 3.2, + + leader_merge_requests: 5.2, + instance_merge_requests: 3.2, + + leader_ci_pipelines: 25.1, + instance_ci_pipelines: 21.3, + + leader_environments: 3.3, + instance_environments: 2.2, + + leader_deployments: 41.3, + instance_deployments: 15.2, + + leader_projects_prometheus_active: 0.31, + instance_projects_prometheus_active: 0.30, + + leader_service_desk_issues: 15.8, + instance_service_desk_issues: 15.1 + } + } + end + + def stub_response(body) + stub_request(:post, 'https://version.gitlab.com/usage_data'). + to_return( + headers: { 'Content-Type' => 'application/json' }, + body: body.to_json + ) + end +end diff --git a/spec/workers/gitlab_usage_ping_worker_spec.rb b/spec/workers/gitlab_usage_ping_worker_spec.rb index 26241044533..49b4e04dc7c 100644 --- a/spec/workers/gitlab_usage_ping_worker_spec.rb +++ b/spec/workers/gitlab_usage_ping_worker_spec.rb @@ -3,21 +3,11 @@ require 'spec_helper' describe GitlabUsagePingWorker do subject { described_class.new } - it "sends POST request" do - stub_application_setting(usage_ping_enabled: true) + it 'delegates to SubmitUsagePingService' do + allow(subject).to receive(:try_obtain_lease).and_return(true) - stub_request(:post, "https://version.gitlab.com/usage_data"). - to_return(status: 200, body: '', headers: {}) - expect(Gitlab::UsageData).to receive(:to_json).with({ force_refresh: true }).and_call_original - expect(subject).to receive(:try_obtain_lease).and_return(true) + expect_any_instance_of(SubmitUsagePingService).to receive(:execute) - expect(subject.perform.response.code.to_i).to eq(200) - end - - it "does not run if usage ping is disabled" do - stub_application_setting(usage_ping_enabled: false) - - expect(subject).not_to receive(:try_obtain_lease) - expect(subject).not_to receive(:perform) + subject.perform end end