Commit 2c2acc53 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch 'nfriend-hide-group-dora-charts-if-not' into 'master'

Restrict visibility of group-level DORA charts to Ultimate users

See merge request gitlab-org/gitlab!65478
parents 83a34286 67274738
......@@ -3,7 +3,6 @@ import { GlTabs, GlTab } from '@gitlab/ui';
import DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_charts.vue';
import LeadTimeCharts from 'ee/dora/components/lead_time_charts.vue';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import { TABS } from '../constants';
import ReleaseStatsCard from './release_stats_card.vue';
export default {
......@@ -15,11 +14,31 @@ export default {
DeploymentFrequencyCharts,
LeadTimeCharts,
},
inject: {
shouldRenderDoraCharts: {
type: Boolean,
default: false,
},
},
data() {
return {
selectedTabIndex: 0,
};
},
computed: {
tabs() {
const tabsToShow = ['release-statistics'];
if (this.shouldRenderDoraCharts) {
tabsToShow.push('deployment-frequency', 'lead-time');
}
return tabsToShow;
},
releaseStatsCardClasses() {
return ['gl-mt-5'];
},
},
created() {
this.selectTab();
window.addEventListener('popstate', this.selectTab);
......@@ -27,13 +46,13 @@ export default {
methods: {
selectTab() {
const [tabQueryParam] = getParameterValues('tab');
const tabIndex = TABS.indexOf(tabQueryParam);
const tabIndex = this.tabs.indexOf(tabQueryParam);
this.selectedTabIndex = tabIndex >= 0 ? tabIndex : 0;
},
onTabChange(newIndex) {
if (newIndex !== this.selectedTabIndex) {
this.selectedTabIndex = newIndex;
const path = mergeUrlParams({ tab: TABS[newIndex] }, window.location.pathname);
const path = mergeUrlParams({ tab: this.tabs[newIndex] }, window.location.pathname);
updateHistory({ url: path, title: window.title });
}
},
......@@ -42,16 +61,19 @@ export default {
</script>
<template>
<div>
<gl-tabs :value="selectedTabIndex" @input="onTabChange">
<gl-tabs v-if="tabs.length > 1" :value="selectedTabIndex" @input="onTabChange">
<gl-tab :title="s__('CICDAnalytics|Release statistics')">
<release-stats-card class="gl-mt-5" />
<release-stats-card :class="releaseStatsCardClasses" />
</gl-tab>
<template v-if="shouldRenderDoraCharts">
<gl-tab :title="s__('CICDAnalytics|Deployment frequency')">
<deployment-frequency-charts />
</gl-tab>
<gl-tab :title="s__('CICDAnalytics|Lead time')">
<lead-time-charts />
</gl-tab>
</template>
</gl-tabs>
<release-stats-card v-else :class="releaseStatsCardClasses" />
</div>
</template>
export const STAT_ERROR_PLACEHOLDER = '-';
export const TABS = ['release-statistics', 'deployment-frequency', 'lead-time'];
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CiCdAnalyticsApp from './components/app.vue';
Vue.use(VueApollo);
......@@ -16,11 +17,14 @@ export default () => {
const { fullPath } = el.dataset;
const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts);
return new Vue({
el,
apolloProvider,
provide: {
groupPath: fullPath,
shouldRenderDoraCharts,
},
render: (createElement) => createElement(CiCdAnalyticsApp),
});
......
......@@ -6,9 +6,11 @@ module EE
override :should_render_dora_charts
def should_render_dora_charts
return false unless @project.feature_available?(:dora4_analytics)
container = @project || @group
can?(current_user, :read_dora4_analytics, @project)
return false unless container.feature_available?(:dora4_analytics)
can?(current_user, :read_dora4_analytics, container)
end
end
end
- page_title _("CI/CD Analytics")
#js-group-ci-cd-analytics-app{ data: { full_path: @group.full_path } }
#js-group-ci-cd-analytics-app{ data: { full_path: @group.full_path,
should_render_dora_charts: should_render_dora_charts.to_s } }
......@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Group CI/CD Analytics', :js do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be_with_refind(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group ) }
let_it_be(:project_1) { create(:project, group: group) }
let_it_be(:project_2) { create(:project, group: group) }
......@@ -15,7 +15,7 @@ RSpec.describe 'Group CI/CD Analytics', :js do
let_it_be(:unrelated_release) { create(:release, project: unrelated_project) }
before do
stub_licensed_features(group_ci_cd_analytics: true)
stub_licensed_features(group_ci_cd_analytics: true, dora4_analytics: true)
group.add_reporter(user)
sign_in(user)
visit group_analytics_ci_cd_analytics_path(group)
......
import { GlTabs, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import CiCdAnalyticsApp from 'ee/analytics/group_ci_cd_analytics/components/app.vue';
import ReleaseStatsCard from 'ee/analytics/group_ci_cd_analytics/components/release_stats_card.vue';
import DeploymentFrequencyCharts from 'ee/dora/components/deployment_frequency_charts.vue';
......@@ -17,8 +18,18 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => {
getParameterValues.mockReturnValue([]);
});
const createComponent = () => {
wrapper = shallowMount(CiCdAnalyticsApp);
const createComponent = (mountOptions = {}) => {
wrapper = shallowMount(
CiCdAnalyticsApp,
merge(
{
provide: {
shouldRenderDoraCharts: true,
},
},
mountOptions,
),
);
};
const findGlTabs = () => wrapper.findComponent(GlTabs);
......@@ -26,15 +37,32 @@ describe('ee/analytics/group_ci_cd_analytics/components/app.vue', () => {
const findGlTabAtIndex = (index) => findAllGlTabs().at(index);
describe('tabs', () => {
describe('when the DORA charts are available', () => {
beforeEach(() => {
createComponent();
});
it('renders tabs in the correct order', () => {
expect(findGlTabs().exists()).toBe(true);
expect(findAllGlTabs().length).toBe(3);
expect(findGlTabAtIndex(0).attributes('title')).toBe('Release statistics');
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency');
expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead time');
});
});
describe('when the DORA charts are not available', () => {
beforeEach(() => {
createComponent({ provide: { shouldRenderDoraCharts: false } });
});
it('does not render any tabs', () => {
expect(findGlTabs().exists()).toBe(false);
});
it('renders the release statistics component', () => {
expect(wrapper.findComponent(ReleaseStatsCard).exists()).toBe(true);
});
});
});
......
......@@ -4,16 +4,15 @@ require 'spec_helper'
RSpec.describe EE::GraphHelper do
let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:group) { create(:group) }
let_it_be(:project) { create(:project, :private) }
let(:project) { create(:project, :private) }
let(:is_feature_licensed) { true }
let(:is_user_authorized) { true }
before do
stub_licensed_features(dora4_analytics: is_feature_licensed)
self.instance_variable_set(:@current_user, current_user)
self.instance_variable_set(:@project, project)
allow(self).to receive(:can?).with(current_user, :read_dora4_analytics, project).and_return(is_user_authorized)
end
describe '#should_render_dora_charts' do
......@@ -25,6 +24,7 @@ RSpec.describe EE::GraphHelper do
it { expect(should_render_dora_charts).to be(false) }
end
shared_examples '#should_render_dora_charts for a specific type of container' do
it_behaves_like 'returns true'
context 'when the feature is not available' do
......@@ -39,4 +39,23 @@ RSpec.describe EE::GraphHelper do
it_behaves_like 'returns false'
end
end
context 'when serving the project-level DORA page' do
before do
self.instance_variable_set(:@project, project)
allow(self).to receive(:can?).with(current_user, :read_dora4_analytics, project).and_return(is_user_authorized)
end
it_behaves_like '#should_render_dora_charts for a specific type of container'
end
context 'when serving the group-level DORA page' do
before do
self.instance_variable_set(:@group, group)
allow(self).to receive(:can?).with(current_user, :read_dora4_analytics, group).and_return(is_user_authorized)
end
it_behaves_like '#should_render_dora_charts for a specific type of container'
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