Commit ae2f030d authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '352720-split-cluster-creation-page-into-two-pages-2' into 'master'

Add clusters Actions menu to group and admin views

See merge request gitlab-org/gitlab!81846
parents bb0e042d cfe32c2f
...@@ -23,11 +23,21 @@ export default { ...@@ -23,11 +23,21 @@ export default {
GlModalDirective, GlModalDirective,
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster'], inject: ['newClusterPath', 'addClusterPath', 'canAddCluster', 'displayClusterAgents'],
computed: { computed: {
tooltip() { tooltip() {
const { connectWithAgent, dropdownDisabledHint } = this.$options.i18n; const { connectWithAgent, connectExistingCluster, dropdownDisabledHint } = this.$options.i18n;
return this.canAddCluster ? connectWithAgent : dropdownDisabledHint;
if (!this.canAddCluster) {
return dropdownDisabledHint;
} else if (this.displayClusterAgents) {
return connectWithAgent;
}
return connectExistingCluster;
},
shouldTriggerModal() {
return this.canAddCluster && this.displayClusterAgents;
}, },
}, },
}; };
...@@ -37,15 +47,16 @@ export default { ...@@ -37,15 +47,16 @@ export default {
<div class="nav-controls gl-ml-auto"> <div class="nav-controls gl-ml-auto">
<gl-dropdown <gl-dropdown
ref="dropdown" ref="dropdown"
v-gl-modal-directive="canAddCluster && $options.INSTALL_AGENT_MODAL_ID" v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
v-gl-tooltip="tooltip" v-gl-tooltip="tooltip"
category="primary" category="primary"
variant="confirm" variant="confirm"
:text="$options.i18n.actionsButton" :text="$options.i18n.actionsButton"
:disabled="!canAddCluster" :disabled="!canAddCluster"
split :split="displayClusterAgents"
right right
> >
<template v-if="displayClusterAgents">
<gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header> <gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header>
<gl-dropdown-item <gl-dropdown-item
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID" v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
...@@ -55,6 +66,8 @@ export default { ...@@ -55,6 +66,8 @@ export default {
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-divider /> <gl-dropdown-divider />
<gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header> <gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header>
</template>
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop> <gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
{{ $options.i18n.createNewCluster }} {{ $options.i18n.createNewCluster }}
</gl-dropdown-item> </gl-dropdown-item>
......
...@@ -3,6 +3,7 @@ import { GlTabs, GlTab } from '@gitlab/ui'; ...@@ -3,6 +3,7 @@ import { GlTabs, GlTab } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { import {
CLUSTERS_TABS, CLUSTERS_TABS,
CERTIFICATE_TAB,
MAX_CLUSTERS_LIST, MAX_CLUSTERS_LIST,
MAX_LIST_COUNT, MAX_LIST_COUNT,
AGENT, AGENT,
...@@ -29,6 +30,7 @@ export default { ...@@ -29,6 +30,7 @@ export default {
}, },
CLUSTERS_TABS, CLUSTERS_TABS,
mixins: [trackingMixin], mixins: [trackingMixin],
inject: ['displayClusterAgents'],
props: { props: {
defaultBranchName: { defaultBranchName: {
default: '.noBranch', default: '.noBranch',
...@@ -42,6 +44,11 @@ export default { ...@@ -42,6 +44,11 @@ export default {
maxAgents: MAX_CLUSTERS_LIST, maxAgents: MAX_CLUSTERS_LIST,
}; };
}, },
computed: {
clusterTabs() {
return this.displayClusterAgents ? CLUSTERS_TABS : [CERTIFICATE_TAB];
},
},
watch: { watch: {
selectedTabIndex(val) { selectedTabIndex(val) {
this.onTabChange(val); this.onTabChange(val);
...@@ -49,10 +56,10 @@ export default { ...@@ -49,10 +56,10 @@ export default {
}, },
methods: { methods: {
setSelectedTab(tabName) { setSelectedTab(tabName) {
this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName); this.selectedTabIndex = this.clusterTabs.findIndex((tab) => tab.queryParamValue === tabName);
}, },
onTabChange(tab) { onTabChange(tab) {
const tabName = CLUSTERS_TABS[tab].queryParamValue; const tabName = this.clusterTabs[tab].queryParamValue;
this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST; this.maxAgents = tabName === AGENT ? MAX_LIST_COUNT : MAX_CLUSTERS_LIST;
this.track(EVENT_ACTIONS_CHANGE, { property: tabName }); this.track(EVENT_ACTIONS_CHANGE, { property: tabName });
...@@ -69,7 +76,7 @@ export default { ...@@ -69,7 +76,7 @@ export default {
lazy lazy
> >
<gl-tab <gl-tab
v-for="(tab, idx) in $options.CLUSTERS_TABS" v-for="(tab, idx) in clusterTabs"
:key="idx" :key="idx"
:title="tab.title" :title="tab.title"
:query-param-value="tab.queryParamValue" :query-param-value="tab.queryParamValue"
......
...@@ -232,6 +232,12 @@ export const CERTIFICATE_BASED_CARD_INFO = { ...@@ -232,6 +232,12 @@ export const CERTIFICATE_BASED_CARD_INFO = {
export const MAX_CLUSTERS_LIST = 6; export const MAX_CLUSTERS_LIST = 6;
export const CERTIFICATE_TAB = {
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
};
export const CLUSTERS_TABS = [ export const CLUSTERS_TABS = [
{ {
title: s__('ClusterAgents|All'), title: s__('ClusterAgents|All'),
...@@ -243,11 +249,7 @@ export const CLUSTERS_TABS = [ ...@@ -243,11 +249,7 @@ export const CLUSTERS_TABS = [
component: 'agents', component: 'agents',
queryParamValue: 'agent', queryParamValue: 'agent',
}, },
{ CERTIFICATE_TAB,
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
},
]; ];
export const CLUSTERS_ACTIONS = { export const CLUSTERS_ACTIONS = {
......
import { GlToast } from '@gitlab/ui'; import { GlToast } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import loadClusters from './load_clusters'; import { parseBoolean } from '~/lib/utils/common_utils';
import loadMainView from './load_main_view'; import createDefaultClient from '~/lib/graphql';
import ClustersMainView from './components/clusters_main_view.vue';
import { createStore } from './store';
Vue.use(GlToast); Vue.use(GlToast);
Vue.use(VueApollo); Vue.use(VueApollo);
export default () => { export default () => {
loadClusters(Vue); const el = document.querySelector('.js-clusters-main-view');
loadMainView(Vue, VueApollo);
if (!el) {
return null;
}
const defaultClient = createDefaultClient();
const {
emptyStateImage,
defaultBranchName,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster,
canAdminCluster,
gitlabVersion,
displayClusterAgents,
} = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
provide: {
emptyStateImage,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster: parseBoolean(canAddCluster),
canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion,
displayClusterAgents: parseBoolean(displayClusterAgents),
},
store: createStore(el.dataset),
render(createElement) {
return createElement(ClustersMainView, {
props: {
defaultBranchName,
},
});
},
});
}; };
import Clusters from './components/clusters.vue';
import { createStore } from './store';
export default (Vue) => {
const el = document.querySelector('#js-clusters-list-app');
if (!el) {
return null;
}
const { emptyStateHelpText, newClusterPath, clustersEmptyStateImage } = el.dataset;
return new Vue({
el,
provide: {
emptyStateHelpText,
newClusterPath,
clustersEmptyStateImage,
},
store: createStore(el.dataset),
render(createElement) {
return createElement(Clusters);
},
});
};
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { parseBoolean } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
import ClustersMainView from './components/clusters_main_view.vue';
import { createStore } from './store';
Vue.use(VueApollo);
export default () => {
const el = document.querySelector('.js-clusters-main-view');
if (!el) {
return null;
}
const defaultClient = createDefaultClient();
const {
emptyStateImage,
defaultBranchName,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster,
canAdminCluster,
gitlabVersion,
} = el.dataset;
return new Vue({
el,
apolloProvider: new VueApollo({ defaultClient }),
provide: {
emptyStateImage,
projectPath,
kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
canAddCluster: parseBoolean(canAddCluster),
canAdminCluster: parseBoolean(canAdminCluster),
gitlabVersion,
},
store: createStore(el.dataset),
render(createElement) {
return createElement(ClustersMainView, {
props: {
defaultBranchName,
},
});
},
});
};
...@@ -28,8 +28,10 @@ module ClustersHelper ...@@ -28,8 +28,10 @@ module ClustersHelper
clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'), clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
empty_state_help_text: clusterable.empty_state_help_text, empty_state_help_text: clusterable.empty_state_help_text,
new_cluster_path: clusterable.new_path(tab: 'create'), new_cluster_path: clusterable.new_path(tab: 'create'),
add_cluster_path: clusterable.new_path(tab: 'add'),
can_add_cluster: clusterable.can_add_cluster?.to_s, can_add_cluster: clusterable.can_add_cluster?.to_s,
can_admin_cluster: clusterable.can_admin_cluster?.to_s can_admin_cluster: clusterable.can_admin_cluster?.to_s,
display_cluster_agents: display_cluster_agents?(clusterable).to_s
} }
end end
...@@ -38,7 +40,6 @@ module ClustersHelper ...@@ -38,7 +40,6 @@ module ClustersHelper
default_branch_name: clusterable.default_branch, default_branch_name: clusterable.default_branch,
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'), empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
project_path: clusterable.full_path, project_path: clusterable.full_path,
add_cluster_path: clusterable.new_path(tab: 'add'),
kas_address: Gitlab::Kas.external_url, kas_address: Gitlab::Kas.external_url,
gitlab_version: Gitlab.version_info gitlab_version: Gitlab.version_info
}.merge(js_clusters_list_data(clusterable)) }.merge(js_clusters_list_data(clusterable))
......
...@@ -7,4 +7,4 @@ ...@@ -7,4 +7,4 @@
%span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2 %span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2
= s_("ClusterIntegration|Connect cluster with certificate") = s_("ClusterIntegration|Connect cluster with certificate")
#js-clusters-list-app{ data: js_clusters_list_data(clusterable) } .js-clusters-main-view{ data: js_clusters_list_data(clusterable) }
...@@ -14,10 +14,13 @@ describe('ClustersActionsComponent', () => { ...@@ -14,10 +14,13 @@ describe('ClustersActionsComponent', () => {
newClusterPath, newClusterPath,
addClusterPath, addClusterPath,
canAddCluster: true, canAddCluster: true,
displayClusterAgents: true,
}; };
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemIds = () =>
findDropdownItems().wrappers.map((x) => x.attributes('data-testid'));
const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link'); const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link'); const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link'); const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
...@@ -47,15 +50,41 @@ describe('ClustersActionsComponent', () => { ...@@ -47,15 +50,41 @@ describe('ClustersActionsComponent', () => {
expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton); expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.actionsButton);
}); });
it('renders a dropdown with 3 actions items', () => {
expect(findDropdownItems()).toHaveLength(3);
});
it('renders correct href attributes for the links', () => { it('renders correct href attributes for the links', () => {
expect(findNewClusterLink().attributes('href')).toBe(newClusterPath); expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath); expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
}); });
describe('when user cannot add clusters', () => {
beforeEach(() => {
createWrapper({ canAddCluster: false });
});
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');
expect(binding.value).toBe(false);
});
});
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',
]);
});
it('renders correct modal id for the agent link', () => { it('renders correct modal id for the agent link', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive'); const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
...@@ -67,18 +96,39 @@ describe('ClustersActionsComponent', () => { ...@@ -67,18 +96,39 @@ describe('ClustersActionsComponent', () => {
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent); expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
}); });
describe('when user cannot add clusters', () => { it('shows split button in dropdown', () => {
expect(findDropdown().props('split')).toBe(true);
});
it('binds split button with modal id', () => {
const binding = getBinding(findDropdown().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
});
describe('when on group or admin level', () => {
beforeEach(() => { beforeEach(() => {
createWrapper({ canAddCluster: false }); createWrapper({ displayClusterAgents: false });
}); });
it('disables dropdown', () => { it('renders a dropdown with 2 actions items', () => {
expect(findDropdown().props('disabled')).toBe(true); expect(findDropdownItemIds()).toEqual(['new-cluster-link', 'connect-cluster-link']);
}); });
it('shows tooltip explaining why dropdown is disabled', () => { it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip'); const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.dropdownDisabledHint); expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectExistingCluster);
});
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');
expect(binding.value).toBe(false);
}); });
}); });
}); });
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
AGENT, AGENT,
CERTIFICATE_BASED, CERTIFICATE_BASED,
CLUSTERS_TABS, CLUSTERS_TABS,
CERTIFICATE_TAB,
MAX_CLUSTERS_LIST, MAX_CLUSTERS_LIST,
MAX_LIST_COUNT, MAX_LIST_COUNT,
EVENT_LABEL_TABS, EVENT_LABEL_TABS,
...@@ -23,12 +24,12 @@ describe('ClustersMainViewComponent', () => { ...@@ -23,12 +24,12 @@ describe('ClustersMainViewComponent', () => {
defaultBranchName, defaultBranchName,
}; };
beforeEach(() => { const createWrapper = ({ displayClusterAgents }) => {
wrapper = shallowMountExtended(ClustersMainView, { wrapper = shallowMountExtended(ClustersMainView, {
propsData, propsData,
provide: { displayClusterAgents },
}); });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); };
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -40,6 +41,12 @@ describe('ClustersMainViewComponent', () => { ...@@ -40,6 +41,12 @@ describe('ClustersMainViewComponent', () => {
const findComponent = () => wrapper.findByTestId('clusters-tab-component'); const findComponent = () => wrapper.findByTestId('clusters-tab-component');
const findModal = () => wrapper.findComponent(InstallAgentModal); const findModal = () => wrapper.findComponent(InstallAgentModal);
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', () => { it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
expect(findTabs().exists()).toBe(true); expect(findTabs().exists()).toBe(true);
expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true); expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
...@@ -68,7 +75,9 @@ describe('ClustersMainViewComponent', () => { ...@@ -68,7 +75,9 @@ describe('ClustersMainViewComponent', () => {
tab | tabName tab | tabName
${'1'} | ${AGENT} ${'1'} | ${AGENT}
${'2'} | ${CERTIFICATE_BASED} ${'2'} | ${CERTIFICATE_BASED}
`('when the child component emits the tab change event for $tabName tab', ({ tab, tabName }) => { `(
'when the child component emits the tab change event for $tabName tab',
({ tab, tabName }) => {
beforeEach(() => { beforeEach(() => {
findComponent().vm.$emit('changeTab', tabName); findComponent().vm.$emit('changeTab', tabName);
}); });
...@@ -76,7 +85,8 @@ describe('ClustersMainViewComponent', () => { ...@@ -76,7 +85,8 @@ describe('ClustersMainViewComponent', () => {
it(`changes the tab value to ${tab}`, () => { it(`changes the tab value to ${tab}`, () => {
expect(findTabs().attributes('value')).toBe(tab); expect(findTabs().attributes('value')).toBe(tab);
}); });
}); },
);
describe.each` describe.each`
tab | tabName | maxAgents tab | tabName | maxAgents
...@@ -102,4 +112,19 @@ describe('ClustersMainViewComponent', () => { ...@@ -102,4 +112,19 @@ describe('ClustersMainViewComponent', () => {
}); });
}); });
}); });
});
describe('when on group or admin level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: false });
});
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(1);
});
it('renders correct tab title', () => {
expect(findGlTabAtIndex(0).attributes('title')).toBe(CERTIFICATE_TAB.title);
});
});
}); });
...@@ -92,6 +92,10 @@ RSpec.describe ClustersHelper do ...@@ -92,6 +92,10 @@ RSpec.describe ClustersHelper do
expect(subject[:new_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=create") expect(subject[:new_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=create")
end end
it 'displays add cluster using certificate path' do
expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=add")
end
context 'user has no permissions to create a cluster' do context 'user has no permissions to create a cluster' do
it 'displays that user can\'t add cluster' do it 'displays that user can\'t add cluster' do
expect(subject[:can_add_cluster]).to eq("false") expect(subject[:can_add_cluster]).to eq("false")
...@@ -114,6 +118,10 @@ RSpec.describe ClustersHelper do ...@@ -114,6 +118,10 @@ RSpec.describe ClustersHelper do
it 'doesn\'t display empty state help text' do it 'doesn\'t display empty state help text' do
expect(subject[:empty_state_help_text]).to be_nil expect(subject[:empty_state_help_text]).to be_nil
end end
it 'displays display_cluster_agents as true' do
expect(subject[:display_cluster_agents]).to eq("true")
end
end end
context 'group cluster' do context 'group cluster' do
...@@ -123,6 +131,10 @@ RSpec.describe ClustersHelper do ...@@ -123,6 +131,10 @@ RSpec.describe ClustersHelper do
it 'displays empty state help text' do it 'displays empty state help text' do
expect(subject[:empty_state_help_text]).to eq(s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.')) expect(subject[:empty_state_help_text]).to eq(s_('ClusterIntegration|Adding an integration to your group will share the cluster across all your projects.'))
end end
it 'displays display_cluster_agents as false' do
expect(subject[:display_cluster_agents]).to eq("false")
end
end end
end end
...@@ -145,10 +157,6 @@ RSpec.describe ClustersHelper do ...@@ -145,10 +157,6 @@ RSpec.describe ClustersHelper do
expect(subject[:project_path]).to eq(project.full_path) expect(subject[:project_path]).to eq(project.full_path)
end end
it 'displays add cluster using certificate path' do
expect(subject[:add_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=add")
end
it 'displays kas address' do it 'displays kas address' do
expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url) expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
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