Skip to content
Snippets Groups Projects
Commit 26dde5f5 authored by Taurie Davis, Simon Knox and Adam Niedzielski's avatar Taurie Davis, Simon Knox and Adam Niedzielski Committed by Adam Niedzielski
Browse files

Add Conversational Development Index page to admin panel

parent c72abcef
No related branches found
No related tags found
No related merge requests found
Showing
with 677 additions and 12 deletions
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 120" enable-background="new 0 0 12 120">
<path d="m12 6c0-3.309-2.691-6-6-6s-6 2.691-6 6c0 2.967 2.167 5.431 5 5.91v108.09h2v-108.09c2.833-.479 5-2.943 5-5.91m-6 4c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4"/>
</svg>
Loading
Loading
@@ -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();
Loading
Loading
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();
}
 
Loading
Loading
@@ -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();
Loading
Loading
Loading
Loading
@@ -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;
$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;
@media (max-width: $screen-lg-min) {
flex-wrap: wrap;
}
}
.convdev-card-wrapper {
display: flex;
flex-direction: column;
align-items: stretch;
text-align: center;
width: 10%;
border-color: $border-color;
margin: 0 0 32px;
padding: $space-between-cards / 2;
position: relative;
@media (max-width: $screen-lg-min) {
width: 16.667%;
.convdev-card-title {
max-width: 100px;
margin: $gl-padding auto auto;
}
.card-scores {
margin: $gl-padding 24px;
}
}
@media (max-width: $screen-md-min) {
width: 20%;
}
@media (max-width: $screen-sm-min) {
width: 25%;
}
@media (max-width: $screen-xs-min) {
width: 50%;
}
}
.convdev-card {
border: solid 1px $border-color;
border-top-width: 3px;
border-top-left-radius: 3px;
border-top-right-radius: 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-top: $gl-padding;
margin-bottom: auto;
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;
justify-content: stretch;
> * {
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;
@media (max-width: $screen-lg-min) {
display: none;
}
}
.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;
}
}
.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;
}
Loading
Loading
@@ -287,6 +287,7 @@ table.u2f-registrations {
 
.user-callout {
margin: 0 auto;
max-width: $screen-lg-min;
 
.bordered-box {
border: 1px solid $blue-300;
Loading
Loading
@@ -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;
 
Loading
Loading
@@ -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;
}
}
 
Loading
Loading
@@ -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;
}
}
}
}
}
Loading
Loading
class Admin::ConversationalDevelopmentIndexController < Admin::ApplicationController
def show
@metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present
end
end
Loading
Loading
@@ -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)
Loading
Loading
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
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
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.map do |feature|
metric.percentage_score(feature)
end.inject(:+)
sum / features.size.to_f
end
end
end
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
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.map(&:percentage_score).inject(:+) / cards.size.to_f
end
end
end
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
- @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
Loading
Loading
.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 they compare with other organizations, discover features your are not using, and learn best practices through blog posts and white papers.
.svg-container.convdev
= custom_icon('convdev_overview')
.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')
.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'
.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'
- @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.row
- @metric.idea_to_production_steps.each_with_index do |step, index|
.convdev-step{ class: "convdev-#{score_level(step.percentage_score)}-score" }
.as
= custom_icon("i2p_step_#{index + 1}")
%h4.convdev-step-title
= step.title
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment