Commit 35ab2384 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '32426-add-deep-links-to-dashboard-for-cycle-analytics' into 'master'

BE deep links to dashboard for cycle analytics

See merge request gitlab-org/gitlab!23235
parents 1e82af42 bb854dbc
...@@ -10,9 +10,9 @@ module CycleAnalyticsParams ...@@ -10,9 +10,9 @@ module CycleAnalyticsParams
end end
def cycle_analytics_group_params def cycle_analytics_group_params
return {} unless params[:cycle_analytics].present? return {} unless params.present?
params[:cycle_analytics].permit(:start_date, :created_after, :created_before, project_ids: []) params.permit(:group_id, :start_date, :created_after, :created_before, project_ids: [])
end end
def options(params) def options(params)
......
...@@ -201,7 +201,7 @@ export default { ...@@ -201,7 +201,7 @@ export default {
</script> </script>
<template> <template>
<div> <div class="js-cycle-analytics">
<div class="page-title-holder d-flex align-items-center"> <div class="page-title-holder d-flex align-items-center">
<h3 class="page-title">{{ __('Cycle Analytics') }}</h3> <h3 class="page-title">{{ __('Cycle Analytics') }}</h3>
</div> </div>
......
...@@ -7,7 +7,7 @@ export default () => { ...@@ -7,7 +7,7 @@ export default () => {
const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath } = el.dataset; const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath } = el.dataset;
return new Vue({ return new Vue({
el: '#js-cycle-analytics-app', el,
name: 'CycleAnalyticsApp', name: 'CycleAnalyticsApp',
store: createStore(), store: createStore(),
components: { components: {
......
...@@ -6,7 +6,6 @@ import { __ } from '~/locale'; ...@@ -6,7 +6,6 @@ import { __ } from '~/locale';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { dateFormats } from '../../shared/constants'; import { dateFormats } from '../../shared/constants';
import { nestQueryStringKeys } from '../utils';
const removeError = () => { const removeError = () => {
const flashEl = document.querySelector('.flash-alert'); const flashEl = document.querySelector('.flash-alert');
...@@ -224,10 +223,10 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => { ...@@ -224,10 +223,10 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
} = getters; } = getters;
dispatch('requestGroupStagesAndEvents'); dispatch('requestGroupStagesAndEvents');
return Api.cycleAnalyticsGroupStagesAndEvents( return Api.cycleAnalyticsGroupStagesAndEvents(fullPath, {
fullPath, start_date: created_after,
nestQueryStringKeys({ start_date: created_after, project_ids }, 'cycle_analytics'), project_ids,
) })
.then(({ data }) => dispatch('receiveGroupStagesAndEventsSuccess', data)) .then(({ data }) => dispatch('receiveGroupStagesAndEventsSuccess', data))
.catch(error => .catch(error =>
handleErrorOrRethrow({ handleErrorOrRethrow({
......
import { isString, isNumber } from 'underscore'; import { isNumber } from 'underscore';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { convertToSnakeCase } from '~/lib/utils/text_utility';
...@@ -91,14 +91,6 @@ export const transformRawTasksByTypeData = (data = []) => { ...@@ -91,14 +91,6 @@ export const transformRawTasksByTypeData = (data = []) => {
return data.map(d => convertObjectPropsToCamelCase(d, { deep: true })); return data.map(d => convertObjectPropsToCamelCase(d, { deep: true }));
}; };
export const nestQueryStringKeys = (obj = null, targetKey = '') => {
if (!obj || !isString(targetKey) || !targetKey.length) return {};
return Object.entries(obj).reduce((prev, [key, value]) => {
const customKey = `${targetKey}[${key}]`;
return { ...prev, [customKey]: value };
}, {});
};
/** /**
* Takes the duration data for selected stages, transforms the date values and returns * Takes the duration data for selected stages, transforms the date values and returns
* the data in a flattened array * the data in a flattened array
......
# frozen_string_literal: true # frozen_string_literal: true
class Analytics::CycleAnalyticsController < Analytics::ApplicationController class Analytics::CycleAnalyticsController < Analytics::ApplicationController
include CycleAnalyticsParams
check_feature_flag Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG check_feature_flag Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG
increment_usage_counter Gitlab::UsageDataCounters::CycleAnalyticsCounter, :views, only: :show increment_usage_counter Gitlab::UsageDataCounters::CycleAnalyticsCounter, :views, only: :show
...@@ -10,4 +12,20 @@ class Analytics::CycleAnalyticsController < Analytics::ApplicationController ...@@ -10,4 +12,20 @@ class Analytics::CycleAnalyticsController < Analytics::ApplicationController
push_frontend_feature_flag(:cycle_analytics_scatterplot_median_enabled, default_enabled: true) push_frontend_feature_flag(:cycle_analytics_scatterplot_median_enabled, default_enabled: true)
push_frontend_feature_flag(:tasks_by_type_chart) push_frontend_feature_flag(:tasks_by_type_chart)
end end
before_action :load_group, only: :show
before_action :load_project, only: :show
before_action :build_request_params, only: :show
def build_request_params
@request_params ||= Gitlab::Analytics::CycleAnalytics::RequestParams.new(allowed_params.merge(group: @group))
end
def allowed_params
params.permit(
:created_after,
:created_before,
project_ids: []
)
end
end end
...@@ -11,6 +11,7 @@ module EE ...@@ -11,6 +11,7 @@ module EE
super.tap do |options| super.tap do |options|
options[:branch] = params[:branch_name] options[:branch] = params[:branch_name]
options[:projects] = params[:project_ids] if params[:project_ids] options[:projects] = params[:project_ids] if params[:project_ids]
options[:group] = params[:group_id] if params[:group_id]
end end
end end
end end
......
- page_title _("Cycle Analytics") - page_title _("Cycle Analytics")
- initial_data = @request_params.valid? ? @request_params.to_data_attributes : {}
#js-cycle-analytics-app{ data: { "empty-state-svg-path" => image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), "no-data-svg-path" => image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), "no-access-svg-path" => image_path("illustrations/analytics/no-access.svg") } } #js-cycle-analytics-app{ data: initial_data.merge({ empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"),
no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"),
no_access_svg_path: image_path("illustrations/analytics/no-access.svg") }) }
...@@ -9,24 +9,51 @@ module Gitlab ...@@ -9,24 +9,51 @@ module Gitlab
include ActiveModel::Attributes include ActiveModel::Attributes
MAX_RANGE_DAYS = 180.days.freeze MAX_RANGE_DAYS = 180.days.freeze
DEFAULT_DATE_RANGE = 30.days
attr_writer :project_ids attr_writer :project_ids
attribute :created_after, :date attribute :created_after, :date
attribute :created_before, :date attribute :created_before, :date
attr_accessor :group
validates :created_after, presence: true validates :created_after, presence: true
validates :created_before, presence: true validates :created_before, presence: true
validate :validate_created_before validate :validate_created_before
validate :validate_date_range validate :validate_date_range
def initialize(params = {})
params[:created_before] ||= Date.today.at_end_of_day
params[:created_after] ||= default_created_after(params[:created_before])
super(params)
end
def project_ids def project_ids
Array(@project_ids) Array(@project_ids)
end end
def to_data_attributes
{}.tap do |attrs|
attrs[:group] = group_data_attributes if group
attrs[:project_ids] = project_ids if project_ids.any?
attrs[:created_after] = created_after.iso8601
attrs[:created_before] = created_before.iso8601
end
end
private private
def group_data_attributes
{
id: group.id,
name: group.name,
full_path: group.full_path
}
end
def validate_created_before def validate_created_before
return if created_after.nil? || created_before.nil? return if created_after.nil? || created_before.nil?
...@@ -40,6 +67,14 @@ module Gitlab ...@@ -40,6 +67,14 @@ module Gitlab
errors.add(:created_after, s_('CycleAnalytics|The given date range is larger than 180 days')) errors.add(:created_after, s_('CycleAnalytics|The given date range is larger than 180 days'))
end end
end end
def default_created_after(start_date = nil)
if start_date
(start_date.to_time - DEFAULT_DATE_RANGE).to_datetime
else
DEFAULT_DATE_RANGE.ago.utc.beginning_of_day
end
end
end end
end end
end end
......
...@@ -49,9 +49,7 @@ describe Groups::CycleAnalytics::EventsController do ...@@ -49,9 +49,7 @@ describe Groups::CycleAnalytics::EventsController do
get(:issue, get(:issue,
params: { params: {
group_id: group.name, group_id: group.name,
cycle_analytics: {
project_ids: [project.id] project_ids: [project.id]
}
}, },
format: :json) format: :json)
......
...@@ -50,9 +50,7 @@ describe Groups::CycleAnalyticsController do ...@@ -50,9 +50,7 @@ describe Groups::CycleAnalyticsController do
get(:show, get(:show,
params: { params: {
group_id: group.name, group_id: group.name,
cycle_analytics: {
project_ids: [project.id] project_ids: [project.id]
}
}, },
format: :json) format: :json)
......
...@@ -7,7 +7,6 @@ import { ...@@ -7,7 +7,6 @@ import {
eventToOption, eventToOption,
eventsByIdentifier, eventsByIdentifier,
getLabelEventsIdentifiers, getLabelEventsIdentifiers,
nestQueryStringKeys,
flattenDurationChartData, flattenDurationChartData,
getDurationChartData, getDurationChartData,
getDurationChartMedianData, getDurationChartMedianData,
...@@ -126,32 +125,6 @@ describe('Cycle analytics utils', () => { ...@@ -126,32 +125,6 @@ describe('Cycle analytics utils', () => {
}); });
}); });
describe('nestQueryStringKeys', () => {
const targetKey = 'foo';
const obj = { bar: 10, baz: 'awesome', qux: false, boo: ['lol', 'something'] };
it('will return an object with each key nested under the targetKey', () => {
expect(nestQueryStringKeys(obj, targetKey)).toEqual({
'foo[bar]': 10,
'foo[baz]': 'awesome',
'foo[qux]': false,
'foo[boo]': ['lol', 'something'],
});
});
it('returns an empty object if the targetKey is not a valid string', () => {
['', null, {}, []].forEach(badStr => {
expect(nestQueryStringKeys(obj, badStr)).toEqual({});
});
});
it('will return an empty object if given an empty object', () => {
[{}, null, [], ''].forEach(tarObj => {
expect(nestQueryStringKeys(tarObj, targetKey)).toEqual({});
});
});
});
describe('flattenDurationChartData', () => { describe('flattenDurationChartData', () => {
it('flattens the data as expected', () => { it('flattens the data as expected', () => {
const flattenedData = flattenDurationChartData(transformedDurationData); const flattenedData = flattenDurationChartData(transformedDurationData);
......
...@@ -17,8 +17,10 @@ describe Gitlab::Analytics::CycleAnalytics::RequestParams do ...@@ -17,8 +17,10 @@ describe Gitlab::Analytics::CycleAnalytics::RequestParams do
params[:created_before] = nil params[:created_before] = nil
end end
it 'is invalid' do it 'is valid' do
expect(subject).not_to be_valid Timecop.travel '2019-03-01' do
expect(subject).to be_valid
end
end end
end end
...@@ -82,4 +84,26 @@ describe Gitlab::Analytics::CycleAnalytics::RequestParams do ...@@ -82,4 +84,26 @@ describe Gitlab::Analytics::CycleAnalytics::RequestParams do
it { expect(subject.project_ids).to eq([]) } it { expect(subject.project_ids).to eq([]) }
end end
end end
describe 'optional `group_id`' do
it { expect(subject.group).to be_nil }
context 'when `group_id` is not empty' do
let(:group_id) { 'ca-test-group' }
before do
params[:group] = group_id
end
it { expect(subject.group).to eq(group_id) }
end
context 'when `group_id` is nil' do
before do
params[:group] = nil
end
it { expect(subject.group).to eq(nil) }
end
end
end end
...@@ -26,7 +26,7 @@ describe 'cycle analytics events' do ...@@ -26,7 +26,7 @@ describe 'cycle analytics events' do
context 'when date range parameters are given' do context 'when date range parameters are given' do
it 'filter by `created_after`' do it 'filter by `created_after`' do
params = { cycle_analytics: { created_after: issue.created_at - 5.days } } params = { created_after: issue.created_at - 5.days }
get group_cycle_analytics_issue_path(group, params: params, format: :json) get group_cycle_analytics_issue_path(group, params: params, format: :json)
...@@ -34,7 +34,7 @@ describe 'cycle analytics events' do ...@@ -34,7 +34,7 @@ describe 'cycle analytics events' do
end end
it 'filters by `created_after` where no events should be found' do it 'filters by `created_after` where no events should be found' do
params = { cycle_analytics: { created_after: issue.created_at + 5.days } } params = { created_after: issue.created_at + 5.days }
get group_cycle_analytics_issue_path(group, params: params, format: :json) get group_cycle_analytics_issue_path(group, params: params, format: :json)
...@@ -42,7 +42,7 @@ describe 'cycle analytics events' do ...@@ -42,7 +42,7 @@ describe 'cycle analytics events' do
end end
it 'filter by `created_after` and `created_before`' do it 'filter by `created_after` and `created_before`' do
params = { cycle_analytics: { created_after: issue.created_at - 5.days, created_before: issue.created_at + 5.days } } params = { created_after: issue.created_at - 5.days, created_before: issue.created_at + 5.days }
get group_cycle_analytics_issue_path(group, params: params, format: :json) get group_cycle_analytics_issue_path(group, params: params, format: :json)
...@@ -50,7 +50,7 @@ describe 'cycle analytics events' do ...@@ -50,7 +50,7 @@ describe 'cycle analytics events' do
end end
it 'raises error when date cannot be parsed' do it 'raises error when date cannot be parsed' do
params = { cycle_analytics: { created_after: 'invalid' } } params = { created_after: 'invalid' }
expect do expect do
get group_cycle_analytics_issue_path(group, params: params, format: :json) get group_cycle_analytics_issue_path(group, params: params, format: :json)
......
...@@ -113,31 +113,41 @@ RSpec.shared_examples 'cycle analytics data endpoint examples' do ...@@ -113,31 +113,41 @@ RSpec.shared_examples 'cycle analytics data endpoint examples' do
end end
end end
context 'when `created_after` is missing' do context 'when `created_before` is missing' do
before do before do
params.delete(:created_after) params.delete(:created_before)
end end
include_examples 'example for invalid parameter' it 'succeeds' do
Timecop.travel '2019-04-01' do
subject
expect(response).to be_successful
end
end
end end
context 'when `created_after` is invalid' do context 'when `created_after` is missing' do
before do before do
params[:created_after] = 'not-a-date' params.delete(:created_after)
end end
include_examples 'example for invalid parameter' it 'succeeds' do
subject
expect(response).to be_successful
end
end end
context 'when `created_before` is missing' do context 'when `created_after` is invalid' do
before do before do
params.delete(:created_before) params[:created_after] = 'not-a-date'
end end
include_examples 'example for invalid parameter' include_examples 'example for invalid parameter'
end end
context 'when `created_after` is invalid' do context 'when `created_before` is invalid' do
before do before do
params[:created_before] = 'not-a-date' params[:created_before] = 'not-a-date'
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