Commit dffb7edd authored by Tiger Watson's avatar Tiger Watson Committed by Heinrich Lee Yu

Expose :certificate_based_clusters feature flag to frontend

Hide Kubernetes sidebar entry for admin and groups
Show 404  on cluster pages
Add feature flag to data helper
Load feature flag status from attributes
Split constants for future re-use
Only show agent tab if cluster are disabled
Hide dropdown and show button if cluster disabled
Co-authored-by: default avatarTiger Watson <twatson@gitlab.com>
parent 69710c13
<script>
import {
GlButton,
GlDropdown,
GlDropdownItem,
GlModalDirective,
......@@ -14,6 +15,7 @@ export default {
i18n: CLUSTERS_ACTIONS,
INSTALL_AGENT_MODAL_ID,
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
......@@ -23,7 +25,13 @@ export default {
GlModalDirective,
GlTooltip: GlTooltipDirective,
},
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster', 'displayClusterAgents'],
inject: [
'newClusterPath',
'addClusterPath',
'canAddCluster',
'displayClusterAgents',
'certificateBasedClustersEnabled',
],
computed: {
tooltip() {
const { connectWithAgent, connectExistingCluster, dropdownDisabledHint } = this.$options.i18n;
......@@ -46,6 +54,7 @@ export default {
<template>
<div class="nav-controls gl-ml-auto">
<gl-dropdown
v-if="certificateBasedClustersEnabled"
ref="dropdown"
v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip"
......@@ -75,5 +84,15 @@ export default {
{{ $options.i18n.connectExistingCluster }}
</gl-dropdown-item>
</gl-dropdown>
<gl-button
v-else
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip"
:disabled="!canAddCluster"
category="primary"
variant="confirm"
>
{{ $options.i18n.connectWithAgent }}
</gl-button>
</div>
</template>
......@@ -9,6 +9,7 @@ import {
AGENT,
EVENT_LABEL_TABS,
EVENT_ACTIONS_CHANGE,
AGENT_TAB,
} from '../constants';
import Agents from './agents.vue';
import InstallAgentModal from './install_agent_modal.vue';
......@@ -28,9 +29,8 @@ export default {
Agents,
InstallAgentModal,
},
CLUSTERS_TABS,
mixins: [trackingMixin],
inject: ['displayClusterAgents'],
inject: ['displayClusterAgents', 'certificateBasedClustersEnabled'],
props: {
defaultBranchName: {
default: '.noBranch',
......@@ -45,21 +45,27 @@ export default {
};
},
computed: {
clusterTabs() {
return this.displayClusterAgents ? CLUSTERS_TABS : [CERTIFICATE_TAB];
availableTabs() {
const clusterTabs = this.displayClusterAgents ? CLUSTERS_TABS : [CERTIFICATE_TAB];
return this.certificateBasedClustersEnabled ? clusterTabs : [AGENT_TAB];
},
},
watch: {
selectedTabIndex(val) {
this.onTabChange(val);
selectedTabIndex: {
handler(val) {
this.onTabChange(val);
},
immediate: true,
},
},
methods: {
setSelectedTab(tabName) {
this.selectedTabIndex = this.clusterTabs.findIndex((tab) => tab.queryParamValue === tabName);
this.selectedTabIndex = this.availableTabs.findIndex(
(tab) => tab.queryParamValue === tabName,
);
},
onTabChange(tab) {
const tabName = this.clusterTabs[tab].queryParamValue;
const tabName = this.availableTabs[tab].queryParamValue;
this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
this.track(EVENT_ACTIONS_CHANGE, { property: tabName });
......@@ -76,7 +82,7 @@ export default {
lazy
>
<gl-tab
v-for="(tab, idx) in clusterTabs"
v-for="(tab, idx) in availableTabs"
:key="idx"
:title="tab.title"
:query-param-value="tab.queryParamValue"
......
......@@ -232,25 +232,24 @@ export const CERTIFICATE_BASED_CARD_INFO = {
export const MAX_CLUSTERS_LIST = 6;
export const ALL_TAB = {
title: s__('ClusterAgents|All'),
component: 'ClustersViewAll',
queryParamValue: 'all',
};
export const AGENT_TAB = {
title: s__('ClusterAgents|Agent'),
component: 'agents',
queryParamValue: 'agent',
};
export const CERTIFICATE_TAB = {
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
};
export const CLUSTERS_TABS = [
{
title: s__('ClusterAgents|All'),
component: 'ClustersViewAll',
queryParamValue: 'all',
},
{
title: s__('ClusterAgents|Agent'),
component: 'agents',
queryParamValue: 'agent',
},
CERTIFICATE_TAB,
];
export const CLUSTERS_TABS = [ALL_TAB, AGENT_TAB, CERTIFICATE_TAB];
export const CLUSTERS_ACTIONS = {
actionsButton: s__('ClusterAgents|Actions'),
......
......@@ -31,6 +31,7 @@ export default () => {
canAdminCluster,
gitlabVersion,
displayClusterAgents,
certificateBasedClustersEnabled,
} = el.dataset;
return new Vue({
......@@ -48,6 +49,7 @@ export default () => {
canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion,
displayClusterAgents: parseBoolean(displayClusterAgents),
certificateBasedClustersEnabled: parseBoolean(certificateBasedClustersEnabled),
},
store: createStore(el.dataset),
render(createElement) {
......
......@@ -2,6 +2,7 @@
class Admin::ClustersController < Clusters::ClustersController
include EnforcesAdminAuthentication
before_action :ensure_feature_enabled!
layout 'admin'
......
......@@ -3,6 +3,7 @@
class Groups::ClustersController < Clusters::ClustersController
include ControllerWithCrossProjectAccessCheck
before_action :ensure_feature_enabled!
prepend_before_action :group
requires_cross_project_access
......
......@@ -464,7 +464,10 @@ module ApplicationSettingsHelper
end
def instance_clusters_enabled?
can?(current_user, :read_cluster, Clusters::Instance.new)
clusterable = Clusters::Instance.new
Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops) &&
can?(current_user, :read_cluster, clusterable)
end
def omnibus_protected_paths_throttle?
......
......@@ -31,7 +31,8 @@ module ClustersHelper
add_cluster_path: clusterable.new_path(tab: 'add'),
can_add_cluster: clusterable.can_add_cluster?.to_s,
can_admin_cluster: clusterable.can_admin_cluster?.to_s,
display_cluster_agents: display_cluster_agents?(clusterable).to_s
display_cluster_agents: display_cluster_agents?(clusterable).to_s,
certificate_based_clusters_enabled: Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops).to_s
}
end
......
......@@ -21,7 +21,10 @@ module Sidebars
override :render?
def render?
can?(context.current_user, :read_cluster, context.group)
clusterable = context.group
Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops) &&
can?(context.current_user, :read_cluster, clusterable)
end
override :extra_container_html_options
......
......@@ -27,7 +27,7 @@ RSpec.describe Admin::ClustersController do
create(:cluster, :disabled, :provided_by_gcp, :production_environment, :instance)
end
include_examples ':certificate_based_clusters feature flag index responses' do
include_examples ':certificate_based_clusters feature flag controller responses' do
let(:subject) { get_index }
end
......
......@@ -32,7 +32,7 @@ RSpec.describe Groups::ClustersController do
create(:cluster, :disabled, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
end
include_examples ':certificate_based_clusters feature flag index responses' do
include_examples ':certificate_based_clusters feature flag controller responses' do
let(:subject) { go }
end
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersActions from '~/clusters_list/components/clusters_actions.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
......@@ -15,6 +15,7 @@ describe('ClustersActionsComponent', () => {
addClusterPath,
canAddCluster: true,
displayClusterAgents: true,
certificateBasedClustersEnabled: true,
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
......@@ -24,6 +25,7 @@ describe('ClustersActionsComponent', () => {
const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
const findConnectWithAgentButton = () => wrapper.findComponent(GlButton);
const createWrapper = (provideData = {}) => {
wrapper = shallowMountExtended(ClustersActions, {
......@@ -45,90 +47,110 @@ describe('ClustersActionsComponent', () => {
afterEach(() => {
wrapper.destroy();
});
describe('when the certificate based clusters are enabled', () => {
it('renders actions menu', () => {
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
});
it('renders actions menu', () => {
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
});
it('renders correct href attributes for the links', () => {
expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
});
it('renders correct href attributes for the links', () => {
expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
});
describe('when user cannot add clusters', () => {
beforeEach(() => {
createWrapper({ canAddCluster: false });
});
describe('when user cannot add clusters', () => {
beforeEach(() => {
createWrapper({ canAddCluster: false });
});
it('disables dropdown', () => {
expect(findDropdown().props('disabled')).toBe(true);
});
it('disables dropdown', () => {
expect(findDropdown().props('disabled')).toBe(true);
});
it('shows tooltip explaining why dropdown is disabled', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
});
it('does not bind split dropdown button', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
it('shows tooltip explaining why dropdown is disabled', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint);
expect(binding.value).toBe(false);
});
});
it('does not bind split dropdown button', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
describe('when on project level', () => {
it('renders a dropdown with 3 actions items', () => {
expect(findDropdownItemIds()).toEqual([
'connect-new-agent-link',
'new-cluster-link',
'connect-cluster-link',
]);
});
expect(binding.value).toBe(false);
});
});
it('renders correct modal id for the agent link', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
describe('when on project level', () => {
it('renders a dropdown with 3 actions items', () => {
expect(findDropdownItemIds()).toEqual([
'connect-new-agent-link',
'new-cluster-link',
'connect-cluster-link',
]);
});
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('renders correct modal id for the agent link', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('shows split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(true);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
it('binds split button with modal id', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
it('shows split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(true);
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
});
it('binds split button with modal id', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
describe('when on group or admin level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: false });
});
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
});
it('renders a dropdown with 2 actions items', () => {
expect(findDropdownItemIds()).toEqual(['new-cluster-link', 'connect-cluster-link']);
});
describe('when on group or admin level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: false });
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectExistingCluster);
});
it('renders a dropdown with 2 actions items', () => {
expect(findDropdownItemIds()).toEqual(['new-cluster-link', 'connect-cluster-link']);
});
it('does not show split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(false);
});
it('does not bind dropdown button to modal', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectExistingCluster);
expect(binding.value).toBe(false);
});
});
});
it('does not show split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(false);
describe('when the certificate based clusters not enabled', () => {
beforeEach(() => {
createWrapper({ certificateBasedClustersEnabled: false });
});
it('does not bind dropdown button to modal', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
it('it does not show the the dropdown', () => {
expect(findDropdown().exists()).toBe(false);
});
expect(binding.value).toBe(false);
it('shows the connect with agent button', () => {
expect(findConnectWithAgentButton().props()).toMatchObject({
disabled: !defaultProvide.canAddCluster,
category: 'primary',
variant: 'confirm',
});
expect(findConnectWithAgentButton().text()).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
});
});
......@@ -6,6 +6,7 @@ import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vu
import {
AGENT,
CERTIFICATE_BASED,
AGENT_TAB,
CLUSTERS_TABS,
CERTIFICATE_TAB,
MAX_CLUSTERS_LIST,
......@@ -24,10 +25,18 @@ describe('ClustersMainViewComponent', () => {
defaultBranchName,
};
const createWrapper = ({ displayClusterAgents }) => {
const defaultProvide = {
certificateBasedClustersEnabled: true,
displayClusterAgents: true,
};
const createWrapper = (extendedProvide = {}) => {
wrapper = shallowMountExtended(ClustersMainView, {
propsData,
provide: { displayClusterAgents },
provide: {
...defaultProvide,
...extendedProvide,
},
});
};
......@@ -40,91 +49,111 @@ describe('ClustersMainViewComponent', () => {
const findGlTabAtIndex = (index) => findAllTabs().at(index);
const findComponent = () => wrapper.findByTestId('clusters-tab-component');
const findModal = () => wrapper.findComponent(InstallAgentModal);
describe('when the certificate based clusters are enabled', () => {
describe('when on project level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: true });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
describe('when on project level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: true });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
expect(findTabs().exists()).toBe(true);
expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
});
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
expect(findTabs().exists()).toBe(true);
expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
});
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(CLUSTERS_TABS.length);
});
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(CLUSTERS_TABS.length);
});
describe('tabs', () => {
it.each`
tabTitle | queryParamValue | lineNumber
${'All'} | ${'all'} | ${0}
${'Agent'} | ${AGENT} | ${1}
${'Certificate'} | ${CERTIFICATE_BASED} | ${2}
`(
'renders correct tab title and query param value',
({ tabTitle, queryParamValue, lineNumber }) => {
expect(findGlTabAtIndex(lineNumber).attributes('title')).toBe(tabTitle);
expect(findGlTabAtIndex(lineNumber).props('queryParamValue')).toBe(queryParamValue);
},
);
});
describe('tabs', () => {
it.each`
tabTitle | queryParamValue | lineNumber
${'All'} | ${'all'} | ${0}
${'Agent'} | ${AGENT} | ${1}
${'Certificate'} | ${CERTIFICATE_BASED} | ${2}
describe.each`
tab | tabName
${'1'} | ${AGENT}
${'2'} | ${CERTIFICATE_BASED}
`(
'renders correct tab title and query param value',
({ tabTitle, queryParamValue, lineNumber }) => {
expect(findGlTabAtIndex(lineNumber).attributes('title')).toBe(tabTitle);
expect(findGlTabAtIndex(lineNumber).props('queryParamValue')).toBe(queryParamValue);
'when the child component emits the tab change event for $tabName tab',
({ tab, tabName }) => {
beforeEach(() => {
findComponent().vm.$emit('changeTab', tabName);
});
it(`changes the tab value to ${tab}`, () => {
expect(findTabs().attributes('value')).toBe(tab);
});
},
);
});
describe.each`
tab | tabName
${'1'} | ${AGENT}
${'2'} | ${CERTIFICATE_BASED}
`(
'when the child component emits the tab change event for $tabName tab',
({ tab, tabName }) => {
describe.each`
tab | tabName | maxAgents
${1} | ${AGENT} | ${MAX_LIST_COUNT}
${2} | ${CERTIFICATE_BASED} | ${MAX_CLUSTERS_LIST}
`('when the active tab is $tabName', ({ tab, tabName, maxAgents }) => {
beforeEach(() => {
findComponent().vm.$emit('changeTab', tabName);
findTabs().vm.$emit('input', tab);
});
it(`changes the tab value to ${tab}`, () => {
expect(findTabs().attributes('value')).toBe(tab);
it('passes child-component param to the component', () => {
expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
});
},
);
describe.each`
tab | tabName | maxAgents
${1} | ${AGENT} | ${MAX_LIST_COUNT}
${2} | ${CERTIFICATE_BASED} | ${MAX_CLUSTERS_LIST}
`('when the active tab is $tabName', ({ tab, tabName, maxAgents }) => {
beforeEach(() => {
findTabs().vm.$emit('input', tab);
it(`sets max-agents param to ${maxAgents} and passes it to the modal`, () => {
expect(findModal().props('maxAgents')).toBe(maxAgents);
});
it(`sends the correct tracking event with the property '${tabName}'`, () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_CHANGE, {
label: EVENT_LABEL_TABS,
property: tabName,
});
});
});
});
it('passes child-component param to the component', () => {
expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
describe('when on group or admin level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: false });
});
it(`sets max-agents param to ${maxAgents} and passes it to the modal`, () => {
expect(findModal().props('maxAgents')).toBe(maxAgents);
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(1);
});
it(`sends the correct tracking event with the property '${tabName}'`, () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_CHANGE, {
label: EVENT_LABEL_TABS,
property: tabName,
});
it('renders correct tab title', () => {
expect(findGlTabAtIndex(0).attributes('title')).toBe(CERTIFICATE_TAB.title);
});
});
});
describe('when on group or admin level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: false });
});
describe('when the certificate based clusters not enabled', () => {
beforeEach(() => {
createWrapper({ certificateBasedClustersEnabled: false });
});
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(1);
});
it('it displays only the Agent tab', () => {
expect(findAllTabs()).toHaveLength(1);
const agentTab = findGlTabAtIndex(0);
it('renders correct tab title', () => {
expect(findGlTabAtIndex(0).attributes('title')).toBe(CERTIFICATE_TAB.title);
expect(agentTab.props()).toMatchObject({
queryParamValue: AGENT_TAB.queryParamValue,
titleLinkClass: '',
});
expect(agentTab.attributes()).toMatchObject({
title: AGENT_TAB.title,
});
});
});
});
});
......@@ -293,4 +293,25 @@ RSpec.describe ApplicationSettingsHelper do
it { is_expected.to eq([%w(Track track), %w(Compress compress)]) }
end
describe '#instance_clusters_enabled?' do
let_it_be(:user) { create(:user) }
subject { helper.instance_clusters_enabled? }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :read_cluster, instance_of(Clusters::Instance)).and_return(true)
end
it { is_expected.to be_truthy}
context ':certificate_based_clusters feature flag is disabled' do
before do
stub_feature_flags(certificate_based_clusters: false)
end
it { is_expected.to be_falsey }
end
end
end
......@@ -136,6 +136,28 @@ RSpec.describe ClustersHelper do
expect(subject[:display_cluster_agents]).to eq("false")
end
end
describe 'certificate based clusters enabled' do
before do
stub_feature_flags(certificate_based_clusters: flag_enabled)
end
context 'feature flag is enabled' do
let(:flag_enabled) { true }
it do
expect(subject[:certificate_based_clusters_enabled]).to eq('true')
end
end
context 'feature flag is disabled' do
let(:flag_enabled) { false }
it do
expect(subject[:certificate_based_clusters_enabled]).to eq('false')
end
end
end
end
describe '#js_clusters_data' do
......
......@@ -28,5 +28,15 @@ RSpec.describe Sidebars::Groups::Menus::KubernetesMenu do
expect(menu.render?).to eq false
end
end
context ':certificate_based_clusters feature flag is disabled' do
before do
stub_feature_flags(certificate_based_clusters: false)
end
it 'returns false' do
expect(menu.render?).to eq false
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