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 {
GlModalDirective,
GlTooltip: GlTooltipDirective,
},
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster'],
inject: ['newClusterPath', 'addClusterPath', 'canAddCluster', 'displayClusterAgents'],
computed: {
tooltip() {
const { connectWithAgent, dropdownDisabledHint } = this.$options.i18n;
return this.canAddCluster ? connectWithAgent : dropdownDisabledHint;
const { connectWithAgent, connectExistingCluster, dropdownDisabledHint } = this.$options.i18n;
if (!this.canAddCluster) {
return dropdownDisabledHint;
} else if (this.displayClusterAgents) {
return connectWithAgent;
}
return connectExistingCluster;
},
shouldTriggerModal() {
return this.canAddCluster && this.displayClusterAgents;
},
},
};
......@@ -37,24 +47,27 @@ export default {
<div class="nav-controls gl-ml-auto">
<gl-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"
category="primary"
variant="confirm"
:text="$options.i18n.actionsButton"
:disabled="!canAddCluster"
split
:split="displayClusterAgents"
right
>
<gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header>
<gl-dropdown-item
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
data-testid="connect-new-agent-link"
>
{{ $options.i18n.connectWithAgent }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header>
<template v-if="displayClusterAgents">
<gl-dropdown-section-header>{{ $options.i18n.agent }}</gl-dropdown-section-header>
<gl-dropdown-item
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
data-testid="connect-new-agent-link"
>
{{ $options.i18n.connectWithAgent }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-section-header>{{ $options.i18n.certificate }}</gl-dropdown-section-header>
</template>
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
{{ $options.i18n.createNewCluster }}
</gl-dropdown-item>
......
......@@ -3,6 +3,7 @@ import { GlTabs, GlTab } from '@gitlab/ui';
import Tracking from '~/tracking';
import {
CLUSTERS_TABS,
CERTIFICATE_TAB,
MAX_CLUSTERS_LIST,
MAX_LIST_COUNT,
AGENT,
......@@ -29,6 +30,7 @@ export default {
},
CLUSTERS_TABS,
mixins: [trackingMixin],
inject: ['displayClusterAgents'],
props: {
defaultBranchName: {
default: '.noBranch',
......@@ -42,6 +44,11 @@ export default {
maxAgents: MAX_CLUSTERS_LIST,
};
},
computed: {
clusterTabs() {
return this.displayClusterAgents ? CLUSTERS_TABS : [CERTIFICATE_TAB];
},
},
watch: {
selectedTabIndex(val) {
this.onTabChange(val);
......@@ -49,10 +56,10 @@ export default {
},
methods: {
setSelectedTab(tabName) {
this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName);
this.selectedTabIndex = this.clusterTabs.findIndex((tab) => tab.queryParamValue === tabName);
},
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.track(EVENT_ACTIONS_CHANGE, { property: tabName });
......@@ -69,7 +76,7 @@ export default {
lazy
>
<gl-tab
v-for="(tab, idx) in $options.CLUSTERS_TABS"
v-for="(tab, idx) in clusterTabs"
:key="idx"
:title="tab.title"
:query-param-value="tab.queryParamValue"
......
......@@ -232,6 +232,12 @@ export const CERTIFICATE_BASED_CARD_INFO = {
export const MAX_CLUSTERS_LIST = 6;
export const CERTIFICATE_TAB = {
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
};
export const CLUSTERS_TABS = [
{
title: s__('ClusterAgents|All'),
......@@ -243,11 +249,7 @@ export const CLUSTERS_TABS = [
component: 'agents',
queryParamValue: 'agent',
},
{
title: s__('ClusterAgents|Certificate'),
component: 'clusters',
queryParamValue: 'certificate_based',
},
CERTIFICATE_TAB,
];
export const CLUSTERS_ACTIONS = {
......
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import loadClusters from './load_clusters';
import loadMainView from './load_main_view';
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(GlToast);
Vue.use(VueApollo);
export default () => {
loadClusters(Vue);
loadMainView(Vue, VueApollo);
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,
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
clusters_empty_state_image: image_path('illustrations/empty-state/empty-state-clusters.svg'),
empty_state_help_text: clusterable.empty_state_help_text,
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_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
......@@ -38,7 +40,6 @@ module ClustersHelper
default_branch_name: clusterable.default_branch,
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
project_path: clusterable.full_path,
add_cluster_path: clusterable.new_path(tab: 'add'),
kas_address: Gitlab::Kas.external_url,
gitlab_version: Gitlab.version_info
}.merge(js_clusters_list_data(clusterable))
......
......@@ -7,4 +7,4 @@
%span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2
= 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', () => {
newClusterPath,
addClusterPath,
canAddCluster: true,
displayClusterAgents: true,
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownItemIds = () =>
findDropdownItems().wrappers.map((x) => x.attributes('data-testid'));
const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
......@@ -47,26 +50,11 @@ describe('ClustersActionsComponent', () => {
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', () => {
expect(findNewClusterLink().attributes('href')).toBe(newClusterPath);
expect(findConnectClusterLink().attributes('href')).toBe(addClusterPath);
});
it('renders correct modal id for the agent link', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
describe('when user cannot add clusters', () => {
beforeEach(() => {
createWrapper({ canAddCluster: false });
......@@ -80,5 +68,67 @@ describe('ClustersActionsComponent', () => {
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', () => {
const binding = getBinding(findConnectNewAgentLink().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
expect(tooltip.value).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
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(() => {
createWrapper({ displayClusterAgents: false });
});
it('renders a dropdown with 2 actions items', () => {
expect(findDropdownItemIds()).toEqual(['new-cluster-link', 'connect-cluster-link']);
});
it('shows tooltip', () => {
const tooltip = getBinding(findDropdown().element, 'gl-tooltip');
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 {
AGENT,
CERTIFICATE_BASED,
CLUSTERS_TABS,
CERTIFICATE_TAB,
MAX_CLUSTERS_LIST,
MAX_LIST_COUNT,
EVENT_LABEL_TABS,
......@@ -23,12 +24,12 @@ describe('ClustersMainViewComponent', () => {
defaultBranchName,
};
beforeEach(() => {
const createWrapper = ({ displayClusterAgents }) => {
wrapper = shallowMountExtended(ClustersMainView, {
propsData,
provide: { displayClusterAgents },
});
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
};
afterEach(() => {
wrapper.destroy();
......@@ -40,66 +41,90 @@ describe('ClustersMainViewComponent', () => {
const findComponent = () => wrapper.findByTestId('clusters-tab-component');
const findModal = () => wrapper.findComponent(InstallAgentModal);
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', () => {
expect(findTabs().exists()).toBe(true);
expect(findTabs().props('syncActiveTabWithQueryParams')).toBe(true);
});
describe('when on project level', () => {
beforeEach(() => {
createWrapper({ displayClusterAgents: true });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(CLUSTERS_TABS.length);
});
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);
});
describe('tabs', () => {
it.each`
tabTitle | queryParamValue | lineNumber
${'All'} | ${'all'} | ${0}
${'Agent'} | ${AGENT} | ${1}
${'Certificate'} | ${CERTIFICATE_BASED} | ${2}
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.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 }) => {
beforeEach(() => {
findComponent().vm.$emit('changeTab', 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(() => {
findTabs().vm.$emit('input', tab);
});
it('passes child-component param to the component', () => {
expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
});
it(`changes the tab value to ${tab}`, () => {
expect(findTabs().attributes('value')).toBe(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,
});
});
});
});
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 }) => {
describe('when on group or admin level', () => {
beforeEach(() => {
findTabs().vm.$emit('input', tab);
createWrapper({ displayClusterAgents: false });
});
it('passes child-component param to the component', () => {
expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
it('renders correct number of tabs', () => {
expect(findAllTabs()).toHaveLength(1);
});
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('renders correct tab title', () => {
expect(findGlTabAtIndex(0).attributes('title')).toBe(CERTIFICATE_TAB.title);
});
});
});
......@@ -92,6 +92,10 @@ RSpec.describe ClustersHelper do
expect(subject[:new_cluster_path]).to eq("#{project_path(project)}/-/clusters/new?tab=create")
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
it 'displays that user can\'t add cluster' do
expect(subject[:can_add_cluster]).to eq("false")
......@@ -114,6 +118,10 @@ RSpec.describe ClustersHelper do
it 'doesn\'t display empty state help text' do
expect(subject[:empty_state_help_text]).to be_nil
end
it 'displays display_cluster_agents as true' do
expect(subject[:display_cluster_agents]).to eq("true")
end
end
context 'group cluster' do
......@@ -123,6 +131,10 @@ RSpec.describe ClustersHelper 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.'))
end
it 'displays display_cluster_agents as false' do
expect(subject[:display_cluster_agents]).to eq("false")
end
end
end
......@@ -145,10 +157,6 @@ RSpec.describe ClustersHelper do
expect(subject[:project_path]).to eq(project.full_path)
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
expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
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