Commit 26dde5f5 authored by Taurie Davis, Simon Knox and Adam Niedzielski's avatar Taurie Davis, Simon Knox and Adam Niedzielski Committed by Adam Niedzielski

Add Conversational Development Index page to admin panel

parent c72abcef
<?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>
...@@ -393,6 +393,9 @@ import ShortcutsBlob from './shortcuts_blob'; ...@@ -393,6 +393,9 @@ import ShortcutsBlob from './shortcuts_blob';
case 'users:show': case 'users:show':
new UserCallout(); new UserCallout();
break; break;
case 'admin:conversational_development_index:show':
new UserCallout();
break;
case 'snippets:show': case 'snippets:show':
new LineHighlighter(); new LineHighlighter();
new BlobViewer(); new BlobViewer();
......
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
export default class UserCallout { export default class UserCallout {
constructor() { constructor(className = 'user-callout') {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); this.userCalloutBody = $(`.${className}`);
this.userCalloutBody = $('.user-callout'); this.cookieName = this.userCalloutBody.data('uid');
this.isCalloutDismissed = Cookies.get(this.cookieName);
this.init(); this.init();
} }
...@@ -18,7 +17,7 @@ export default class UserCallout { ...@@ -18,7 +17,7 @@ export default class UserCallout {
dismissCallout(e) { dismissCallout(e) {
const $currentTarget = $(e.currentTarget); const $currentTarget = $(e.currentTarget);
Cookies.set(USER_CALLOUT_COOKIE, 'true', { expires: 365 }); Cookies.set(this.cookieName, 'true', { expires: 365 });
if ($currentTarget.hasClass('close')) { if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove(); this.userCalloutBody.remove();
......
...@@ -569,3 +569,10 @@ $filter-value-selected-color: #d7d7d7; ...@@ -569,3 +569,10 @@ $filter-value-selected-color: #d7d7d7;
Animation Functions Animation Functions
*/ */
$dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1); $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;
}
...@@ -287,6 +287,7 @@ table.u2f-registrations { ...@@ -287,6 +287,7 @@ table.u2f-registrations {
.user-callout { .user-callout {
margin: 0 auto; margin: 0 auto;
max-width: $screen-lg-min;
.bordered-box { .bordered-box {
border: 1px solid $blue-300; border: 1px solid $blue-300;
...@@ -295,14 +296,15 @@ table.u2f-registrations { ...@@ -295,14 +296,15 @@ table.u2f-registrations {
position: relative; position: relative;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;
} }
.landing { .landing {
margin-top: $gl-padding; padding: 32px;
margin-bottom: $gl-padding;
.close { .close {
position: absolute; position: absolute;
top: 20px;
right: 20px; right: 20px;
opacity: 1; opacity: 1;
...@@ -330,11 +332,20 @@ table.u2f-registrations { ...@@ -330,11 +332,20 @@ table.u2f-registrations {
height: 110px; height: 110px;
vertical-align: top; vertical-align: top;
} }
&.convdev {
margin: 0 0 0 30px;
svg {
height: 127px;
}
}
} }
.user-callout-copy { .user-callout-copy {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
max-width: 570px;
} }
} }
...@@ -348,12 +359,20 @@ table.u2f-registrations { ...@@ -348,12 +359,20 @@ table.u2f-registrations {
.landing { .landing {
.svg-container, .svg-container,
.user-callout-copy { .user-callout-copy {
margin: 0; margin: 0 auto;
display: block; display: block;
svg { svg {
height: 75px; height: 75px;
} }
&.convdev {
margin: $gl-padding auto 0;
svg {
height: 120px;
}
}
} }
} }
} }
......
class Admin::ConversationalDevelopmentIndexController < Admin::ApplicationController
def show
@metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present
end
end
...@@ -275,8 +275,8 @@ module ApplicationHelper ...@@ -275,8 +275,8 @@ module ApplicationHelper
'active' if condition 'active' if condition
end end
def show_user_callout? def show_callout?(name)
cookies[:user_callout_dismissed].nil? cookies[name] != 'true'
end end
def linkedin_url(user) def linkedin_url(user)
......
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 - @no_container = true
- page_title "Background Jobs" - page_title "Background Jobs"
= render 'admin/background_jobs/head' = render 'admin/monitoring/head'
%div{ class: container_class } %div{ class: container_class }
%h3.page-title Background Jobs %h3.page-title Background Jobs
......
.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
- @no_container = true - @no_container = true
- page_title "Health Check" - page_title "Health Check"
= render 'admin/background_jobs/head' = render 'admin/monitoring/head'
%div{ class: container_class } %div{ class: container_class }
%h3.page-title %h3.page-title
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger, - loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::EnvironmentLogger, Gitlab::SidekiqLogger, Gitlab::EnvironmentLogger, Gitlab::SidekiqLogger,
Gitlab::RepositoryCheckLogger] Gitlab::RepositoryCheckLogger]
= render 'admin/background_jobs/head' = render 'admin/monitoring/head'
%div{ class: container_class } %div{ class: container_class }
%ul.nav-links.log-tabs %ul.nav-links.log-tabs
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
= render 'shared/nav_scroll' = render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs .nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) } %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 = nav_link(controller: :system_info) do
= link_to admin_system_info_path, title: 'System Info' do = link_to admin_system_info_path, title: 'System Info' do
%span %span
......
- @no_container = true - @no_container = true
- page_title 'Requests Profiles' - page_title 'Requests Profiles'
= render 'admin/background_jobs/head' = render 'admin/monitoring/head'
%div{ class: container_class } %div{ class: container_class }
%h3.page-title %h3.page-title
......
- @no_container = true - @no_container = true
- page_title "System Info" - page_title "System Info"
= render 'admin/background_jobs/head' = render 'admin/monitoring/head'
%div{ class: container_class } %div{ class: container_class }
.prepend-top-default .prepend-top-default
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= render "projects/last_push" = render "projects/last_push"
%div{ class: container_class } %div{ class: container_class }
- if show_user_callout? - if show_callout?('user_callout_dismissed')
= render 'shared/user_callout' = render 'shared/user_callout'
- if @projects.any? || params[:name] - if @projects.any? || params[:name]
......
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span %span
Overview Overview
= nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do
= link_to admin_system_info_path, title: 'Monitoring' do = link_to admin_conversational_development_index_path, title: 'Monitoring' do
%span %span
Monitoring Monitoring
= nav_link(controller: :broadcast_messages) do = nav_link(controller: :broadcast_messages) do
......
.user-callout .user-callout{ data: { uid: 'user_callout_dismissed' } }
.bordered-box.landing.content-block .bordered-box.landing.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button', %button.btn.btn-default.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss customize experience box' } 'aria-label' => 'Dismiss customize experience box' }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="m45.688 18.854c-4.869-1.989-10.488-1.975-15.29-.001-2.413.979-4.597 2.414-6.493 4.268-1.836 1.8-3.33 3.985-4.346 6.381-1.013 2.38-1.525 4.916-1.525 7.537 0 2.066.33 4.118.983 6.104.469 1.388 1.089 2.706 1.83 3.937-1.275 1.101-2.086 2.725-2.086 4.538 0 3.309 2.691 6 6 6s6-2.691 6-6-2.691-6-6-6c-.779 0-1.522.154-2.205.425-.665-1.105-1.221-2.289-1.642-3.533-.585-1.776-.881-3.618-.881-5.472 0-2.351.459-4.623 1.391-6.814.89-2.096 2.231-4.059 3.88-5.675 1.708-1.669 3.675-2.962 5.85-3.845 4.329-1.778 9.392-1.79 13.78.002 2.17.881 4.137 2.175 5.843 3.84 3.39 3.34 5.257 7.776 5.257 12.493.002 1.86-.294 3.705-.878 5.481-.579 1.75-1.443 3.406-2.569 4.923-2.134 2.866-3.818 4.698-5.174 6.173-2.424 2.643-3.98 4.599-4.383 8.384h-10.815c-.553 0-1 .447-1 1s.447 1 1 1h11.739c.532 0 .971-.416.999-.947.19-3.645 1.345-5.263 3.934-8.09 1.385-1.506 3.107-3.381 5.304-6.331 1.254-1.688 2.218-3.535 2.864-5.489.651-1.98.98-4.04.979-6.109 0-5.256-2.078-10.198-5.856-13.92-1.897-1.851-4.081-3.287-6.49-4.265m-16.927 32.763c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4"/>
<path d="m40 74h-4c-.553 0-1 .447-1 1s.447 1 1 1h4c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m42 70h-8c-.553 0-1 .447-1 1s.447 1 1 1h8c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m38 10c.553 0 1-.447 1-1v-8c0-.553-.447-1-1-1s-1 .447-1 1v8c0 .553.447 1 1 1"/>
<path d="m20.828 15.828c.256 0 .512-.098.707-.293.391-.391.391-1.023 0-1.414l-5.656-5.656c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l5.656 5.656c.195.195.451.293.707.293"/>
<path d="m10 33h-8c-.553 0-1 .447-1 1s.447 1 1 1h8c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m60.12 8.465l-5.656 5.656c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293s.512-.098.707-.293l5.656-5.656c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0"/>
<path d="m74 33h-8c-.553 0-1 .447-1 1s.447 1 1 1h8c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m43 66h-10c-.553 0-1 .447-1 1s.447 1 1 1h10c.553 0 1-.447 1-1s-.447-1-1-1"/>
</svg>
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="m5 43c0 .553.447 1 1 1s1-.447 1-1v-4h4c.553 0 1-.447 1-1s-.447-1-1-1h-4v-4c0-.553-.447-1-1-1s-1 .447-1 1v4h-4c-.553 0-1 .447-1 1s.447 1 1 1h4v4"/>
<path d="m75 37h-4v-4c0-.553-.447-1-1-1s-1 .447-1 1v4h-4c-.553 0-1 .447-1 1s.447 1 1 1h4v4c0 .553.447 1 1 1s1-.447 1-1v-4h4c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m21 38c0 .345.178.665.47.848l8 5c.165.103.348.152.529.152.333 0 .659-.166.849-.47.293-.469.15-1.086-.317-1.378l-6.644-4.152 6.644-4.152c.468-.292.61-.909.317-1.378s-.908-.611-1.378-.317l-8 5c-.292.182-.47.502-.47.847"/>
<path d="m55 38c0-.345-.178-.665-.47-.848l-8-5c-.469-.294-1.086-.151-1.378.317-.293.469-.15 1.086.317 1.378l6.644 4.153-6.644 4.152c-.468.292-.61.909-.317 1.378.189.304.516.47.849.47.181 0 .364-.049.529-.152l8-5c.292-.183.47-.503.47-.848"/>
<path d="m41.803 26.05c-.525-.168-1.089.124-1.256.65l-7 22c-.167.525.124 1.088.65 1.256.101.032.202.047.303.047.424 0 .817-.271.953-.697l7-22c.167-.526-.124-1.088-.65-1.256"/>
<path d="m62 7c3.859 0 7 3.141 7 7v11c0 .553.447 1 1 1s1-.447 1-1v-11c0-4.963-4.04-9-9-9h-16.09c-.479-2.833-2.943-5-5.91-5-3.309 0-6 2.691-6 6s2.691 6 6 6c2.967 0 5.431-2.167 5.91-5h16.09m-22 3c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4"/>
<path d="m6 26c.553 0 1-.447 1-1v-11c0-3.859 3.141-7 7-7h11.09l-3.293 3.293c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293s.512-.098.707-.293l5-5c.391-.391.391-1.023 0-1.414l-5-5c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l3.293 3.293h-11.09c-4.963 0-9 4.04-9 9v11c0 .553.447 1 1 1"/>
<path d="m36 64c-2.967 0-5.431 2.167-5.91 5h-16.09c-3.859 0-7-3.141-7-7v-11c0-.553-.447-1-1-1s-1 .447-1 1v11c0 4.963 4.04 9 9 9h16.09c.478 2.833 2.942 5 5.91 5 3.309 0 6-2.691 6-6s-2.691-6-6-6m0 10c-2.206 0-4-1.794-4-4s1.794-4 4-4 4 1.794 4 4-1.794 4-4 4"/>
<path d="m70 50c-.553 0-1 .447-1 1v11c0 3.859-3.141 7-7 7h-11.09l3.293-3.293c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0l-5 5c-.391.391-.391 1.023 0 1.414l5 5c.195.195.451.293.707.293s.512-.098.707-.293c.391-.391.391-1.023 0-1.414l-3.293-3.293h11.09c4.963 0 9-4.04 9-9v-11c0-.553-.447-1-1-1"/>
</svg>
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="m42.26 40.44c.558.073 1.045-.329 1.109-.877l2.625-22.444c.033-.283-.057-.567-.246-.781-.189-.214-.462-.336-.747-.336h-14c-.284 0-.555.121-.744.332-.19.212-.281.494-.25.776l3.454 31.575c-1.503 1.285-2.46 3.19-2.46 5.317 0 3.859 3.141 7 7 7s7-3.141 7-7-3.141-7-7-7c-.94 0-1.835.189-2.655.527l-3.23-29.527h11.761l-2.494 21.328c-.065.549.328 1.045.877 1.11m.741 13.562c0 2.757-2.243 5-5 5s-5-2.243-5-5 2.243-5 5-5 5 2.243 5 5"/>
<path d="M73.236,23.749c-0.207-0.513-0.796-0.76-1.302-0.552c-0.513,0.207-0.759,0.79-0.552,1.302 C73.119,28.787,74,33.329,74,38c0,19.851-16.149,36-36,36S2,57.851,2,38S18.149,2,38,2c7.6,0,14.83,2.332,20.965,6.74 C58.339,9.702,58,10.825,58,12c0,1.603,0.624,3.109,1.758,4.242C60.891,17.376,62.397,18,64,18c1.603,0,3.109-0.624,4.242-1.758 C69.376,15.109,70,13.603,70,12s-0.624-3.109-1.758-4.242C67.109,6.624,65.603,6,64,6c-1.346,0-2.622,0.445-3.668,1.259 C53.812,2.512,46.104,0,38,0C17.047,0,0,17.047,0,38s17.047,38,38,38s38-17.047,38-38C76,33.07,75.07,28.275,73.236,23.749z M64,8 c1.068,0,2.072,0.416,2.828,1.172S68,10.932,68,12s-0.416,2.072-1.172,2.828c-1.512,1.512-4.145,1.512-5.656,0 C60.416,14.072,60,13.068,60,12s0.416-2.072,1.172-2.828S62.932,8,64,8z"/>
</svg>
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="m12 8c0-3.309-2.691-6-6-6s-6 2.691-6 6c0 2.967 2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909s2.167 5.431 5 5.91v8.181c-2.833.478-5 2.942-5 5.909 0 3.309 2.691 6 6 6s6-2.691 6-6c0-2.967-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.478 5-2.942 5-5.91s-2.167-5.431-5-5.91v-8.18c2.833-.479 5-2.943 5-5.91m-10 0c0-2.206 1.794-4 4-4s4 1.794 4 4-1.794 4-4 4-4-1.794-4-4m8 60c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4m0-20c0 2.206-1.794 4-4 4s-4-1.794-4-4 1.794-4 4-4 4 1.794 4 4"/>
<path d="m21 6h54c.553 0 1-.447 1-1s-.447-1-1-1h-54c-.553 0-1 .447-1 1s.447 1 1 1"/>
<path d="m21 12h35c.553 0 1-.447 1-1s-.447-1-1-1h-35c-.553 0-1 .447-1 1s.447 1 1 1"/>
<path d="m75 24h-54c-.553 0-1 .447-1 1s.447 1 1 1h54c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m21 32h34c.553 0 1-.447 1-1s-.447-1-1-1h-34c-.553 0-1 .447-1 1s.447 1 1 1"/>
<path d="m75 44h-54c-.553 0-1 .447-1 1s.447 1 1 1h54c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m21 52h34c.553 0 1-.447 1-1s-.447-1-1-1h-34c-.553 0-1 .447-1 1s.447 1 1 1"/>
<path d="m75 64h-54c-.553 0-1 .447-1 1s.447 1 1 1h54c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m55 70h-34c-.553 0-1 .447-1 1s.447 1 1 1h34c.553 0 1-.447 1-1s-.447-1-1-1"/>
</svg>
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="m67.7 10h-6.751c-.507-5.598-5.221-10-10.949-10-6.06 0-11 4.935-11 11s4.935 11 11 11c5.728 0 10.442-4.402 10.949-10h6.751c1.269 0 2.3.987 2.3 2.2v57.6c0 1.213-1.031 2.2-2.3 2.2h-59.4c-1.269 0-2.3-.987-2.3-2.2v-57.6c0-1.213 1.031-2.2 2.3-2.2h15.15c.553 0 1-.447 1-1s-.447-1-1-1h-15.15c-2.371 0-4.3 1.884-4.3 4.2v57.6c0 2.316 1.929 4.2 4.3 4.2h59.4c2.371 0 4.3-1.884 4.3-4.2v-57.6c0-2.316-1.929-4.2-4.3-4.2m-17.7 10c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/>
<path d="m21.293 29.29c-.391.391-.391 1.023 0 1.414l12.975 12.975-12.975 12.974c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293s.512-.098.707-.293l13.682-13.682c.391-.391.391-1.023 0-1.414l-13.682-13.681c-.391-.391-1.023-.391-1.414 0"/>
<path d="m54 59c.553 0 1-.447 1-1s-.447-1-1-1h-12c-.553 0-1 .447-1 1s.447 1 1 1h12"/>
</svg>
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="m48.949 37c-.507-5.598-5.221-10-10.949-10s-10.442 4.402-10.949 10h-13.05c-.553 0-1 .447-1 1s.447 1 1 1h13.05c.507 5.598 5.221 10 10.949 10s10.442-4.402 10.949-10h12.24c.553 0 1-.447 1-1s-.447-1-1-1h-12.24m-10.949 10c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/>
<path d="M73.236,23.749c-0.207-0.513-0.797-0.76-1.302-0.552c-0.513,0.207-0.759,0.79-0.552,1.302 C73.119,28.787,74,33.329,74,38c0,19.851-16.149,36-36,36S2,57.851,2,38S18.149,2,38,2c7.6,0,14.83,2.332,20.965,6.74 C58.339,9.702,58,10.825,58,12c0,1.603,0.624,3.109,1.758,4.242C60.891,17.376,62.397,18,64,18c1.603,0,3.109-0.624,4.242-1.758 C69.376,15.109,70,13.603,70,12s-0.624-3.109-1.758-4.242C67.109,6.624,65.603,6,64,6c-1.346,0-2.622,0.445-3.668,1.259 C53.812,2.512,46.104,0,38,0C17.047,0,0,17.047,0,38s17.047,38,38,38s38-17.047,38-38C76,33.07,75.07,28.275,73.236,23.749z M64,8 c1.068,0,2.072,0.416,2.828,1.172S68,10.932,68,12s-0.416,2.072-1.172,2.828c-1.512,1.512-4.145,1.512-5.656,0 C60.416,14.072,60,13.068,60,12s0.416-2.072,1.172-2.828S62.932,8,64,8z"/>
</svg>
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="m14.267 7.32l-4.896 5.277-1.702-1.533c-.409-.369-1.043-.338-1.412.074-.369.41-.337 1.042.074 1.412l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062.114.044.235.066.356.066.135 0 .27-.028.396-.082.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6c.375-.404.352-1.037-.054-1.413-.405-.377-1.036-.353-1.412.053"/>
<path d="m31 9h44c.553 0 1-.447 1-1s-.447-1-1-1h-44c-.553 0-1 .447-1 1s.447 1 1 1"/>
<path d="m31 15h24c.553 0 1-.447 1-1s-.447-1-1-1h-24c-.553 0-1 .447-1 1s.447 1 1 1"/>
<path d="m11 0c-6.07 0-11 4.935-11 11s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/>
<path d="m14.267 34.32l-4.896 5.277-1.702-1.533c-.409-.368-1.043-.338-1.412.074-.369.41-.337 1.042.074 1.412l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062.114.044.235.066.356.066.135 0 .27-.028.396-.082.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6c.375-.404.352-1.037-.054-1.413-.405-.377-1.036-.353-1.412.053"/>
<path d="m75 34h-44c-.553 0-1 .447-1 1s.447 1 1 1h44c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m31 42h24c.553 0 1-.447 1-1s-.447-1-1-1h-24c-.553 0-1 .447-1 1s.447 1 1 1"/>
<path d="m11 27c-6.07 0-11 4.935-11 11s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/>
<path d="m14.267 61.32l-4.896 5.277-1.702-1.533c-.409-.368-1.043-.338-1.412.074-.369.41-.337 1.042.074 1.412l2.434 2.192c.064.058.139.091.212.13.035.018.065.048.101.062.114.044.235.066.356.066.135 0 .27-.028.396-.082.044-.019.077-.058.118-.084.076-.047.155-.086.219-.154l5.566-6c.375-.404.352-1.037-.054-1.413-.405-.377-1.036-.353-1.412.053"/>
<path d="m11 54c-6.07 0-11 4.935-11 11s4.935 11 11 11 11-4.935 11-11-4.935-11-11-11m0 20c-4.963 0-9-4.04-9-9s4.04-9 9-9 9 4.04 9 9-4.04 9-9 9"/>
<path d="m75 61h-44c-.553 0-1 .447-1 1s.447 1 1 1h44c.553 0 1-.447 1-1s-.447-1-1-1"/>
<path d="m55 67h-24c-.553 0-1 .447-1 1s.447 1 1 1h24c.553 0 1-.447 1-1s-.447-1-1-1"/>
</svg>
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="M73.236,23.749c-0.208-0.513-0.798-0.76-1.302-0.552c-0.513,0.207-0.759,0.79-0.552,1.302 C73.119,28.787,74,33.329,74,38c0,19.851-16.149,36-36,36S2,57.851,2,38S18.149,2,38,2c7.6,0,14.83,2.332,20.965,6.74 C58.339,9.702,58,10.825,58,12c0,1.603,0.624,3.109,1.758,4.242C60.891,17.376,62.397,18,64,18c1.603,0,3.109-0.624,4.242-1.758 C69.376,15.109,70,13.603,70,12s-0.624-3.109-1.758-4.242C67.109,6.624,65.603,6,64,6c-1.346,0-2.622,0.445-3.668,1.259 C53.812,2.512,46.104,0,38,0C17.047,0,0,17.047,0,38s17.047,38,38,38s38-17.047,38-38C76,33.07,75.07,28.275,73.236,23.749z M64,8 c1.068,0,2.072,0.416,2.828,1.172S68,10.932,68,12s-0.416,2.072-1.172,2.828c-1.512,1.512-4.145,1.512-5.656,0 C60.416,14.072,60,13.068,60,12s0.416-2.072,1.172-2.828S62.932,8,64,8z"/>
<path d="m27.19 32.17c-.277-.479-.89-.643-1.366-.364l-12.654 7.326c-.309.179-.499.509-.499.865s.19.687.499.865l12.654 7.326c.157.092.33.135.5.135.345 0 .681-.179.866-.499.277-.478.113-1.09-.364-1.366l-11.159-6.461 11.159-6.461c.478-.276.642-.889.364-1.366"/>
<path d="m48.808 47.827c.186.32.521.499.866.499.17 0 .343-.043.5-.135l12.654-7.326c.309-.179.499-.509.499-.865s-.19-.687-.499-.865l-12.654-7.326c-.478-.278-1.09-.114-1.366.364-.277.478-.113 1.09.364 1.366l11.159 6.461-11.159 6.461c-.478.276-.642.889-.364 1.366"/>
<path d="m42.71 23.06l-11.312 33.23c-.179.522.102 1.091.624 1.269.106.037.216.054.322.054.416 0 .805-.262.946-.678l11.312-33.23c.179-.522-.102-1.091-.624-1.269-.523-.181-1.089.101-1.268.624"/>
</svg>
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="m62.44 54.765l-9.912-11.09c.315-3.881.481-7.241.508-10.271-.029-13.871-3.789-23.05-13.413-32.746-.855-.859-2.411-.828-3.294.059-7.594 7.65-11.139 13.934-12.575 22.3-1.776.062-3.437.776-4.699 2.039-1.321 1.321-2.05 3.079-2.05 4.949s.729 3.628 2.051 4.949c1.321 1.322 3.079 2.051 4.949 2.051s3.628-.729 4.949-2.051c1.322-1.321 2.051-3.079 2.051-4.949 0-1.869-.729-3.627-2.051-4.949-.9-.9-2-1.517-3.205-1.824 1.373-7.859 4.764-13.818 11.999-21.11.128-.13.356-.158.456-.059 9.207 9.274 12.805 18.06 12.832 31.33-.026 3.079-.202 6.527-.536 10.54-.023.273.067.545.25.749l10.166 11.379c.062.076.109.23.093.32l-4.547 17.407c-.004.015-.009.036-.079.106-.062.063-.155.1-.2.106l-3.577.002c-.144-.009-.265-.077-.309-.153l-5.425-10.328c-.173-.329-.515-.535-.886-.535h-15.962c-.371 0-.713.206-.886.535l-5.407 10.303-.069.072c-.07.07-.165.105-.199.105l-3.588.001c-.179-.009-.304-.123-.33-.227l-4.531-17.338c-.029-.146.019-.301.049-.34l10.197-11.415c.367-.412.332-1.044-.08-1.412-.411-.366-1.042-.333-1.412.08l-10.229 11.453c-.448.554-.63 1.312-.474 2.084l4.544 17.396c.253.963 1.146 1.669 2.218 1.719h3.636c.581 0 1.187-.261 1.615-.693.114-.114.286-.286.406-.528l5.144-9.793h14.754l5.16 9.822c.396.697 1.124 1.143 2.01 1.192l3.712-.003c.604-.046 1.137-.285 1.544-.694.313-.316.504-.646.598-1.022l4.557-17.451c.143-.718-.039-1.476-.518-2.066m-33.435-24.765c0 1.335-.521 2.591-1.465 3.535s-2.2 1.465-3.535 1.465-2.591-.521-3.535-1.465-1.465-2.2-1.465-3.535.521-2.591 1.465-3.535 2.2-1.465 3.535-1.465 2.591.521 3.535 1.465 1.465 2.2 1.465 3.535"/>
</svg>
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 76" enable-background="new 0 0 76 76">
<path d="m68 67c-1.725 0-3.36.541-4.723 1.545-2.298-4.02-6.592-6.545-11.277-6.545-2.734 0-5.359.853-7.555 2.43l-2.286-15.43h1.228l3.829 7.645c.339.598.962.979 1.724 1.022l2.812-.003c.507-.039.974-.25 1.316-.595.264-.266.433-.559.514-.882l3.433-13.145c.12-.611-.037-1.258-.449-1.763l-7.385-8.268c.231-2.875.354-5.376.374-7.641-.023-10.507-2.871-17.462-10.162-24.806-.737-.742-2.072-.715-2.829.044-5.617 5.659-8.309 10.336-9.446 16.463-1.267.186-2.438.764-3.36 1.686-1.134 1.134-1.758 2.64-1.758 4.243s.624 3.109 1.758 4.242c1.133 1.134 2.639 1.758 4.242 1.758s3.109-.624 4.242-1.758c1.134-1.133 1.758-2.639 1.758-4.242s-.624-3.109-1.758-4.242c-.858-.859-1.932-1.424-3.098-1.648 1.095-5.538 3.637-9.855 8.83-15.14 6.874 6.924 9.561 13.485 9.581 23.392-.021 2.316-.151 4.903-.402 7.91-.023.273.067.544.25.749l7.663 8.572-3.391 13.07-2.695.036-4.081-8.15c-.17-.339-.516-.553-.895-.553h-12.01c-.379 0-.725.214-.895.553l-4.04 8.114-2.707.015-3.427-13.07 7.671-8.588c.367-.412.332-1.044-.08-1.412-.411-.366-1.043-.333-1.412.08l-7.7 8.623c-.383.47-.54 1.116-.406 1.787l3.419 13.08c.216.829.98 1.438 1.907 1.48h2.735c.508 0 1.016-.218 1.391-.595.091-.09.242-.241.358-.475l3.804-7.597h1.228l-2.286 15.43c-2.196-1.577-4.821-2.43-7.555-2.43-4.685 0-8.979 2.53-11.277 6.545-1.363-1-2.998-1.545-4.723-1.545-4.411 0-8 3.589-8 8 0 .553.447 1 1 1h74c.553 0 1-.447 1-1 0-4.411-3.589-8-8-8m-36-44c0 1.068-.416 2.072-1.172 2.828-1.512 1.512-4.145 1.512-5.656 0-.756-.756-1.172-1.76-1.172-2.828s.416-2.072 1.172-2.828 1.76-1.172 2.828-1.172 2.072.416 2.828 1.172 1.172 1.76 1.172 2.828m-29.917 51c.478-2.834 2.949-5 5.917-5 1.638 0 3.17.652 4.313 1.836.231.24.562.35.895.29.327-.058.604-.274.739-.579 1.765-3.977 5.711-6.547 10.05-6.547 2.836 0 5.532 1.085 7.593 3.055.271.258.665.345 1.016.224.354-.122.61-.43.665-.8l2.588-17.479h4.275l2.589 17.479c.055.37.312.678.665.8s.745.035 1.016-.224c2.061-1.97 4.757-3.055 7.593-3.055 4.343 0 8.288 2.57 10.05 6.547.135.305.412.521.739.579.329.059.663-.051.895-.29 1.143-1.184 2.675-1.836 4.313-1.836 2.968 0 5.439 2.166 5.917 5h-71.834"/>
</svg>
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
Snippets Snippets
%div{ class: container_class } %div{ class: container_class }
- if @user == current_user && show_user_callout? - if @user == current_user && show_callout?('user_callout_dismissed')
= render 'shared/user_callout' = render 'shared/user_callout'
.tab-content .tab-content
#activity.tab-pane #activity.tab-pane
......
...@@ -3,29 +3,17 @@ class GitlabUsagePingWorker ...@@ -3,29 +3,17 @@ class GitlabUsagePingWorker
include Sidekiq::Worker include Sidekiq::Worker
include CronjobQueue include CronjobQueue
include HTTParty
def perform 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. # Multiple Sidekiq workers could run this. We should only do this at most once a day.
return unless try_obtain_lease return unless try_obtain_lease
begin SubmitUsagePingService.new.execute
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
end end
private
def try_obtain_lease def try_obtain_lease
Gitlab::ExclusiveLease.new('gitlab_usage_ping_worker:ping', timeout: LEASE_TIMEOUT).try_obtain Gitlab::ExclusiveLease.new('gitlab_usage_ping_worker:ping', timeout: LEASE_TIMEOUT).try_obtain
end end
def url
'https://version.gitlab.com/usage_data'
end
end end
---
title: Add ConvDev Index page to admin area
merge_request: 11377
author:
...@@ -72,6 +72,8 @@ namespace :admin do ...@@ -72,6 +72,8 @@ namespace :admin do
resource :system_info, controller: 'system_info', only: [:show] resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ } resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
get 'conversational_development_index' => 'conversational_development_index#show'
resources :projects, only: [:index] resources :projects, only: [:index]
scope(path: 'projects/*namespace_id', scope(path: 'projects/*namespace_id',
......
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
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
...@@ -370,6 +370,31 @@ ActiveRecord::Schema.define(version: 20170525174156) do ...@@ -370,6 +370,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"], 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 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| create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false t.integer "deploy_key_id", null: false
t.integer "project_id", null: false t.integer "project_id", null: false
......
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
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
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
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
...@@ -3,21 +3,11 @@ require 'spec_helper' ...@@ -3,21 +3,11 @@ require 'spec_helper'
describe GitlabUsagePingWorker do describe GitlabUsagePingWorker do
subject { described_class.new } subject { described_class.new }
it "sends POST request" do it 'delegates to SubmitUsagePingService' do
stub_application_setting(usage_ping_enabled: true) allow(subject).to receive(:try_obtain_lease).and_return(true)
stub_request(:post, "https://version.gitlab.com/usage_data"). expect_any_instance_of(SubmitUsagePingService).to receive(:execute)
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(subject.perform.response.code.to_i).to eq(200) subject.perform
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)
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment