Commit 01513668 authored by Vitali Tatarintev's avatar Vitali Tatarintev

Merge branch 'move-analytics-to-admin-panel' into 'master'

Move Analytics to admin panel

See merge request gitlab-org/gitlab!39368
parents 6dd8b3a2 92e75774
# frozen_string_literal: true
class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController
class Admin::CohortsController < Admin::ApplicationController
include Analytics::UniqueVisitsHelper
before_action :authenticate_usage_ping_enabled_or_admin!
track_unique_visits :index, target_id: 'i_analytics_cohorts'
def index
......@@ -16,8 +14,4 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon
@cohorts = CohortsSerializer.new.represent(cohorts_results)
end
end
def authenticate_usage_ping_enabled_or_admin!
render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin?
end
end
# frozen_string_literal: true
class InstanceStatistics::DevOpsScoreController < InstanceStatistics::ApplicationController
class Admin::DevOpsScoreController < Admin::ApplicationController
include Analytics::UniqueVisitsHelper
track_unique_visits :index, target_id: 'i_analytics_dev_ops_score'
track_unique_visits :show, target_id: 'i_analytics_dev_ops_score'
# rubocop: disable CodeReuse/ActiveRecord
def index
def show
@metric = DevOpsScore::Metric.order(:created_at).last&.present
end
# rubocop: enable CodeReuse/ActiveRecord
......
# frozen_string_literal: true
class InstanceStatistics::ApplicationController < ApplicationController
before_action :authorize_read_instance_statistics!
layout 'instance_statistics'
def authorize_read_instance_statistics!
render_404 unless can?(current_user, :read_instance_statistics)
end
end
......@@ -298,7 +298,6 @@ module ApplicationSettingsHelper
:unique_ips_limit_per_user,
:unique_ips_limit_time_window,
:usage_ping_enabled,
:instance_statistics_visibility_private,
:user_default_external,
:user_show_add_ssh_key_message,
:user_default_internal_regex,
......
......@@ -62,6 +62,10 @@ module NavHelper
%w(system_info background_jobs health_check requests_profiles)
end
def admin_analytics_nav_links
%w(dev_ops_score cohorts)
end
def group_issues_sub_menu_items
%w(groups#issues labels#index milestones#index boards#index boards#show)
end
......
......@@ -127,5 +127,3 @@ module TabHelper
end
end
end
TabHelper.prepend_if_ee('EE::TabHelper')
......@@ -8,6 +8,7 @@ class ApplicationSetting < ApplicationRecord
include IgnorableColumns
ignore_column :namespace_storage_size_limit, remove_with: '13.5', remove_after: '2020-09-22'
ignore_column :instance_statistics_visibility_private, remove_with: '13.6', remove_after: '2020-10-22'
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
......
......@@ -87,7 +87,6 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'],
instance_statistics_visibility_private: false,
issues_create_limit: 300,
local_markdown_version: 0,
login_recaptcha_protection_enabled: false,
......
......@@ -15,14 +15,9 @@ class GlobalPolicy < BasePolicy
@user&.required_terms_not_accepted?
end
condition(:private_instance_statistics, score: 0) { Gitlab::CurrentSettings.instance_statistics_visibility_private? }
condition(:project_bot, scope: :user) { @user&.project_bot? }
condition(:migration_bot, scope: :user) { @user&.migration_bot? }
rule { admin | (~private_instance_statistics & ~anonymous) }
.enable :read_instance_statistics
rule { anonymous }.policy do
prevent :log_in
prevent :receive_notifications
......@@ -103,6 +98,7 @@ class GlobalPolicy < BasePolicy
rule { admin }.policy do
enable :read_custom_attribute
enable :update_custom_attribute
enable :read_instance_statistics
end
# We can't use `read_statistics` because the user may have different permissions for different projects
......
......@@ -34,8 +34,5 @@
- deactivating_usage_ping_path = help_page_path('development/telemetry/usage_ping', anchor: 'disable-usage-ping')
- deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path }
= s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe }
.form-group.mt-3
= f.label :instance_statistics_visibility_private, _('Instance Statistics visibility')
= f.select :instance_statistics_visibility_private, options_for_select({_('All users') => false, _('Only admins') => true}, Gitlab::CurrentSettings.instance_statistics_visibility_private?), {}, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
- page_title _('Analytics')
- header_title _('Analytics'), instance_statistics_root_path
- nav 'instance_statistics'
- @left_sidebar = true
= render template: 'layouts/application'
- return unless dashboard_nav_link?(:analytics)
= nav_link(controller: [:dev_ops_score, :cohorts], html_options: { class: "d-none d-xl-block"}) do
= link_to instance_statistics_root_path, class: 'chart-icon', title: _('Analytics'), aria: { label: _('Analytics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('chart', size: 18)
......@@ -43,8 +43,6 @@
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
= _('Snippets')
= render_if_exists 'layouts/nav/sidebar/analytics_link'
%li.dropdown
= render_if_exists 'dashboard/nav_link_list'
......@@ -66,8 +64,6 @@
= link_to sherlock_transactions_path, class: 'admin-icon' do
= _('Sherlock Transactions')
= render_if_exists 'layouts/nav/analytics_link'
- if current_user.admin?
= nav_link(controller: 'admin/dashboard', html_options: { class: "d-none d-xl-block"}) do
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
......
......@@ -48,6 +48,28 @@
%span
= _('Gitaly Servers')
= nav_link(controller: admin_analytics_nav_links) do
= link_to admin_dev_ops_score_path, data: { qa_selector: 'admin_analytics_link' } do
.nav-icon-container
= sprite_icon('chart')
%span.nav-item-name
= _('Analytics')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_analytics_submenu_content' } }
= nav_link(controller: admin_analytics_nav_links, html_options: { class: "fly-out-top-item" }) do
= link_to admin_dev_ops_score_path do
%strong.fly-out-top-item-name
= _('Analytics')
%li.divider.fly-out-top-item
= nav_link(controller: :dev_ops_score) do
= link_to admin_dev_ops_score_path, title: _('DevOps Score') do
%span
= _('DevOps Score')
= nav_link(controller: :cohorts) do
= link_to admin_cohorts_path, title: _('Cohorts') do
%span
= _('Cohorts')
= nav_link(controller: admin_monitoring_nav_links) do
= link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_link' } do
.nav-icon-container
......
- return unless dashboard_nav_link?(:analytics)
= nav_link(controller: [:dev_ops_score, :cohorts]) do
= link_to instance_statistics_root_path, class: 'd-xl-none' do
= _('Analytics')
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to instance_statistics_root_path, title: _('Analytics') do
.avatar-container.s40.settings-avatar
= sprite_icon('chart', size: 24)
.sidebar-context-title= _('Analytics')
%ul.sidebar-top-level-items
= render 'layouts/nav/sidebar/instance_statistics_links'
= render 'shared/sidebar_toggle_button'
- return unless can?(current_user, :read_instance_statistics)
= nav_link(controller: :dev_ops_score) do
= link_to instance_statistics_dev_ops_score_index_path do
.nav-icon-container
= sprite_icon('comment')
%span.nav-item-name
= _('DevOps Score')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :dev_ops_score, html_options: { class: "fly-out-top-item" } ) do
= link_to instance_statistics_dev_ops_score_index_path do
%strong.fly-out-top-item-name
= _('DevOps Score')
- if Gitlab::CurrentSettings.usage_ping_enabled
= nav_link(controller: :cohorts) do
= link_to instance_statistics_cohorts_path do
.nav-icon-container
= sprite_icon('users')
%span.nav-item-name
= _('Cohorts')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do
= link_to instance_statistics_cohorts_path do
%strong.fly-out-top-item-name
= _('Cohorts')
......@@ -72,6 +72,10 @@ Rails.application.routes.draw do
# Begin of the /-/ scope.
# Use this scope for all new global routes.
scope path: '-' do
# remove in 13.5
get '/instance_statistics', to: redirect('admin/dev_ops_score')
get '/instance_statistics/dev_ops_score', to: redirect('admin/dev_ops_score')
get '/instance_statistics/cohorts', to: redirect('admin/cohorts')
# Autocomplete
get '/autocomplete/users' => 'autocomplete#users'
get '/autocomplete/users/:id' => 'autocomplete#user'
......@@ -123,7 +127,6 @@ Rails.application.routes.draw do
get 'ide/*vueroute' => 'ide#index', format: false
draw :operations
draw :instance_statistics
Gitlab.ee do
draw :security
......
......@@ -89,6 +89,10 @@ namespace :admin do
resources :projects, only: [:index]
resources :instance_statistics, only: :index
resource :dev_ops_score, controller: 'dev_ops_score', only: :show
resources :cohorts, only: :index
scope(path: 'projects/*namespace_id',
as: :namespace,
constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
......
# frozen_string_literal: true
namespace :instance_statistics do
root to: redirect('-/instance_statistics/dev_ops_score')
resources :cohorts, only: :index
resources :dev_ops_score, only: :index
end
# frozen_string_literal: true
class Analytics::AnalyticsController < Analytics::ApplicationController
def index
if can?(current_user, :read_instance_statistics)
redirect_to instance_statistics_dev_ops_score_index_path
else
render_404
end
end
end
......@@ -31,14 +31,6 @@ module EE
!current_user.has_current_license? && current_user.admin?
end
def analytics_nav_url
if can?(current_user, :read_instance_statistics)
instance_statistics_root_path
else
'errors/not_found'
end
end
private
override :get_dashboard_nav_links
......
# frozen_string_literal: true
module EE
module TabHelper
extend ::Gitlab::Utils::Override
def analytics_controllers
['analytics/productivity_analytics', 'analytics/cycle_analytics', 'instance_statistics/dev_ops_score', 'instance_statistics/cohorts']
end
end
end
- page_title _('Security')
- header_title _('Security'), instance_statistics_root_path
- header_title _('Security')
- nav 'security'
- @left_sidebar = true
......
- page_title _('Analytics')
- header_title _('Analytics'), instance_statistics_root_path
- nav 'analytics'
- @left_sidebar = true
= render template: 'layouts/application'
- return unless dashboard_nav_link?(:analytics)
= nav_link(controller: analytics_controllers, html_options: { class: "d-none d-xl-block"}) do
= link_to analytics_nav_url, class: 'chart-icon', title: _('Analytics'), aria: { label: _('Analytics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('chart', size: 18)
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
.context-header
= link_to analytics_root_path, title: _('Analytics') do
.avatar-container.s40.settings-avatar
= sprite_icon('chart', size: 24)
.sidebar-context-title= _('Analytics')
%ul.sidebar-top-level-items
= render_ce 'layouts/nav/sidebar/instance_statistics_links'
= render 'shared/sidebar_toggle_button'
- return unless dashboard_nav_link?(:analytics)
= nav_link(controller: analytics_controllers) do
= link_to analytics_root_path, class: 'd-xl-none' do
= _('Analytics')
---
title: Move Analytics to admin panel
merge_request: 39368
author:
type: changed
# frozen_string_literal: true
namespace :analytics do
root to: 'analytics#index'
root to: redirect('admin/dev_ops_score')
constraints(-> (req) { Gitlab::Analytics.cycle_analytics_enabled? }) do
resource :cycle_analytics, only: :show, path: 'value_stream_analytics'
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::AnalyticsController do
let(:user) { create(:user) }
before do
sign_in(user)
end
describe 'GET index' do
it 'renders devops score page when all the analytics feature flags are disabled' do
get :index
expect(response).to redirect_to(instance_statistics_dev_ops_score_index_path)
end
context 'when instance statistics is private' do
before do
stub_application_setting(instance_statistics_visibility_private: true)
end
it 'renders 404, not found' do
get :index
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
include_examples GracefulTimeoutHandling
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'accessing the analytics workspace' do
let(:user) { create(:user) }
before do
sign_in(user)
end
it 'renders the productivity analytics landing page' do
stub_licensed_features(Gitlab::Analytics::PRODUCTIVITY_ANALYTICS_FEATURE_FLAG => true)
visit analytics_root_path
expect(page.status_code).to eq(200)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Group Value Stream Analytics', :js do
include DragTo
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, name: 'CA-test-group') }
let_it_be(:sub_group) { create(:group, name: 'CA-sub-group', parent: group) }
let_it_be(:group2) { create(:group, name: 'CA-bad-test-group') }
let_it_be(:project) { create(:project, :repository, namespace: group, group: group, name: 'Cool fun project') }
let_it_be(:group_label1) { create(:group_label, group: group) }
let_it_be(:group_label2) { create(:group_label, group: group) }
let_it_be(:label) { create(:group_label, group: group2) }
let_it_be(:sub_group_label1) { create(:group_label, group: sub_group) }
let_it_be(:sub_group_label2) { create(:group_label, group: sub_group) }
let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
stage_nav_selector = '.stage-nav'
path_nav_selector = '.js-path-navigation'
filter_bar_selector = '.js-filter-bar'
duration_stage_selector = '.js-dropdown-stages'
value_stream_selector = '[data-testid="dropdown-value-streams"]'
3.times do |i|
let_it_be("issue_#{i}".to_sym) { create(:issue, title: "New Issue #{i}", project: project, created_at: 2.days.ago) }
end
shared_examples 'empty state' do
it 'displays an empty state before a group is selected' do
element = page.find('.row.empty-state')
expect(element).to have_content(_('Value Stream Analytics can help you determine your team’s velocity'))
expect(element.find('.svg-content img')['src']).to have_content('illustrations/analytics/cycle-analytics-empty-chart')
end
end
before do
stub_licensed_features(cycle_analytics_for_groups: true)
group.add_owner(user)
project.add_maintainer(user)
sign_in(user)
visit analytics_cycle_analytics_path
end
it_behaves_like 'empty state'
context 'deep linked url parameters' do
group_dropdown = '.js-groups-dropdown-filter'
projects_dropdown = '.js-projects-dropdown-filter'
before do
stub_licensed_features(cycle_analytics_for_groups: true)
group.add_owner(user)
sign_in(user)
end
shared_examples 'group dropdown set' do
it 'has the group dropdown prepopulated' do
element = page.find(group_dropdown)
expect(element).to have_content group.name
end
end
context 'without valid query parameters set' do
context 'with no group_id set' do
before do
visit analytics_cycle_analytics_path
end
it_behaves_like 'empty state'
end
context 'with created_after date > created_before date' do
before do
visit "#{analytics_cycle_analytics_path}?created_after=2019-12-31&created_before=2019-11-01"
end
it_behaves_like 'empty state'
end
context 'with fake parameters' do
before do
visit "#{analytics_cycle_analytics_path}?beans=not-cool"
end
it_behaves_like 'empty state'
end
end
context 'with valid query parameters set' do
context 'with group_id set' do
before do
visit "#{analytics_cycle_analytics_path}?group_id=#{group.full_path}"
end
it_behaves_like 'group dropdown set'
end
context 'with project_ids set' do
before do
visit "#{analytics_cycle_analytics_path}?group_id=#{group.full_path}&project_ids[]=#{project.id}"
end
it 'has the projects dropdown prepopulated' do
element = page.find(projects_dropdown)
expect(element).to have_content project.name
end
it_behaves_like 'group dropdown set'
end
context 'with created_before and created_after set' do
date_range = '.js-daterange-picker'
before do
visit "#{analytics_cycle_analytics_path}?group_id=#{group.full_path}&created_before=2019-12-31&created_after=2019-11-01"
end
it 'has the date range prepopulated' do
element = page.find(date_range)
expect(element.find('.js-daterange-picker-from input').value).to eq '2019-11-01'
expect(element.find('.js-daterange-picker-to input').value).to eq '2019-12-31'
end
it_behaves_like 'group dropdown set'
end
end
end
context 'displays correct fields after group selection' do
before do
select_group
end
it 'hides the empty state' do
expect(page).to have_selector('.row.empty-state', visible: false)
end
it 'shows the projects filter' do
expect(page).to have_selector('.dropdown-projects', visible: true)
end
it 'shows the date filter' do
expect(page).to have_selector('.js-daterange-picker', visible: true)
end
it 'shows the path navigation' do
expect(page).to have_selector(path_nav_selector)
end
it 'shows the filter bar' do
expect(page).to have_selector(filter_bar_selector, visible: false)
end
end
context 'with path navigation feature flag disabled' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: false)
visit analytics_cycle_analytics_path
select_group
end
it 'shows the path navigation' do
expect(page).not_to have_selector(path_nav_selector)
end
end
# Adding this context as part of a fix for https://gitlab.com/gitlab-org/gitlab/-/issues/233439
# This can be removed when the feature flag is removed
context 'create multiple value streams disabled' do
before do
stub_feature_flags(value_stream_analytics_create_multiple_value_streams: false)
visit analytics_cycle_analytics_path
select_group
end
it 'displays the list of stages' do
expect(page).to have_selector(stage_nav_selector, visible: true)
end
it 'displays the duration chart' do
expect(page).to have_selector(duration_stage_selector, visible: true)
end
end
def wait_for_stages_to_load
expect(page).to have_selector '.js-stage-table'
end
def select_group(name = group.name)
dropdown = page.find('.dropdown-groups')
dropdown.click
dropdown.find('.js-group-path', exact_text: name).click
wait_for_stages_to_load
end
def select_project
select_group
dropdown = page.find('.dropdown-projects')
dropdown.click
dropdown.find('a').click
dropdown.click
end
it 'displays empty text' do
[
'Value Stream Analytics can help you determine your team’s velocity',
'Start by choosing a group to see how your team is spending time. You can then drill down to the project level.'
].each do |content|
expect(page).to have_content(content)
end
end
shared_examples 'group value stream analytics' do
context 'summary table', :js do
it 'will display recent activity' do
page.within(find('.js-recent-activity')) do
expect(page).to have_content(_('Recent Activity'))
end
end
it 'will display time metrics' do
page.within(find('.js-recent-activity')) do
expect(page).to have_content(_('Time'))
end
end
end
context 'stage panel' do
it 'displays the stage table headers' do
expect(page).to have_selector('.stage-header', visible: true)
expect(page).to have_selector('.median-header', visible: true)
expect(page).to have_selector('.event-header', visible: true)
expect(page).to have_selector('.total-time-header', visible: true)
end
end
context 'stage nav' do
it 'displays the list of stages' do
expect(page).to have_selector(stage_nav_selector, visible: true)
end
it 'displays the default list of stages' do
stage_nav = page.find(stage_nav_selector)
%w[Issue Plan Code Test Review Staging Total].each do |item|
string_id = "CycleAnalytics|#{item}"
expect(stage_nav).to have_content(s_(string_id))
end
end
end
context 'path nav' do
before do
stub_feature_flags(value_stream_analytics_path_navigation: true)
end
it 'displays the default list of stages' do
path_nav = page.find(path_nav_selector)
%w[Issue Plan Code Test Review Staging Overview].each do |item|
string_id = "CycleAnalytics|#{item}"
expect(path_nav).to have_content(s_(string_id))
end
end
end
end
context 'with a group selected' do
card_metric_selector = '.js-recent-activity .js-metric-card-item'
before do
select_group
expect(page).to have_css(card_metric_selector)
end
it_behaves_like 'group value stream analytics'
it 'displays the number of issues' do
issue_count = page.all(card_metric_selector)[2]
expect(issue_count).to have_content(n_('New Issue', 'New Issues', 3))
expect(issue_count).to have_content('3')
end
it 'displays the number of deploys' do
deploys_count = page.all(card_metric_selector)[3]
expect(deploys_count).to have_content(n_('Deploy', 'Deploys', 0))
expect(deploys_count).to have_content('-')
end
it 'displays the deployment frequency' do
deployment_frequency = page.all(card_metric_selector).last
expect(deployment_frequency).to have_content(_('Deployment Frequency'))
expect(deployment_frequency).to have_content('-')
end
it 'displays the lead time' do
lead_time = page.all(card_metric_selector).first
expect(lead_time).to have_content(_('Lead Time'))
expect(lead_time).to have_content('-')
end
it 'displays the cycle time' do
cycle_time = page.all(card_metric_selector)[1]
expect(cycle_time).to have_content(_('Cycle Time'))
expect(cycle_time).to have_content('-')
end
end
context 'with a sub group selected' do
before do
select_group(sub_group.full_name)
end
it_behaves_like 'group value stream analytics'
end
def select_stage(name)
string_id = "CycleAnalyticsStage|#{name}"
page.find('.stage-nav .stage-nav-item .stage-name', text: s_(string_id), match: :prefer_exact).click
wait_for_requests
end
def create_merge_request(id, extra_params = {})
params = {
id: id,
target_branch: 'master',
source_project: project2,
source_branch: "feature-branch-#{id}",
title: "mr name#{id}",
created_at: 2.days.ago
}.merge(extra_params)
create(:merge_request, params)
end
context 'with lots of data', :js do
let_it_be(:issue) { create(:issue, project: project, created_at: 5.days.ago) }
around do |example|
Timecop.freeze { example.run }
end
before do
create_cycle(user, project, issue, mr, milestone, pipeline)
issue.metrics.update!(first_mentioned_in_commit_at: mr.created_at - 5.hours)
mr.metrics.update!(first_deployed_to_production_at: mr.created_at + 2.hours, merged_at: mr.created_at + 1.hour)
deploy_master(user, project, environment: 'staging')
deploy_master(user, project)
select_group
end
dummy_stages = [
{ title: 'Issue', description: 'Time before an issue gets scheduled', events_count: 1, median: '5 days' },
{ title: 'Plan', description: 'Time before an issue starts implementation', events_count: 0, median: 'Not enough data' },
{ title: 'Code', description: 'Time until first merge request', events_count: 1, median: 'about 5 hours' },
{ title: 'Test', description: 'Total test time for all commits/merges', events_count: 0, median: 'Not enough data' },
{ title: 'Review', description: 'Time between merge request creation and merge/close', events_count: 1, median: 'about 1 hour' },
{ title: 'Staging', description: 'From merge request merge until deploy to production', events_count: 1, median: 'about 1 hour' },
{ title: 'Total', description: 'From issue creation until deploy to production', events_count: 1, median: '5 days' }
]
it 'each stage will have median values', :sidekiq_might_not_need_inline do
stages = page.all('.stage-nav .stage-median').collect(&:text)
stages.each_with_index do |median, index|
expect(median).to eq(dummy_stages[index][:median])
end
end
it 'each stage will display the events description when selected', :sidekiq_might_not_need_inline do
dummy_stages.each do |stage|
select_stage(stage[:title])
if stage[:events_count] == 0
expect(page).not_to have_selector('.stage-events .events-description')
else
expect(page.find('.stage-events .events-description').text).to have_text(_(stage[:description]))
end
end
end
it 'each stage with events will display the stage events list when selected', :sidekiq_might_not_need_inline do
dummy_stages.each do |stage|
select_stage(stage[:title])
if stage[:events_count] == 0
expect(page).not_to have_selector('.stage-events .stage-event-item')
else
expect(page).to have_selector('.stage-events .stage-event-list')
expect(page.all('.stage-events .stage-event-item').length).to eq(stage[:events_count])
end
end
end
it 'each stage will be selectable' do
dummy_stages.each do |stage|
select_stage(stage[:title])
expect(page.find('.stage-nav .active .stage-name').text).to eq(stage[:title])
end
end
end
describe 'Tasks by type chart', :js do
context 'enabled' do
before do
stub_licensed_features(cycle_analytics_for_groups: true, type_of_work_analytics: true)
sign_in(user)
end
context 'with data available' do
before do
3.times do |i|
create(:labeled_issue, created_at: i.days.ago, project: create(:project, group: group), labels: [group_label1])
create(:labeled_issue, created_at: i.days.ago, project: create(:project, group: group), labels: [group_label2])
end
visit analytics_cycle_analytics_path
select_group
end
it 'displays the chart' do
expect(page).to have_text(s_('CycleAnalytics|Type of work'))
expect(page).to have_text(s_('CycleAnalytics|Tasks by type'))
end
it 'has 2 labels selected' do
expect(page).to have_text('Showing Issues and 2 labels')
end
it 'has chart filters' do
expect(page).to have_css('.js-tasks-by-type-chart-filters')
end
end
context 'no data available' do
before do
visit analytics_cycle_analytics_path
select_group
end
it 'shows the no data available message' do
expect(page).to have_text(s_('CycleAnalytics|Type of work'))
expect(page).to have_text(_('There is no data available. Please change your selection.'))
end
end
end
end
describe 'Customizable cycle analytics', :js do
custom_stage_name = 'Cool beans'
custom_stage_with_labels_name = 'Cool beans - now with labels'
start_event_identifier = :merge_request_created
end_event_identifier = :merge_request_merged
start_label_event = :issue_label_added
stop_label_event = :issue_label_removed
let(:add_stage_button) { '.js-add-stage-button' }
let(:params) { { name: custom_stage_name, start_event_identifier: start_event_identifier, end_event_identifier: end_event_identifier } }
let(:first_default_stage) { page.find('.stage-nav-item-cell', text: 'Issue').ancestor('.stage-nav-item') }
let(:first_custom_stage) { page.find('.stage-nav-item-cell', text: custom_stage_name).ancestor('.stage-nav-item') }
let(:nav) { page.find(stage_nav_selector) }
def create_custom_stage(parent_group = group)
Analytics::CycleAnalytics::Stages::CreateService.new(parent: parent_group, params: params, current_user: user).execute
end
def toggle_more_options(stage)
stage.hover
stage.find('.more-actions-toggle').click
end
def select_dropdown_option(name, value = start_event_identifier)
page.find("select[name='#{name}']").all('option').find { |item| item.value == value.to_s }.select_option
end
def select_dropdown_option_by_value(name, value, elem = 'option')
page.find("select[name='#{name}']").find("#{elem}[value=#{value}]").select_option
end
def wait_for_labels(field)
page.within("[name=#{field}]") do
find('.dropdown-toggle').click
wait_for_requests
expect(find('.dropdown-menu')).to have_selector('.dropdown-item')
end
end
def select_dropdown_label(field, index = 2)
page.find("[name=#{field}] .dropdown-menu").all('.dropdown-item')[index].click
end
def confirm_stage_order(stages)
page.within('.stage-nav>ul') do
stages.each_with_index do |stage, index|
expect(find("li:nth-child(#{index + 1})")).to have_content(stage)
end
end
end
def drag_from_index_to_index(from, to)
drag_to(selector: '.stage-nav>ul',
from_index: from,
to_index: to)
end
default_stage_order = %w[Issue Plan Code Test Review Staging Total].freeze
default_custom_stage_order = %w[Issue Plan Code Test Review Staging Total Cool\ beans].freeze
stages_near_middle_swapped = %w[Issue Plan Test Code Review Staging Total Cool\ beans].freeze
stage_dragged_to_top = %w[Review Issue Plan Code Test Staging Total Cool\ beans].freeze
stage_dragged_to_bottom = %w[Issue Plan Code Test Staging Total Cool\ beans Review].freeze
shared_examples 'manual ordering disabled' do
it 'does not allow stages to be draggable', :js do
confirm_stage_order(default_stage_order)
drag_from_index_to_index(0, 1)
confirm_stage_order(default_stage_order)
end
end
context 'enabled' do
context 'Manual ordering' do
before do
select_group
end
context 'with only default stages' do
it_behaves_like 'manual ordering disabled'
end
context 'with at least one custom stage', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/216745' do
shared_examples 'draggable stage' do |original_order, updated_order, start_index, end_index,|
before do
page.driver.browser.manage.window.resize_to(1650, 1150)
create_custom_stage
select_group
end
it 'allows a stage to be dragged' do
confirm_stage_order(original_order)
drag_from_index_to_index(start_index, end_index)
confirm_stage_order(updated_order)
end
it 'persists the order when a group is selected' do
drag_from_index_to_index(start_index, end_index)
select_group
confirm_stage_order(updated_order)
end
end
context 'dragging a stage to the top', :js do
it_behaves_like 'draggable stage', default_custom_stage_order, stage_dragged_to_top, 4, 0
end
context 'dragging a stage to the bottom', :js do
it_behaves_like 'draggable stage', default_custom_stage_order, stage_dragged_to_bottom, 4, 7
end
context 'dragging stages in the middle', :js do
it_behaves_like 'draggable stage', default_custom_stage_order, stages_near_middle_swapped, 2, 3
end
end
end
context 'Add a stage button' do
before do
select_group
end
it 'is visible' do
expect(page).to have_selector(add_stage_button, visible: true)
expect(page).to have_text(s_('CustomCycleAnalytics|Add a stage'))
end
it 'becomes active when clicked' do
expect(page).not_to have_selector("#{add_stage_button}.active")
find(add_stage_button).click
expect(page).to have_selector("#{add_stage_button}.active")
end
it 'displays the custom stage form when clicked' do
expect(page).not_to have_text(s_('CustomCycleAnalytics|New stage'))
page.find(add_stage_button).click
expect(page).to have_text(s_('CustomCycleAnalytics|New stage'))
end
end
shared_examples 'can create custom stages' do
context 'Custom stage form' do
let(:show_form_add_stage_button) { '.js-add-stage-button' }
before do
page.find(show_form_add_stage_button).click
wait_for_requests
end
context 'with empty fields' do
it 'submit button is disabled by default' do
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
end
shared_examples 'submits the form successfully' do |stage_name|
it 'submit button is enabled' do
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: false)
end
it 'submit button is disabled if the start event changes' do
select_dropdown_option 'custom-stage-start-event', 'issue_created'
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
it 'the custom stage is saved' do
click_button(s_('CustomCycleAnalytics|Add stage'))
expect(page).to have_selector('.stage-nav-item', text: stage_name)
end
it 'a confirmation message is displayed' do
fill_in 'custom-stage-name', with: stage_name
click_button(s_('CustomCycleAnalytics|Add stage'))
expect(page.find('.flash-notice')).to have_text(_("Your custom stage '%{title}' was created") % { title: stage_name })
end
it 'with a default name' do
fill_in 'custom-stage-name', with: 'issue'
click_button(s_('CustomCycleAnalytics|Add stage'))
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
end
context 'with all required fields set' do
before do
fill_in 'custom-stage-name', with: custom_stage_name
select_dropdown_option 'custom-stage-start-event', start_event_identifier
select_dropdown_option 'custom-stage-stop-event', end_event_identifier
end
it 'does not have label dropdowns' do
expect(page).not_to have_content(s_('CustomCycleAnalytics|Start event label'))
expect(page).not_to have_content(s_('CustomCycleAnalytics|Stop event label'))
end
it_behaves_like 'submits the form successfully', custom_stage_name
end
context 'with label based stages selected' do
before do
fill_in 'custom-stage-name', with: custom_stage_with_labels_name
select_dropdown_option_by_value 'custom-stage-start-event', start_label_event
select_dropdown_option_by_value 'custom-stage-stop-event', stop_label_event
end
it 'has label dropdowns' do
expect(page).to have_content(s_('CustomCycleAnalytics|Start event label'))
expect(page).to have_content(s_('CustomCycleAnalytics|Stop event label'))
end
it 'submit button is disabled' do
expect(page).to have_button(s_('CustomCycleAnalytics|Add stage'), disabled: true)
end
context 'with labels available' do
start_field = 'custom-stage-start-event-label'
end_field = 'custom-stage-stop-event-label'
it 'does not contain labels from outside the group' do
wait_for_labels(start_field)
menu = page.find("[name=#{start_field}] .dropdown-menu")
expect(menu).not_to have_content(other_label.name)
expect(menu).to have_content(first_label.name)
expect(menu).to have_content(second_label.name)
end
context 'with all required fields set' do
before do
wait_for_labels(start_field)
select_dropdown_label start_field, 1
wait_for_labels(end_field)
select_dropdown_label end_field, 2
end
it_behaves_like 'submits the form successfully', custom_stage_with_labels_name
end
end
end
end
end
shared_examples 'can edit custom stages' do
context 'Edit stage form' do
stage_form_class = '.custom-stage-form'
stage_save_button = '.js-save-stage'
name_field = 'custom-stage-name'
start_event_field = 'custom-stage-start-event'
end_event_field = 'custom-stage-stop-event'
updated_custom_stage_name = 'Extra uber cool stage'
def select_edit_stage
toggle_more_options(first_custom_stage)
click_button(_('Edit stage'))
end
context 'with no changes to the data' do
before do
select_edit_stage
end
it 'displays the editing stage form' do
expect(page.find(stage_form_class)).to have_text(s_('CustomCycleAnalytics|Editing stage'))
end
it 'prepoulates the stage data' do
expect(page.find_field(name_field).value).to eq custom_stage_name
expect(page.find_field(start_event_field).value).to eq start_event_identifier.to_s
expect(page.find_field(end_event_field).value).to eq end_event_identifier.to_s
end
it 'disables the submit form button' do
expect(page.find(stage_save_button)[:disabled]).to eq 'true'
end
end
context 'with changes' do
before do
select_edit_stage
end
it 'enables the submit button' do
fill_in name_field, with: updated_custom_stage_name
expect(page.find(stage_save_button)[:disabled]).to eq nil
end
it 'will persist updates to the stage' do
fill_in name_field, with: updated_custom_stage_name
page.find(stage_save_button).click
expect(page.find('.flash-notice')).to have_text(_('Stage data updated'))
expect(page.find(stage_nav_selector)).not_to have_text custom_stage_name
expect(page.find(stage_nav_selector)).to have_text updated_custom_stage_name
end
it 'disables the submit form button if incomplete' do
fill_in name_field, with: ''
expect(page.find(stage_save_button)[:disabled]).to eq 'true'
end
it 'with a default name' do
fill_in name_field, with: 'issue'
page.find(stage_save_button).click
expect(page.find(stage_form_class)).to have_text(s_('CustomCycleAnalytics|Stage name already exists'))
end
end
end
end
context 'with a group' do
context 'selected' do
before do
select_group
end
it_behaves_like 'can create custom stages' do
let(:first_label) { group_label1 }
let(:second_label) { group_label2 }
let(:other_label) { label }
end
end
context 'with a custom stage created' do
before do
create_custom_stage
select_group
expect(page).to have_text custom_stage_name
end
it_behaves_like 'can edit custom stages'
end
end
context 'with a sub group' do
context 'selected' do
before do
select_group(sub_group.full_name)
end
it_behaves_like 'can create custom stages' do
let(:first_label) { sub_group_label1 }
let(:second_label) { sub_group_label2 }
let(:other_label) { label }
end
end
context 'with a custom stage created' do
before do
create_custom_stage(sub_group)
select_group(sub_group.full_name)
expect(page).to have_text custom_stage_name
end
it_behaves_like 'can edit custom stages'
end
end
context 'Stage table' do
context 'default stages' do
def open_recover_stage_dropdown
find(add_stage_button).click
expect(page).to have_content(s_('CustomCycleAnalytics|New stage'))
expect(page).to have_content(_('Recover hidden stage'))
click_button(_('Recover hidden stage'))
within(:css, '.js-recover-hidden-stage-dropdown') do
expect(find('.dropdown-menu')).to have_content(_('Default stages'))
end
end
def active_stages
page.all('.stage-nav .stage-name').collect(&:text)
end
before do
select_group
toggle_more_options(first_default_stage)
end
it 'can be hidden' do
expect(first_default_stage.find('.more-actions-dropdown')).to have_text(_('Hide stage'))
end
it 'can not be edited' do
expect(first_default_stage.find('.more-actions-dropdown')).not_to have_text(_('Edit stage'))
end
it 'can not be removed' do
expect(first_default_stage.find('.more-actions-dropdown')).not_to have_text(_('Remove stage'))
end
context 'hidden' do
before do
click_button(_('Hide stage'))
# wait for the stage list to laod
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
end
it 'will not appear in the stage table' do
expect(active_stages).not_to include(s_('CycleAnalyticsStage|Issue'))
end
it 'can be recovered' do
open_recover_stage_dropdown
expect(page.find('.js-recover-hidden-stage-dropdown')).to have_text(s_('CycleAnalyticsStage|Issue'))
end
end
context 'recovered' do
before do
click_button(_('Hide stage'))
# wait for the stage list to laod
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
end
it 'will appear in the stage table' do
open_recover_stage_dropdown
click_button(s_('CycleAnalyticsStage|Issue'))
# wait for the stage list to laod
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
expect(page.find('.flash-notice')).to have_content(_('Stage data updated'))
expect(active_stages).to include(s_('CycleAnalyticsStage|Issue'))
end
end
end
context 'custom stages' do
before do
create_custom_stage
select_group
expect(page).to have_text custom_stage_name
toggle_more_options(first_custom_stage)
end
it 'can not be hidden' do
expect(first_custom_stage.find('.more-actions-dropdown')).not_to have_text(_('Hide stage'))
end
it 'can be edited' do
expect(first_custom_stage.find('.more-actions-dropdown')).to have_text(_('Edit stage'))
end
it 'can be removed' do
expect(first_custom_stage.find('.more-actions-dropdown')).to have_text(_('Remove stage'))
end
it 'will not appear in the stage table after being removed' do
nav = page.find(stage_nav_selector)
expect(nav).to have_text(custom_stage_name)
click_button(_('Remove stage'))
expect(page.find('.flash-notice')).to have_text(_('Stage removed'))
expect(nav).not_to have_text(custom_stage_name)
end
end
end
context 'Duration chart' do
let(:duration_chart_dropdown) { page.find(duration_stage_selector) }
let_it_be(:translated_default_stage_names) do
Gitlab::Analytics::CycleAnalytics::DefaultStages.names.map do |name|
stage = Analytics::CycleAnalytics::GroupStage.new(name: name)
Analytics::CycleAnalytics::StagePresenter.new(stage).title
end.freeze
end
def duration_chart_stages
duration_chart_dropdown.all('.dropdown-item').collect(&:text)
end
def toggle_duration_chart_dropdown
duration_chart_dropdown.click
end
before do
select_group
end
it 'has all the default stages' do
toggle_duration_chart_dropdown
expect(duration_chart_stages).to eq(translated_default_stage_names)
end
context 'hidden stage' do
before do
toggle_more_options(first_default_stage)
click_button(_('Hide stage'))
# wait for the stage list to laod
expect(nav).to have_content(s_('CycleAnalyticsStage|Plan'))
end
it 'will not appear in the duration chart dropdown' do
toggle_duration_chart_dropdown
expect(duration_chart_stages).not_to include(s_('CycleAnalyticsStage|Issue'))
end
end
end
end
end
describe 'Create value stream', :js do
let(:custom_value_stream_name) { "Test value stream" }
let(:value_stream_dropdown) { page.find(value_stream_selector) }
def toggle_value_stream_dropdown
value_stream_dropdown.click
end
before do
visit analytics_cycle_analytics_path
select_group
end
it 'can create a value stream' do
toggle_value_stream_dropdown
page.find_button(_('Create new Value Stream')).click
fill_in 'create-value-stream-name', with: custom_value_stream_name
page.find_button(_('Create Value Stream')).click
expect(page).to have_text(_("'%{name}' Value Stream created") % { name: custom_value_stream_name })
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Showing analytics' do
before do
sign_in user if user
end
# Using a path that is publicly accessible
subject { visit explore_projects_path }
context 'for regular users' do
let(:user) { create(:user) }
context 'with access to instance statistics or analytics features' do
it 'shows the analytics link' do
subject
expect(page).to have_link('Analytics')
end
end
context 'without access to instance statistics and analytics features' do
before do
stub_application_setting(instance_statistics_visibility_private: true)
end
it 'does not show the analytics link' do
subject
expect(page).not_to have_link('Analytics')
end
end
end
end
......@@ -197,32 +197,4 @@ RSpec.describe DashboardHelper, type: :helper do
it { is_expected.to eq(output) }
end
end
describe 'analytics_nav_url' do
before do
allow(helper).to receive(:current_user).and_return(user)
end
context 'when analytics features are disabled' do
context 'and user has access to instance statistics features' do
before do
allow(helper).to receive(:can?) { true }
end
it 'returns the instance statistics root path' do
expect(helper.analytics_nav_url).to match(instance_statistics_root_path)
end
end
context 'and user does not have access to instance statistics features' do
before do
allow(helper).to receive(:can?) { false }
end
it 'returns the not found path' do
expect(helper.analytics_nav_url).to match('errors/not_found')
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'admin dev ops analytics' do
let(:user) { create(:admin) }
before do
login_as(user)
end
it 'redirects from -/analytics to admin/dev_ops_score' do
get '/-/analytics'
expect(response).to redirect_to(admin_dev_ops_score_path)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Analytics' do
include Warden::Test::Helpers
it 'redirects to sign_in if user is not authenticated' do
expect(get('/-/analytics')).to route_to('analytics/analytics#index')
end
context 'when user is logged in' do
let(:user) { create(:user) }
before do
login_as(user)
end
context 'cycle_analytics feature flag is enabled by default' do
it 'succeeds' do
expect(Gitlab::Analytics).to receive(:cycle_analytics_enabled?).and_call_original
expect(get('/-/analytics/value_stream_analytics')).to route_to('analytics/cycle_analytics#show')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'layouts/nav/sidebar/_analytics' do
it_behaves_like 'has nav sidebar'
context 'top-level items' do
context 'when feature flags are enabled' do
it 'has `Analytics` link' do
render
expect(rendered).to have_content('Analytics')
expect(rendered).to include(analytics_root_path)
expect(rendered).to match(/<use xlink:href=".+?icons-.+?#chart">/)
end
context 'and user has access to instance statistics features' do
before do
allow(view).to receive(:can?) { true }
end
it 'has `DevOps Score` link' do
render
expect(rendered).to have_content('DevOps Score')
expect(rendered).to include(instance_statistics_dev_ops_score_index_path)
expect(rendered).to match(/<use xlink:href=".+?icons-.+?#comment">/)
end
it 'has `Cohorts` link' do
render
expect(rendered).to have_content('Cohorts')
expect(rendered).to include(instance_statistics_cohorts_path)
expect(rendered).to match(/<use xlink:href=".+?icons-.+?#users">/)
end
end
context 'and user does not have access to instance statistics features' do
before do
allow(view).to receive(:can?) { false }
end
it 'no instance statistics links are rendered' do
render
expect(rendered).not_to have_content('DevOps Score')
expect(rendered).not_to have_content('Cohorts')
end
end
end
context 'when user has access to instance statistics features' do
before do
allow(view).to receive(:can?) { true }
end
it 'has `DevOps Score` link' do
render
expect(rendered).to have_content('DevOps Score')
expect(rendered).to include(instance_statistics_dev_ops_score_index_path)
expect(rendered).to match(/<use xlink:href=".+?icons-.+?#comment">/)
end
it 'has `Cohorts` link' do
render
expect(rendered).to have_content('Cohorts')
expect(rendered).to include(instance_statistics_cohorts_path)
expect(rendered).to match(/<use xlink:href=".+?icons-.+?#users">/)
end
end
context 'and user does not have access to instance statistics features' do
before do
allow(view).to receive(:can?) { false }
end
it 'no instance statistics links are rendered' do
render
expect(rendered).not_to have_content('DevOps Score')
expect(rendered).not_to have_content('Cohorts')
end
end
end
end
......@@ -140,7 +140,6 @@ module API
end
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins'
optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated'
optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5
optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking'
......
......@@ -13189,9 +13189,6 @@ msgstr[1] ""
msgid "Instance Configuration"
msgstr ""
msgid "Instance Statistics visibility"
msgstr ""
msgid "Instance administrators group already exists"
msgstr ""
......@@ -17005,9 +17002,6 @@ msgstr ""
msgid "Only active this projects shows up in the search and on the dashboard."
msgstr ""
msgid "Only admins"
msgstr ""
msgid "Only admins can delete project"
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::CohortsController do
context 'as admin' do
let(:user) { create(:admin) }
before do
sign_in(user)
end
it 'renders 200' do
get :index
expect(response).to have_gitlab_http_status(:success)
end
describe 'GET #index' do
it_behaves_like 'tracking unique visits', :index do
let(:request_params) { {} }
let(:target_id) { 'i_analytics_cohorts' }
end
end
end
context 'as normal user' do
let(:user) { create(:user) }
before do
sign_in(user)
end
it 'renders a 404' do
get :index
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::DevOpsScoreController do
describe 'GET #show' do
context 'as admin' do
let(:user) { create(:admin) }
before do
sign_in(user)
end
it 'responds with success' do
get :show
expect(response).to have_gitlab_http_status(:success)
end
it_behaves_like 'tracking unique visits', :show do
let(:request_params) { {} }
let(:target_id) { 'i_analytics_dev_ops_score' }
end
end
end
context 'as normal user' do
let(:user) { create(:user) }
before do
sign_in(user)
end
it 'responds with 404' do
get :show
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe InstanceStatistics::CohortsController do
let(:user) { create(:user) }
before do
sign_in(user)
end
it_behaves_like 'instance statistics availability'
it 'renders a 404 when the usage ping is disabled' do
stub_application_setting(usage_ping_enabled: false)
get :index
expect(response).to have_gitlab_http_status(:not_found)
end
describe 'GET #index' do
it_behaves_like 'tracking unique visits', :index do
let(:request_params) { {} }
let(:target_id) { 'i_analytics_cohorts' }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe InstanceStatistics::DevOpsScoreController do
it_behaves_like 'instance statistics availability'
describe 'GET #index' do
let(:user) { create(:user) }
before do
sign_in(user)
end
it_behaves_like 'tracking unique visits', :index do
let(:request_params) { {} }
let(:target_id) { 'i_analytics_dev_ops_score' }
end
end
end
......@@ -12,7 +12,7 @@ RSpec.describe 'Cohorts page' do
it 'See users count per month' do
create_list(:user, 2)
visit instance_statistics_cohorts_path
visit admin_cohorts_path
expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
end
......
......@@ -2,13 +2,13 @@
require 'spec_helper'
RSpec.describe 'DevOps Score' do
RSpec.describe 'DevOps Score page' do
before do
sign_in(create(:admin))
end
it 'has dismissable intro callout', :js do
visit instance_statistics_dev_ops_score_index_path
visit admin_dev_ops_score_path
expect(page).to have_content 'Introducing Your DevOps Score'
......@@ -23,13 +23,13 @@ RSpec.describe 'DevOps Score' do
end
it 'shows empty state' do
visit instance_statistics_dev_ops_score_index_path
visit admin_dev_ops_score_path
expect(page).to have_content('Usage ping is not enabled')
end
it 'hides the intro callout' do
visit instance_statistics_dev_ops_score_index_path
visit admin_dev_ops_score_path
expect(page).not_to have_content 'Introducing Your DevOps Score'
end
......@@ -39,7 +39,7 @@ RSpec.describe 'DevOps Score' do
it 'shows empty state' do
stub_application_setting(usage_ping_enabled: true)
visit instance_statistics_dev_ops_score_index_path
visit admin_dev_ops_score_path
expect(page).to have_content('Data is still calculating')
end
......@@ -50,7 +50,7 @@ RSpec.describe 'DevOps Score' do
stub_application_setting(usage_ping_enabled: true)
create(:dev_ops_score_metric)
visit instance_statistics_dev_ops_score_index_path
visit admin_dev_ops_score_path
expect(page).to have_content(
'Issues created per active user 1.2 You 9.3 Lead 13.3%'
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Showing analytics' do
before do
sign_in user if user
end
# Using a path that is publicly accessible
subject { visit explore_projects_path }
context 'for unauthenticated users' do
let(:user) { nil }
it 'does not show the Analytics link' do
subject
expect(page).not_to have_link('Analytics')
end
end
context 'for regular users' do
let(:user) { create(:user) }
context 'when instance statistics are publicly available' do
before do
stub_application_setting(instance_statistics_visibility_private: false)
end
it 'shows the analytics link' do
subject
expect(page).to have_link('Analytics')
end
end
context 'when instance statistics are not publicly available' do
before do
stub_application_setting(instance_statistics_visibility_private: true)
end
it 'does not show the analytics link' do
subject
# Skipping this test on EE as there is an EE specifc spec for this functionality
# ee/spec/features/dashboards/analytics_spec.rb
skip if Gitlab.ee?
expect(page).not_to have_link('Analytics')
end
end
end
context 'for admins' do
let(:user) { create(:admin) }
it 'shows the analytics link' do
subject
expect(page).to have_link('Analytics')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Cohorts page', :js do
before do
sign_in(create(:admin))
end
it 'hides cohorts nav button when usage ping is disabled' do
stub_application_setting(usage_ping_enabled: false)
visit instance_statistics_root_path
expect(find('.nav-sidebar')).not_to have_content('Cohorts')
end
it 'shows cohorts nav button when usage ping is enabled' do
stub_application_setting(usage_ping_enabled: true)
visit instance_statistics_root_path
expect(find('.nav-sidebar')).to have_content('Cohorts')
end
end
......@@ -372,35 +372,13 @@ RSpec.describe GlobalPolicy do
describe 'read instance statistics' do
context 'regular user' do
it { is_expected.to be_allowed(:read_instance_statistics) }
context 'when instance statistics are set to private' do
before do
stub_application_setting(instance_statistics_visibility_private: true)
end
it { is_expected.not_to be_allowed(:read_instance_statistics) }
end
it { is_expected.to be_disallowed(:read_instance_statistics) }
end
context 'admin' do
context 'admin', :enable_admin_mode do
let(:current_user) { create(:admin) }
it { is_expected.to be_allowed(:read_instance_statistics) }
context 'when instance statistics are set to private' do
before do
stub_application_setting(instance_statistics_visibility_private: true)
end
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:read_instance_statistics) }
end
context 'when admin mode is disabled' do
it { is_expected.to be_disallowed(:read_instance_statistics) }
end
end
end
context 'anonymous' do
......
......@@ -31,7 +31,6 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['ecdsa_key_restriction']).to eq(0)
expect(json_response['ed25519_key_restriction']).to eq(0)
expect(json_response['performance_bar_allowed_group_id']).to be_nil
expect(json_response['instance_statistics_visibility_private']).to be(false)
expect(json_response['allow_local_requests_from_hooks_and_services']).to be(false)
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to be(false)
expect(json_response['allow_local_requests_from_system_hooks']).to be(true)
......@@ -104,7 +103,6 @@ RSpec.describe API::Settings, 'Settings' do
enforce_terms: true,
terms: 'Hello world!',
performance_bar_allowed_group_path: group.full_path,
instance_statistics_visibility_private: true,
diff_max_patch_bytes: 150_000,
default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3,
......@@ -146,7 +144,6 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['enforce_terms']).to be(true)
expect(json_response['terms']).to eq('Hello world!')
expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
expect(json_response['instance_statistics_visibility_private']).to be(true)
expect(json_response['diff_max_patch_bytes']).to eq(150_000)
expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
expect(json_response['local_markdown_version']).to eq(3)
......
......@@ -6,6 +6,6 @@ RSpec.describe 'Instance Statistics', 'routing' do
include RSpec::Rails::RequestExampleGroup
it "routes '/-/instance_statistics' to dev ops score" do
expect(get('/-/instance_statistics')).to redirect_to('/-/instance_statistics/dev_ops_score')
expect(get('/-/instance_statistics')).to redirect_to('/admin/dev_ops_score')
end
end
# frozen_string_literal: true
RSpec.shared_examples 'instance statistics availability' do
let(:user) { create(:user) }
before do
sign_in(user)
stub_application_setting(usage_ping_enabled: true)
end
describe 'GET #index' do
it 'is available when the feature is available publicly' do
get :index
expect(response).to have_gitlab_http_status(:success)
end
it 'renders a 404 when the feature is not available publicly' do
stub_application_setting(instance_statistics_visibility_private: true)
get :index
expect(response).to have_gitlab_http_status(:not_found)
end
context 'for admins' do
let(:user) { create(:admin) }
context 'when admin mode disabled' do
it 'forbids access when the feature is not available publicly' do
stub_application_setting(instance_statistics_visibility_private: true)
get :index
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when admin mode enabled', :enable_admin_mode do
it 'allows access when the feature is not available publicly' do
stub_application_setting(instance_statistics_visibility_private: true)
get :index
expect(response).to have_gitlab_http_status(:success)
end
end
end
end
end
......@@ -66,6 +66,14 @@ RSpec.describe 'layouts/nav/sidebar/_admin' do
it_behaves_like 'page has active tab', 'Messages'
end
context 'on analytics' do
before do
allow(controller).to receive(:controller_name).and_return('dev_ops_score')
end
it_behaves_like 'page has active tab', 'Analytics'
end
context 'on hooks' do
before do
allow(controller).to receive(:controller_name).and_return('hooks')
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'layouts/nav/sidebar/_instance_statistics' do
it_behaves_like 'has nav sidebar'
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