Commit 92753a6d authored by Adam Hegyi's avatar Adam Hegyi

Project level issues analytics

The change is behind a default enabled feature flag:
`project_level_issues_analytics`
parent c128d58a
......@@ -4,13 +4,14 @@ type: reference
# 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.
The default timespan is 13 months, which includes the current month, and the 12 months
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.
......
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
def project_analytics_navbar_links(project, current_user)
super + [
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
end
......@@ -24,6 +25,17 @@ module EE
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)
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)
......
......@@ -36,6 +36,7 @@ module EE
]
end
# rubocop: disable Metrics/CyclomaticComplexity
override :get_project_nav_tabs
def get_project_nav_tabs(project, current_user)
nav_tabs = super
......@@ -71,12 +72,17 @@ module EE
nav_tabs << :operations
end
if project.feature_available?(:issues_analytics) && can?(current_user, :read_project, project)
nav_tabs << :issues_analytics
end
if project.insights_available?
nav_tabs << :project_insights
end
nav_tabs
end
# rubocop: enable Metrics/CyclomaticComplexity
override :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
namespace :analytics do
resources :code_reviews, only: [:index]
resource :issues_analytics, only: [:show]
end
resources :approvers, only: :destroy
......
......@@ -3,81 +3,19 @@
require 'spec_helper'
describe Groups::IssuesAnalyticsController do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project1) { create(:project, :empty_repo, namespace: group) }
let(:project2) { create(:project, :empty_repo, namespace: group) }
before do
group.add_owner(user)
sign_in(user)
end
describe 'GET #show' do
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
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(:project2) { create(:project, :empty_repo, namespace: group) }
let_it_be(:issue1) { create(:issue, project: project1, confidential: true) }
let_it_be(:issue2) { create(:issue, project: project2, state: :closed) }
before do
group.add_owner(user)
sign_in(user)
end
context 'when user does not have permission to read group' do
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
let(:params) { { group_id: group.to_param } }
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 @@
module QA
context 'Plan', :reliable do
describe 'Issues analytics' do
shared_examples 'issues analytics page' do
let(:issue) do
Resource::Issue.fabricate_via_api!
end
......@@ -12,12 +12,24 @@ module QA
end
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|
expect(issues_analytics.graph).to be_visible
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
......@@ -3,102 +3,123 @@
require 'spec_helper'
describe 'Project navbar' do
it_behaves_like 'verified navigation bar' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:structure) do
[
{
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')
]
},
{
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
}
let(:analytics_nav_item) do
{
nav_item: _('Analytics'),
nav_sub_items: [
_('CI / CD Analytics'),
(_('Code Review') if Gitlab.ee?),
_('Repository Analytics'),
_('Value Stream Analytics')
]
end
}
end
before do
project.add_maintainer(user)
sign_in(user)
let(:structure) do
[
{
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)
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
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