Commit 3b39cf4e authored by Sean McGivern's avatar Sean McGivern

Merge branch '30469-convdev-index' into 'master'

ConvDev Index

Closes #30469

See merge request !11377
parents e90ca0f0 d913ce17
<?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;
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;
}
...@@ -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.sum do |feature|
metric.percentage_score(feature)
end
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.sum(&:percentage_score) / 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 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')
.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.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
- @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
...@@ -371,6 +371,31 @@ ActiveRecord::Schema.define(version: 20170525174156) do ...@@ -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"], 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