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
end
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
def options(params)
......
......@@ -201,7 +201,7 @@ export default {
</script>
<template>
<div>
<div class="js-cycle-analytics">
<div class="page-title-holder d-flex align-items-center">
<h3 class="page-title">{{ __('Cycle Analytics') }}</h3>
</div>
......
......@@ -7,7 +7,7 @@ export default () => {
const { emptyStateSvgPath, noDataSvgPath, noAccessSvgPath } = el.dataset;
return new Vue({
el: '#js-cycle-analytics-app',
el,
name: 'CycleAnalyticsApp',
store: createStore(),
components: {
......
......@@ -6,7 +6,6 @@ import { __ } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types';
import { dateFormats } from '../../shared/constants';
import { nestQueryStringKeys } from '../utils';
const removeError = () => {
const flashEl = document.querySelector('.flash-alert');
......@@ -224,10 +223,10 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
} = getters;
dispatch('requestGroupStagesAndEvents');
return Api.cycleAnalyticsGroupStagesAndEvents(
fullPath,
nestQueryStringKeys({ start_date: created_after, project_ids }, 'cycle_analytics'),
)
return Api.cycleAnalyticsGroupStagesAndEvents(fullPath, {
start_date: created_after,
project_ids,
})
.then(({ data }) => dispatch('receiveGroupStagesAndEventsSuccess', data))
.catch(error =>
handleErrorOrRethrow({
......
import { isString, isNumber } from 'underscore';
import { isNumber } from 'underscore';
import dateFormat from 'dateformat';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
......@@ -91,14 +91,6 @@ export const transformRawTasksByTypeData = (data = []) => {
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
* the data in a flattened array
......
# frozen_string_literal: true
class Analytics::CycleAnalyticsController < Analytics::ApplicationController
include CycleAnalyticsParams
check_feature_flag Gitlab::Analytics::CYCLE_ANALYTICS_FEATURE_FLAG
increment_usage_counter Gitlab::UsageDataCounters::CycleAnalyticsCounter, :views, only: :show
......@@ -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(:tasks_by_type_chart)
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
......@@ -11,6 +11,7 @@ module EE
super.tap do |options|
options[:branch] = params[:branch_name]
options[:projects] = params[:project_ids] if params[:project_ids]
options[:group] = params[:group_id] if params[:group_id]
end
end
end
......
- 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
include ActiveModel::Attributes
MAX_RANGE_DAYS = 180.days.freeze
DEFAULT_DATE_RANGE = 30.days
attr_writer :project_ids
attribute :created_after, :date
attribute :created_before, :date
attr_accessor :group
validates :created_after, presence: true
validates :created_before, presence: true
validate :validate_created_before
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
Array(@project_ids)
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
def group_data_attributes
{
id: group.id,
name: group.name,
full_path: group.full_path
}
end
def validate_created_before
return if created_after.nil? || created_before.nil?
......@@ -40,6 +67,14 @@ module Gitlab
errors.add(:created_after, s_('CycleAnalytics|The given date range is larger than 180 days'))
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
......
......@@ -49,9 +49,7 @@ describe Groups::CycleAnalytics::EventsController do
get(:issue,
params: {
group_id: group.name,
cycle_analytics: {
project_ids: [project.id]
}
project_ids: [project.id]
},
format: :json)
......
......@@ -50,9 +50,7 @@ describe Groups::CycleAnalyticsController do
get(:show,
params: {
group_id: group.name,
cycle_analytics: {
project_ids: [project.id]
}
project_ids: [project.id]
},
format: :json)
......
......@@ -7,7 +7,6 @@ import {
eventToOption,
eventsByIdentifier,
getLabelEventsIdentifiers,
nestQueryStringKeys,
flattenDurationChartData,
getDurationChartData,
getDurationChartMedianData,
......@@ -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', () => {
it('flattens the data as expected', () => {
const flattenedData = flattenDurationChartData(transformedDurationData);
......
......@@ -17,8 +17,10 @@ describe Gitlab::Analytics::CycleAnalytics::RequestParams do
params[:created_before] = nil
end
it 'is invalid' do
expect(subject).not_to be_valid
it 'is valid' do
Timecop.travel '2019-03-01' do
expect(subject).to be_valid
end
end
end
......@@ -82,4 +84,26 @@ describe Gitlab::Analytics::CycleAnalytics::RequestParams do
it { expect(subject.project_ids).to eq([]) }
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
......@@ -26,7 +26,7 @@ describe 'cycle analytics events' do
context 'when date range parameters are given' 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)
......@@ -34,7 +34,7 @@ describe 'cycle analytics events' do
end
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)
......@@ -42,7 +42,7 @@ describe 'cycle analytics events' do
end
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)
......@@ -50,7 +50,7 @@ describe 'cycle analytics events' do
end
it 'raises error when date cannot be parsed' do
params = { cycle_analytics: { created_after: 'invalid' } }
params = { created_after: 'invalid' }
expect do
get group_cycle_analytics_issue_path(group, params: params, format: :json)
......
......@@ -113,31 +113,41 @@ RSpec.shared_examples 'cycle analytics data endpoint examples' do
end
end
context 'when `created_after` is missing' do
context 'when `created_before` is missing' do
before do
params.delete(:created_after)
params.delete(:created_before)
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
context 'when `created_after` is invalid' do
context 'when `created_after` is missing' do
before do
params[:created_after] = 'not-a-date'
params.delete(:created_after)
end
include_examples 'example for invalid parameter'
it 'succeeds' do
subject
expect(response).to be_successful
end
end
context 'when `created_before` is missing' do
context 'when `created_after` is invalid' do
before do
params.delete(:created_before)
params[:created_after] = 'not-a-date'
end
include_examples 'example for invalid parameter'
end
context 'when `created_after` is invalid' do
context 'when `created_before` is invalid' do
before do
params[:created_before] = 'not-a-date'
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