Commit cfe32c2f authored by Anna Vovchenko's avatar Anna Vovchenko Committed by Paul Slaughter

Added cluster Actions menu to group and admin view

As we want to unify clusters views for all levels,
we are adding tabs and Actions menu to group and admin views.
As we don't support Agent for groups and admin yet,
there will be only certificate related options and tabs.

Changelog: changed
parent 9f23f82b
......@@ -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