Commit 9ffd2186 authored by James Lopez's avatar James Lopez

Merge branch '196561-project-level-issues-analytics' into 'master'

Project level issues analytics

Closes #196561

See merge request gitlab-org/gitlab!25417
parents 1841bf43 92753a6d
...@@ -4,13 +4,14 @@ type: reference ...@@ -4,13 +4,14 @@ type: reference
# Issues Analytics **(PREMIUM)** # Issues Analytics **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7478) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.5. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7478) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.5.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/196561) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.9 at the project level.
Issues Analytics is a bar graph which illustrates the number of issues created each month. Issues Analytics is a bar graph which illustrates the number of issues created each month.
The default timespan is 13 months, which includes the current month, and the 12 months The default timespan is 13 months, which includes the current month, and the 12 months
prior. prior.
To access the chart, navigate to a group's sidebar and select **Analytics > Issues Analytics**. To access the chart, navigate to your group or project sidebar and select **{chart}** **Analytics > Issues Analytics**.
Hover over each bar to see the total number of issues. Hover over each bar to see the total number of issues.
......
import initIssuesAnalytics from 'ee/issues_analytics';
document.addEventListener('DOMContentLoaded', () => {
initIssuesAnalytics();
});
# frozen_string_literal: true
class Projects::Analytics::IssuesAnalyticsController < Projects::ApplicationController
include IssuableCollections
before_action :authorize_read_issue_analytics!
def show
respond_to do |format|
format.html
format.json do
@chart_data =
IssuablesAnalytics.new(issuables: issuables_collection, months_back: params[:months_back]).data
render json: @chart_data
end
end
end
private
def authorize_read_issue_analytics!
render_404 unless project.feature_available?(:issues_analytics)
end
def finder_type
IssuesFinder
end
def default_state
'all'
end
def preload_for_collection
nil
end
end
...@@ -8,7 +8,8 @@ module EE ...@@ -8,7 +8,8 @@ module EE
def project_analytics_navbar_links(project, current_user) def project_analytics_navbar_links(project, current_user)
super + [ super + [
insights_navbar_link(project, current_user), insights_navbar_link(project, current_user),
code_review_analytics_navbar_link(project, current_user) code_review_analytics_navbar_link(project, current_user),
project_issues_analytics_navbar_link(project, current_user)
].compact ].compact
end end
...@@ -24,6 +25,17 @@ module EE ...@@ -24,6 +25,17 @@ module EE
private private
def project_issues_analytics_navbar_link(project, current_user)
return unless ::Feature.enabled?(:project_level_issues_analytics, project, default_enabled: true)
return unless project_nav_tab?(:issues_analytics)
navbar_sub_item(
title: _('Issues Analytics'),
path: 'issues_analytics#show',
link: project_analytics_issues_analytics_path(project)
)
end
def productivity_analytics_navbar_link(group, current_user) def productivity_analytics_navbar_link(group, current_user)
return unless ::Feature.enabled?(:analytics_pages_under_group_analytics_sidebar, group, default_enabled: true) return unless ::Feature.enabled?(:analytics_pages_under_group_analytics_sidebar, group, default_enabled: true)
return unless ::Feature.enabled?(:group_level_productivity_analytics, default_enabled: true) return unless ::Feature.enabled?(:group_level_productivity_analytics, default_enabled: true)
......
...@@ -36,6 +36,7 @@ module EE ...@@ -36,6 +36,7 @@ module EE
] ]
end end
# rubocop: disable Metrics/CyclomaticComplexity
override :get_project_nav_tabs override :get_project_nav_tabs
def get_project_nav_tabs(project, current_user) def get_project_nav_tabs(project, current_user)
nav_tabs = super nav_tabs = super
...@@ -71,12 +72,17 @@ module EE ...@@ -71,12 +72,17 @@ module EE
nav_tabs << :operations nav_tabs << :operations
end end
if project.feature_available?(:issues_analytics) && can?(current_user, :read_project, project)
nav_tabs << :issues_analytics
end
if project.insights_available? if project.insights_available?
nav_tabs << :project_insights nav_tabs << :project_insights
end end
nav_tabs nav_tabs
end end
# rubocop: enable Metrics/CyclomaticComplexity
override :tab_ability_map override :tab_ability_map
def tab_ability_map def tab_ability_map
......
- page_title _('Issues Analytics')
= render 'shared/issuable/search_bar', type: :issues_analytics, show_sorting_dropdown: false
#js-issues-analytics{ data: { endpoint: project_analytics_issues_analytics_path(@project) } }
---
title: Introduce Project level issues analytics
merge_request: 25417
author:
type: added
...@@ -93,6 +93,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -93,6 +93,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :analytics do namespace :analytics do
resources :code_reviews, only: [:index] resources :code_reviews, only: [:index]
resource :issues_analytics, only: [:show]
end end
resources :approvers, only: :destroy resources :approvers, only: :destroy
......
...@@ -3,81 +3,19 @@ ...@@ -3,81 +3,19 @@
require 'spec_helper' require 'spec_helper'
describe Groups::IssuesAnalyticsController do describe Groups::IssuesAnalyticsController do
let(:user) { create(:user) } it_behaves_like 'issues analytics controller' do
let(:group) { create(:group) } let_it_be(:user) { create(:user) }
let(:project1) { create(:project, :empty_repo, namespace: group) } let_it_be(:group) { create(:group) }
let(:project2) { create(:project, :empty_repo, namespace: group) } let_it_be(:project1) { create(:project, :empty_repo, namespace: group) }
let_it_be(:project2) { create(:project, :empty_repo, namespace: group) }
before do let_it_be(:issue1) { create(:issue, project: project1, confidential: true) }
group.add_owner(user) let_it_be(:issue2) { create(:issue, project: project2, state: :closed) }
sign_in(user)
end before do
group.add_owner(user)
describe 'GET #show' do sign_in(user)
context 'when issues analytics is not available for license' do
it 'renders 404' do
get :show, params: { group_id: group.to_param }
expect(response).to have_gitlab_http_status(:not_found)
end
end end
context 'when user does not have permission to read group' do let(:params) { { group_id: group.to_param } }
let(:user) { create(:user) }
before do
sign_in(user)
end
it 'renders 404' do
get :show, params: { group_id: group.to_param }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when issues analytics is available for license' do
before do
stub_licensed_features(issues_analytics: true)
end
context 'as HTML' do
it 'renders show template' do
get :show, params: { group_id: group.to_param, months_back: 2 }
expect(response).to render_template(:show)
end
end
context 'as JSON' do
let!(:issue1) { create(:issue, project: project1, confidential: true) }
let!(:issue2) { create(:issue, project: project2, state: :closed) }
it 'renders chart data as JSON' do
expected_result = { issue1.created_at.strftime(IssuablesAnalytics::DATE_FORMAT) => 2 }
get :show, params: { group_id: group.to_param }, format: :json
expect(json_response).to include(expected_result)
end
context 'when user cannot view issues' do
let(:guest) { create(:user) }
before do
group.add_guest(guest)
sign_in(guest)
end
it 'does not count issues which user cannot view' do
expected_result = { issue1.created_at.strftime(IssuablesAnalytics::DATE_FORMAT) => 1 }
get :show, params: { group_id: group.to_param }, format: :json
expect(json_response).to include(expected_result)
end
end
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Analytics::IssuesAnalyticsController do
it_behaves_like 'issues analytics controller' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project1) { create(:project, :empty_repo, namespace: group) }
let_it_be(:issue1) { create(:issue, project: project1, confidential: true) }
let_it_be(:issue2) { create(:issue, project: project1, state: :closed) }
before do
group.add_owner(user)
sign_in(user)
end
let(:params) { { namespace_id: group.to_param, project_id: project1.to_param } }
end
end
# frozen_string_literal: true
RSpec.shared_examples 'issues analytics controller' do
describe 'GET #show' do
subject { get :show, params: params }
context 'when issues analytics is not available for license' do
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user does not have permission to read the resource' do
let(:user) { create(:user) }
before do
sign_in(user)
end
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when issues analytics is available for license' do
before do
stub_licensed_features(issues_analytics: true)
end
context 'as HTML' do
before do
params[:months_back] = 2
end
it 'renders show template' do
subject
expect(response).to render_template(:show)
end
end
context 'as JSON' do
subject { get :show, params: params, format: :json }
it 'renders chart data as JSON' do
expected_result = { issue1.created_at.strftime(IssuablesAnalytics::DATE_FORMAT) => 2 }
subject
expect(json_response).to include(expected_result)
end
context 'when user cannot view issues' do
let(:guest) { create(:user) }
before do
group.add_guest(guest)
sign_in(guest)
end
it 'does not count issues which user cannot view' do
expected_result = { issue1.created_at.strftime(IssuablesAnalytics::DATE_FORMAT) => 1 }
subject
expect(json_response).to include(expected_result)
end
end
end
end
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module QA module QA
context 'Plan', :reliable do context 'Plan', :reliable do
describe 'Issues analytics' do shared_examples 'issues analytics page' do
let(:issue) do let(:issue) do
Resource::Issue.fabricate_via_api! Resource::Issue.fabricate_via_api!
end end
...@@ -12,12 +12,24 @@ module QA ...@@ -12,12 +12,24 @@ module QA
end end
it 'displays a graph' do it 'displays a graph' do
page.visit("#{issue.project.group.web_url}/-/issues_analytics") page.visit(analytics_path)
EE::Page::Group::IssuesAnalytics.perform do |issues_analytics| EE::Page::Group::IssuesAnalytics.perform do |issues_analytics|
expect(issues_analytics.graph).to be_visible expect(issues_analytics.graph).to be_visible
end end
end end
end end
describe 'Group level issues analytics' do
it_behaves_like 'issues analytics page' do
let(:analytics_path) { "#{issue.project.group.web_url}/-/issues_analytics" }
end
end
describe 'Project level issues analytics' do
it_behaves_like 'issues analytics page' do
let(:analytics_path) { "#{issue.project.web_url}/-/analytics/issues_analytics" }
end
end
end end
end end
...@@ -3,102 +3,123 @@ ...@@ -3,102 +3,123 @@
require 'spec_helper' require 'spec_helper'
describe 'Project navbar' do describe 'Project navbar' do
it_behaves_like 'verified navigation bar' do let(:user) { create(:user) }
let(:user) { create(:user) } let(:project) { create(:project, :repository) }
let(:project) { create(:project, :repository) }
let(:structure) do let(:analytics_nav_item) do
[ {
{ nav_item: _('Analytics'),
nav_item: _('Project overview'), nav_sub_items: [
nav_sub_items: [ _('CI / CD Analytics'),
_('Details'), (_('Code Review') if Gitlab.ee?),
_('Activity'), _('Repository Analytics'),
_('Releases') _('Value Stream Analytics')
]
},
{
nav_item: _('Repository'),
nav_sub_items: [
_('Files'),
_('Commits'),
_('Branches'),
_('Tags'),
_('Contributors'),
_('Graph'),
_('Compare'),
(_('Locked Files') if Gitlab.ee?)
]
},
{
nav_item: _('Issues'),
nav_sub_items: [
_('List'),
_('Boards'),
_('Labels'),
_('Milestones')
]
},
{
nav_item: _('Merge Requests'),
nav_sub_items: []
},
{
nav_item: _('CI / CD'),
nav_sub_items: [
_('Pipelines'),
_('Jobs'),
_('Artifacts'),
_('Schedules')
]
},
{
nav_item: _('Operations'),
nav_sub_items: [
_('Metrics'),
_('Environments'),
_('Error Tracking'),
_('Serverless'),
_('Kubernetes')
]
},
{
nav_item: _('Analytics'),
nav_sub_items: [
_('CI / CD Analytics'),
(_('Code Review') if Gitlab.ee?),
_('Repository Analytics'),
_('Value Stream Analytics')
]
},
{
nav_item: _('Wiki'),
nav_sub_items: []
},
{
nav_item: _('Snippets'),
nav_sub_items: []
},
{
nav_item: _('Settings'),
nav_sub_items: [
_('General'),
_('Members'),
_('Integrations'),
_('Repository'),
_('CI / CD'),
_('Operations'),
(_('Audit Events') if Gitlab.ee?)
].compact
}
] ]
end }
end
before do let(:structure) do
project.add_maintainer(user) [
sign_in(user) {
nav_item: _('Project overview'),
nav_sub_items: [
_('Details'),
_('Activity'),
_('Releases')
]
},
{
nav_item: _('Repository'),
nav_sub_items: [
_('Files'),
_('Commits'),
_('Branches'),
_('Tags'),
_('Contributors'),
_('Graph'),
_('Compare'),
(_('Locked Files') if Gitlab.ee?)
]
},
{
nav_item: _('Issues'),
nav_sub_items: [
_('List'),
_('Boards'),
_('Labels'),
_('Milestones')
]
},
{
nav_item: _('Merge Requests'),
nav_sub_items: []
},
{
nav_item: _('CI / CD'),
nav_sub_items: [
_('Pipelines'),
_('Jobs'),
_('Artifacts'),
_('Schedules')
]
},
{
nav_item: _('Operations'),
nav_sub_items: [
_('Metrics'),
_('Environments'),
_('Error Tracking'),
_('Serverless'),
_('Kubernetes')
]
},
analytics_nav_item,
{
nav_item: _('Wiki'),
nav_sub_items: []
},
{
nav_item: _('Snippets'),
nav_sub_items: []
},
{
nav_item: _('Settings'),
nav_sub_items: [
_('General'),
_('Members'),
_('Integrations'),
_('Repository'),
_('CI / CD'),
_('Operations'),
(_('Audit Events') if Gitlab.ee?)
].compact
}
]
end
before do
project.add_maintainer(user)
sign_in(user)
end
it_behaves_like 'verified navigation bar' do
before do
visit project_path(project) visit project_path(project)
end end
end end
if Gitlab.ee?
context 'when issues analytics is available' do
before do
stub_licensed_features(issues_analytics: true)
analytics_nav_item[:nav_sub_items] << _('Issues Analytics')
analytics_nav_item[:nav_sub_items].sort!
visit project_path(project)
end
it_behaves_like 'verified navigation bar'
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