Commit f76a42a5 authored by Anna Vovchenko's avatar Anna Vovchenko Committed by Nicolò Maria Mezzopera

Move navigation and actions on clusters page to Vue

parent 0fe05e70
...@@ -6,3 +6,7 @@ export function generateAgentRegistrationCommand(agentToken, kasAddress) { ...@@ -6,3 +6,7 @@ export function generateAgentRegistrationCommand(agentToken, kasAddress) {
--agent-version stable \\ --agent-version stable \\
--namespace gitlab-kubernetes-agent | kubectl apply -f -`; --namespace gitlab-kubernetes-agent | kubectl apply -f -`;
} }
export function getAgentConfigPath(clusterAgentName) {
return `.gitlab/agents/${clusterAgentName}`;
}
<script> <script>
import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants'; import { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants';
export default { export default {
i18n: I18N_AGENTS_EMPTY_STATE, i18n: I18N_AGENTS_EMPTY_STATE,
modalId: INSTALL_AGENT_MODAL_ID, modalId: INSTALL_AGENT_MODAL_ID,
multipleClustersDocsUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
installDocsUrl: helpPagePath('administration/clusters/kas'),
getStartedDocsUrl: helpPagePath('user/clusters/agent/index', {
anchor: 'define-a-configuration-repository',
}),
integrationsDocsUrl: helpPagePath('user/clusters/agent/index', {
anchor: 'get-started-with-gitops-and-the-gitlab-agent',
}),
components: { components: {
GlButton, GlButton,
GlEmptyState, GlEmptyState,
...@@ -15,14 +24,7 @@ export default { ...@@ -15,14 +24,7 @@ export default {
directives: { directives: {
GlModalDirective, GlModalDirective,
}, },
inject: [ inject: ['emptyStateImage', 'projectPath'],
'emptyStateImage',
'projectPath',
'multipleClustersDocsUrl',
'installDocsUrl',
'getStartedDocsUrl',
'integrationDocsUrl',
],
props: { props: {
hasConfigurations: { hasConfigurations: {
type: Boolean, type: Boolean,
...@@ -47,7 +49,7 @@ export default { ...@@ -47,7 +49,7 @@ export default {
<gl-sprintf :message="$options.i18n.multipleClustersText"> <gl-sprintf :message="$options.i18n.multipleClustersText">
<template #link="{ content }"> <template #link="{ content }">
<gl-link <gl-link
:href="multipleClustersDocsUrl" :href="$options.multipleClustersDocsUrl"
target="_blank" target="_blank"
data-testid="multiple-clusters-docs-link" data-testid="multiple-clusters-docs-link"
> >
...@@ -58,7 +60,7 @@ export default { ...@@ -58,7 +60,7 @@ export default {
</p> </p>
<p class="mw-460 gl-mx-auto"> <p class="mw-460 gl-mx-auto">
<gl-link :href="installDocsUrl" target="_blank" data-testid="install-docs-link"> <gl-link :href="$options.installDocsUrl" target="_blank" data-testid="install-docs-link">
{{ $options.i18n.learnMoreText }} {{ $options.i18n.learnMoreText }}
</gl-link> </gl-link>
</p> </p>
...@@ -75,7 +77,7 @@ export default { ...@@ -75,7 +77,7 @@ export default {
<gl-button <gl-button
category="primary" category="primary"
variant="info" variant="info"
:href="getStartedDocsUrl" :href="$options.getStartedDocsUrl"
target="_blank" target="_blank"
class="gl-ml-0!" class="gl-ml-0!"
> >
......
<script> <script>
import { import {
GlButton,
GlLink, GlLink,
GlModalDirective, GlModalDirective,
GlTable, GlTable,
...@@ -12,11 +11,12 @@ import { ...@@ -12,11 +11,12 @@ import {
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { INSTALL_AGENT_MODAL_ID, AGENT_STATUSES, TROUBLESHOOTING_LINK } from '../constants'; import { helpPagePath } from '~/helpers/help_page_helper';
import { INSTALL_AGENT_MODAL_ID, AGENT_STATUSES } from '../constants';
import { getAgentConfigPath } from '../clusters_util';
export default { export default {
components: { components: {
GlButton,
GlLink, GlLink,
GlTable, GlTable,
GlIcon, GlIcon,
...@@ -29,10 +29,12 @@ export default { ...@@ -29,10 +29,12 @@ export default {
GlModalDirective, GlModalDirective,
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
inject: ['integrationDocsUrl'],
INSTALL_AGENT_MODAL_ID, INSTALL_AGENT_MODAL_ID,
AGENT_STATUSES, AGENT_STATUSES,
TROUBLESHOOTING_LINK,
troubleshooting_link: helpPagePath('user/clusters/agent/index', {
anchor: 'troubleshooting',
}),
props: { props: {
agents: { agents: {
required: true, required: true,
...@@ -41,112 +43,102 @@ export default { ...@@ -41,112 +43,102 @@ export default {
}, },
computed: { computed: {
fields() { fields() {
const tdClass = 'gl-py-5!';
return [ return [
{ {
key: 'name', key: 'name',
label: s__('ClusterAgents|Name'), label: s__('ClusterAgents|Name'),
tdClass,
}, },
{ {
key: 'status', key: 'status',
label: s__('ClusterAgents|Connection status'), label: s__('ClusterAgents|Connection status'),
tdClass,
}, },
{ {
key: 'lastContact', key: 'lastContact',
label: s__('ClusterAgents|Last contact'), label: s__('ClusterAgents|Last contact'),
tdClass,
}, },
{ {
key: 'configuration', key: 'configuration',
label: s__('ClusterAgents|Configuration'), label: s__('ClusterAgents|Configuration'),
tdClass,
}, },
]; ];
}, },
}, },
methods: {
getCellId(item) {
return `connection-status-${item.name}`;
},
getAgentConfigPath,
},
}; };
</script> </script>
<template> <template>
<div> <gl-table
<div class="gl-display-block gl-text-right gl-my-3"> :items="agents"
<gl-button :fields="fields"
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID" stacked="md"
variant="confirm" head-variant="white"
category="primary" thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
>{{ s__('ClusterAgents|Install a new GitLab Agent') }} class="gl-mb-4!"
</gl-button> data-testid="cluster-agent-list-table"
</div> >
<template #cell(name)="{ item }">
<gl-link :href="item.webPath" data-testid="cluster-agent-name-link">
{{ item.name }}
</gl-link>
</template>
<gl-table <template #cell(status)="{ item }">
:items="agents" <span :id="getCellId(item)" class="gl-md-pr-5" data-testid="cluster-agent-connection-status">
:fields="fields" <span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3">
stacked="md" <gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="12" /></span
head-variant="white" >{{ $options.AGENT_STATUSES[item.status].name }}
thead-class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100" </span>
data-testid="cluster-agent-list-table" <gl-tooltip v-if="item.status === 'active'" :target="getCellId(item)" placement="right">
> <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title"
<template #cell(name)="{ item }"> ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
<gl-link :href="item.webPath" data-testid="cluster-agent-name-link"> </gl-sprintf>
{{ item.name }} </gl-tooltip>
</gl-link> <gl-popover
</template> v-else
:target="getCellId(item)"
<template #cell(status)="{ item }"> :title="$options.AGENT_STATUSES[item.status].tooltip.title"
<span placement="right"
:id="`connection-status-${item.name}`" container="viewport"
class="gl-pr-5" >
data-testid="cluster-agent-connection-status" <p>
> <gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.body"
<span :class="$options.AGENT_STATUSES[item.status].class" class="gl-mr-3"> ><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template></gl-sprintf
<gl-icon :name="$options.AGENT_STATUSES[item.status].icon" :size="12" /></span >
>{{ $options.AGENT_STATUSES[item.status].name }} </p>
</span> <p class="gl-mb-0">
<gl-tooltip <gl-link :href="$options.troubleshooting_link" target="_blank" class="gl-font-sm">
v-if="item.status === 'active'" {{ s__('ClusterAgents|Learn how to troubleshoot') }}</gl-link
:target="`connection-status-${item.name}`" >
placement="right" </p>
> </gl-popover>
<gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.title" </template>
><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template>
</gl-sprintf>
</gl-tooltip>
<gl-popover
v-else
:target="`connection-status-${item.name}`"
:title="$options.AGENT_STATUSES[item.status].tooltip.title"
placement="right"
container="viewport"
>
<p>
<gl-sprintf :message="$options.AGENT_STATUSES[item.status].tooltip.body"
><template #timeAgo>{{ timeFormatted(item.lastContact) }}</template></gl-sprintf
>
</p>
<p class="gl-mb-0">
{{ s__('ClusterAgents|For more troubleshooting information go to') }}
<gl-link :href="$options.TROUBLESHOOTING_LINK" target="_blank" class="gl-font-sm">
{{ $options.TROUBLESHOOTING_LINK }}</gl-link
>
</p>
</gl-popover>
</template>
<template #cell(lastContact)="{ item }"> <template #cell(lastContact)="{ item }">
<span data-testid="cluster-agent-last-contact"> <span data-testid="cluster-agent-last-contact">
<time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" /> <time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" />
<span v-else>{{ s__('ClusterAgents|Never') }}</span> <span v-else>{{ s__('ClusterAgents|Never') }}</span>
</span> </span>
</template> </template>
<template #cell(configuration)="{ item }"> <template #cell(configuration)="{ item }">
<span data-testid="cluster-agent-configuration-link"> <span data-testid="cluster-agent-configuration-link">
<!-- eslint-disable @gitlab/vue-require-i18n-strings --> <gl-link v-if="item.configFolder" :href="item.configFolder.webPath">
<gl-link v-if="item.configFolder" :href="item.configFolder.webPath"> {{ getAgentConfigPath(item.name) }}
.gitlab/agents/{{ item.name }} </gl-link>
</gl-link>
<span v-else>.gitlab/agents/{{ item.name }}</span> <span v-else>{{ getAgentConfigPath(item.name) }}</span>
<!-- eslint-enable @gitlab/vue-require-i18n-strings --> </span>
</span> </template>
</template> </gl-table>
</gl-table>
</div>
</template> </template>
...@@ -4,7 +4,6 @@ import { MAX_LIST_COUNT, ACTIVE_CONNECTION_TIME } from '../constants'; ...@@ -4,7 +4,6 @@ import { MAX_LIST_COUNT, ACTIVE_CONNECTION_TIME } from '../constants';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql'; import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import AgentEmptyState from './agent_empty_state.vue'; import AgentEmptyState from './agent_empty_state.vue';
import AgentTable from './agent_table.vue'; import AgentTable from './agent_table.vue';
import InstallAgentModal from './install_agent_modal.vue';
export default { export default {
apollo: { apollo: {
...@@ -26,7 +25,6 @@ export default { ...@@ -26,7 +25,6 @@ export default {
components: { components: {
AgentEmptyState, AgentEmptyState,
AgentTable, AgentTable,
InstallAgentModal,
GlAlert, GlAlert,
GlKeysetPagination, GlKeysetPagination,
GlLoadingIcon, GlLoadingIcon,
...@@ -135,19 +133,18 @@ export default { ...@@ -135,19 +133,18 @@ export default {
</script> </script>
<template> <template>
<gl-loading-icon v-if="isLoading" size="md" class="gl-mt-3" /> <gl-loading-icon v-if="isLoading" size="md" />
<section v-else-if="agentList" class="gl-mt-3"> <section v-else-if="agentList">
<div v-if="agentList.length"> <div v-if="agentList.length">
<AgentTable :agents="agentList" /> <agent-table :agents="agentList" />
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5"> <div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination v-bind="agentPageInfo" @prev="prevPage" @next="nextPage" /> <gl-keyset-pagination v-bind="agentPageInfo" @prev="prevPage" @next="nextPage" />
</div> </div>
</div> </div>
<AgentEmptyState v-else :has-configurations="hasConfigurations" /> <agent-empty-state v-else :has-configurations="hasConfigurations" />
<InstallAgentModal @agentRegistered="reloadAgents" />
</section> </section>
<gl-alert v-else variant="danger" :dismissible="false"> <gl-alert v-else variant="danger" :dismissible="false">
......
...@@ -57,30 +57,37 @@ export default { ...@@ -57,30 +57,37 @@ export default {
}, },
}, },
fields() { fields() {
const tdClass = 'gl-py-5!';
return [ return [
{ {
key: 'name', key: 'name',
label: __('Kubernetes cluster'), label: __('Kubernetes cluster'),
tdClass,
}, },
{ {
key: 'environment_scope', key: 'environment_scope',
label: __('Environment scope'), label: __('Environment scope'),
tdClass,
}, },
{ {
key: 'node_size', key: 'node_size',
label: __('Nodes'), label: __('Nodes'),
tdClass,
}, },
{ {
key: 'total_cpu', key: 'total_cpu',
label: __('Total cores (CPUs)'), label: __('Total cores (CPUs)'),
tdClass,
}, },
{ {
key: 'total_memory', key: 'total_memory',
label: __('Total memory (GB)'), label: __('Total memory (GB)'),
tdClass,
}, },
{ {
key: 'cluster_type', key: 'cluster_type',
label: __('Cluster level'), label: __('Cluster level'),
tdClass,
formatter: (value) => CLUSTER_TYPES[value], formatter: (value) => CLUSTER_TYPES[value],
}, },
]; ];
...@@ -201,7 +208,7 @@ export default { ...@@ -201,7 +208,7 @@ export default {
</script> </script>
<template> <template>
<gl-loading-icon v-if="loadingClusters" size="md" class="gl-mt-3" /> <gl-loading-icon v-if="loadingClusters" size="md" />
<section v-else> <section v-else>
<ancestor-notice /> <ancestor-notice />
...@@ -210,10 +217,11 @@ export default { ...@@ -210,10 +217,11 @@ export default {
v-if="hasClusters" v-if="hasClusters"
:items="clusters" :items="clusters"
:fields="fields" :fields="fields"
fixed
stacked="md" stacked="md"
head-variant="white" head-variant="white"
thead-class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100" thead-class="gl-border-b-solid gl-border-b-2 gl-border-b-gray-100"
class="qa-clusters-table" class="qa-clusters-table gl-mb-4!"
data-testid="cluster_list_table" data-testid="cluster_list_table"
> >
<template #cell(name)="{ item }"> <template #cell(name)="{ item }">
......
<script>
import { GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui';
import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '../constants';
export default {
i18n: CLUSTERS_ACTIONS,
INSTALL_AGENT_MODAL_ID,
components: {
GlDropdown,
GlDropdownItem,
},
directives: {
GlModalDirective,
},
inject: ['newClusterPath', 'addClusterPath'],
};
</script>
<template>
<div class="nav-controls gl-ml-auto">
<gl-dropdown
ref="dropdown"
v-gl-modal-directive="$options.INSTALL_AGENT_MODAL_ID"
category="primary"
variant="confirm"
:text="$options.i18n.actionsButton"
split
right
>
<gl-dropdown-item :href="newClusterPath" data-testid="new-cluster-link" @click.stop>
{{ $options.i18n.createNewCluster }}
</gl-dropdown-item>
<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-item :href="addClusterPath" data-testid="connect-cluster-link" @click.stop>
{{ $options.i18n.connectExistingCluster }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
...@@ -14,6 +14,7 @@ export default { ...@@ -14,6 +14,7 @@ export default {
}, },
inject: ['emptyStateHelpText', 'clustersEmptyStateImage', 'newClusterPath'], inject: ['emptyStateHelpText', 'clustersEmptyStateImage', 'newClusterPath'],
learnMoreHelpUrl: helpPagePath('user/project/clusters/index'), learnMoreHelpUrl: helpPagePath('user/project/clusters/index'),
multipleClustersHelpUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
computed: { computed: {
...mapState(['canAddCluster']), ...mapState(['canAddCluster']),
}, },
......
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { CLUSTERS_TABS } from '../constants';
import Agents from './agents.vue';
import InstallAgentModal from './install_agent_modal.vue';
import ClustersActions from './clusters_actions.vue';
import Clusters from './clusters.vue';
export default {
components: {
GlTabs,
GlTab,
ClustersActions,
Clusters,
Agents,
InstallAgentModal,
},
CLUSTERS_TABS,
props: {
defaultBranchName: {
default: '.noBranch',
required: false,
type: String,
},
},
data() {
return {
selectedTabIndex: 0,
};
},
methods: {
onTabChange(tabName) {
this.selectedTabIndex = CLUSTERS_TABS.findIndex((tab) => tab.queryParamValue === tabName);
},
},
};
</script>
<template>
<div>
<gl-tabs
v-model="selectedTabIndex"
sync-active-tab-with-query-params
nav-class="gl-flex-grow-1 gl-align-items-center"
lazy
>
<gl-tab
v-for="(tab, idx) in $options.CLUSTERS_TABS"
:key="idx"
:title="tab.title"
:query-param-value="tab.queryParamValue"
class="gl-line-height-20 gl-mt-5"
>
<component
:is="tab.component"
:default-branch-name="defaultBranchName"
data-testid="clusters-tab-component"
@changeTab="onTabChange"
/>
</gl-tab>
<template #tabs-end>
<clusters-actions />
</template>
</gl-tabs>
<install-agent-modal :default-branch-name="defaultBranchName" />
</div>
</template>
...@@ -12,9 +12,11 @@ import { helpPagePath } from '~/helpers/help_page_helper'; ...@@ -12,9 +12,11 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue';
import { generateAgentRegistrationCommand } from '../clusters_util'; import { generateAgentRegistrationCommand } from '../clusters_util';
import { INSTALL_AGENT_MODAL_ID, I18N_INSTALL_AGENT_MODAL } from '../constants'; import { INSTALL_AGENT_MODAL_ID, I18N_INSTALL_AGENT_MODAL, MAX_LIST_COUNT } from '../constants';
import { addAgentToStore } from '../graphql/cache_update';
import createAgent from '../graphql/mutations/create_agent.mutation.graphql'; import createAgent from '../graphql/mutations/create_agent.mutation.graphql';
import createAgentToken from '../graphql/mutations/create_agent_token.mutation.graphql'; import createAgentToken from '../graphql/mutations/create_agent_token.mutation.graphql';
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import AvailableAgentsDropdown from './available_agents_dropdown.vue'; import AvailableAgentsDropdown from './available_agents_dropdown.vue';
export default { export default {
...@@ -33,12 +35,20 @@ export default { ...@@ -33,12 +35,20 @@ export default {
GlSprintf, GlSprintf,
}, },
inject: ['projectPath', 'kasAddress'], inject: ['projectPath', 'kasAddress'],
props: {
defaultBranchName: {
default: '.noBranch',
required: false,
type: String,
},
},
data() { data() {
return { return {
registering: false, registering: false,
agentName: null, agentName: null,
agentToken: null, agentToken: null,
error: null, error: null,
clusterAgent: null,
}; };
}, },
computed: { computed: {
...@@ -62,20 +72,24 @@ export default { ...@@ -62,20 +72,24 @@ export default {
advancedInstallPath() { advancedInstallPath() {
return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' }); return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' });
}, },
getAgentsQueryVariables() {
return {
defaultBranchName: this.defaultBranchName,
first: MAX_LIST_COUNT,
last: null,
projectPath: this.projectPath,
};
},
}, },
methods: { methods: {
setAgentName(name) { setAgentName(name) {
this.agentName = name; this.agentName = name;
}, },
cancelClicked() { closeModal() {
this.$refs.modal.hide();
},
doneClicked() {
this.$emit('agentRegistered');
this.$refs.modal.hide(); this.$refs.modal.hide();
}, },
resetModal() { resetModal() {
this.registering = null; this.registering = false;
this.agentName = null; this.agentName = null;
this.agentToken = null; this.agentToken = null;
this.error = null; this.error = null;
...@@ -90,6 +104,14 @@ export default { ...@@ -90,6 +104,14 @@ export default {
projectPath: this.projectPath, projectPath: this.projectPath,
}, },
}, },
update: (store, { data: { createClusterAgent } }) => {
addAgentToStore(
store,
createClusterAgent,
getAgentsQuery,
this.getAgentsQueryVariables,
);
},
}) })
.then(({ data: { createClusterAgent } }) => createClusterAgent); .then(({ data: { createClusterAgent } }) => createClusterAgent);
}, },
...@@ -117,6 +139,8 @@ export default { ...@@ -117,6 +139,8 @@ export default {
throw new Error(agentErrors[0]); throw new Error(agentErrors[0]);
} }
this.clusterAgent = clusterAgent;
const { errors: tokenErrors, secret } = await this.createAgentTokenMutation( const { errors: tokenErrors, secret } = await this.createAgentTokenMutation(
clusterAgent.id, clusterAgent.id,
); );
...@@ -240,10 +264,10 @@ export default { ...@@ -240,10 +264,10 @@ export default {
</template> </template>
<template #modal-footer> <template #modal-footer>
<gl-button v-if="canCancel" @click="cancelClicked">{{ $options.i18n.cancel }} </gl-button> <gl-button v-if="canCancel" @click="closeModal">{{ $options.i18n.cancel }} </gl-button>
<gl-button v-if="registered" variant="confirm" category="primary" @click="doneClicked" <gl-button v-if="registered" variant="confirm" category="primary" @click="closeModal"
>{{ $options.i18n.done }} >{{ $options.i18n.close }}
</gl-button> </gl-button>
<gl-button <gl-button
...@@ -252,7 +276,7 @@ export default { ...@@ -252,7 +276,7 @@ export default {
variant="confirm" variant="confirm"
category="primary" category="primary"
@click="registerAgent" @click="registerAgent"
>{{ $options.i18n.next }} >{{ $options.i18n.registerAgentButton }}
</gl-button> </gl-button>
</template> </template>
</gl-modal> </gl-modal>
......
...@@ -3,8 +3,6 @@ import { __, s__, sprintf } from '~/locale'; ...@@ -3,8 +3,6 @@ import { __, s__, sprintf } from '~/locale';
export const MAX_LIST_COUNT = 25; export const MAX_LIST_COUNT = 25;
export const INSTALL_AGENT_MODAL_ID = 'install-agent'; export const INSTALL_AGENT_MODAL_ID = 'install-agent';
export const ACTIVE_CONNECTION_TIME = 480000; export const ACTIVE_CONNECTION_TIME = 480000;
export const TROUBLESHOOTING_LINK =
'https://docs.gitlab.com/ee/user/clusters/agent/#troubleshooting';
export const CLUSTER_ERRORS = { export const CLUSTER_ERRORS = {
default: { default: {
...@@ -66,8 +64,8 @@ export const STATUSES = { ...@@ -66,8 +64,8 @@ export const STATUSES = {
}; };
export const I18N_INSTALL_AGENT_MODAL = { export const I18N_INSTALL_AGENT_MODAL = {
next: __('Next'), registerAgentButton: s__('ClusterAgents|Register Agent'),
done: __('Done'), close: __('Close'),
cancel: __('Cancel'), cancel: __('Cancel'),
modalTitle: s__('ClusterAgents|Install new Agent'), modalTitle: s__('ClusterAgents|Install new Agent'),
...@@ -168,3 +166,23 @@ export const I18N_CLUSTERS_EMPTY_STATE = { ...@@ -168,3 +166,23 @@ export const I18N_CLUSTERS_EMPTY_STATE = {
learnMoreLinkText: s__('ClusterIntegration|Learn more about the GitLab managed clusters'), learnMoreLinkText: s__('ClusterIntegration|Learn more about the GitLab managed clusters'),
buttonText: s__('ClusterIntegration|Connect with a certificate'), buttonText: s__('ClusterIntegration|Connect with a certificate'),
}; };
export const CLUSTERS_TABS = [
{
title: s__('ClusterAgents|Agent'),
component: 'agents',
queryParamValue: 'agent',
},
{
title: s__('ClusterAgents|Certificate based'),
component: 'clusters',
queryParamValue: 'certificate_based',
},
];
export const CLUSTERS_ACTIONS = {
actionsButton: s__('ClusterAgents|Actions'),
createNewCluster: s__('ClusterAgents|Create new cluster'),
connectWithAgent: s__('ClusterAgents|Connect with Agent'),
connectExistingCluster: s__('ClusterAgents|Connect with certificate'),
};
import produce from 'immer';
import { getAgentConfigPath } from '../clusters_util';
export function addAgentToStore(store, createClusterAgent, query, variables) {
const { clusterAgent } = createClusterAgent;
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
const configuration = {
name: clusterAgent.name,
path: getAgentConfigPath(clusterAgent.name),
webPath: clusterAgent.webPath,
__typename: 'TreeEntry',
};
draftData.project.clusterAgents.nodes.push(clusterAgent);
draftData.project.repository.tree.trees.nodes.push(configuration);
});
store.writeQuery({
query,
variables,
data,
});
}
fragment ClusterAgentFragment on ClusterAgent {
id
name
webPath
tokens {
nodes {
lastUsedAt
}
}
}
#import "../fragments/cluster_agent.fragment.graphql"
mutation createClusterAgent($input: CreateClusterAgentInput!) { mutation createClusterAgent($input: CreateClusterAgentInput!) {
createClusterAgent(input: $input) { createClusterAgent(input: $input) {
clusterAgent { clusterAgent {
id ...ClusterAgentFragment
} }
errors errors
} }
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" #import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/cluster_agent.fragment.graphql"
query getAgents( query getAgents(
$defaultBranchName: String! $defaultBranchName: String!
...@@ -13,14 +14,7 @@ query getAgents( ...@@ -13,14 +14,7 @@ query getAgents(
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
clusterAgents(first: $first, last: $last, before: $beforeAgent, after: $afterAgent) { clusterAgents(first: $first, last: $last, before: $beforeAgent, after: $afterAgent) {
nodes { nodes {
id ...ClusterAgentFragment
name
webPath
tokens {
nodes {
lastUsedAt
}
}
} }
pageInfo { pageInfo {
......
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import loadClusters from './load_clusters'; import loadClusters from './load_clusters';
import loadAgents from './load_agents'; import loadMainView from './load_main_view';
Vue.use(VueApollo); Vue.use(VueApollo);
export default () => { export default () => {
loadClusters(Vue); loadClusters(Vue);
loadAgents(Vue, VueApollo); loadMainView(Vue, VueApollo);
}; };
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import Agents from './components/agents.vue'; import ClustersMainView from './components/clusters_main_view.vue';
import { createStore } from './store';
export default (Vue, VueApollo) => { Vue.use(VueApollo);
const el = document.querySelector('#js-cluster-agents-list');
export default () => {
const el = document.querySelector('.js-clusters-main-view');
if (!el) { if (!el) {
return null; return null;
...@@ -14,11 +19,11 @@ export default (Vue, VueApollo) => { ...@@ -14,11 +19,11 @@ export default (Vue, VueApollo) => {
emptyStateImage, emptyStateImage,
defaultBranchName, defaultBranchName,
projectPath, projectPath,
multipleClustersDocsUrl,
installDocsUrl,
getStartedDocsUrl,
integrationDocsUrl,
kasAddress, kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
} = el.dataset; } = el.dataset;
return new Vue({ return new Vue({
...@@ -27,14 +32,15 @@ export default (Vue, VueApollo) => { ...@@ -27,14 +32,15 @@ export default (Vue, VueApollo) => {
provide: { provide: {
emptyStateImage, emptyStateImage,
projectPath, projectPath,
multipleClustersDocsUrl,
installDocsUrl,
getStartedDocsUrl,
integrationDocsUrl,
kasAddress, kasAddress,
newClusterPath,
addClusterPath,
emptyStateHelpText,
clustersEmptyStateImage,
}, },
store: createStore(el.dataset),
render(createElement) { render(createElement) {
return createElement(Agents, { return createElement(ClustersMainView, {
props: { props: {
defaultBranchName, defaultBranchName,
}, },
......
...@@ -20,4 +20,23 @@ ...@@ -20,4 +20,23 @@
@include gl-flex-wrap; @include gl-flex-wrap;
} }
} }
.gl-card-body {
@include media-breakpoint-up(sm) {
@include gl-pt-2;
min-height: 372px;
}
}
@include media-breakpoint-down(xs) {
.nav-controls {
@include gl-w-full;
order: -1;
.gl-new-dropdown,
.split-content-button {
@include gl-w-full;
}
}
}
} }
...@@ -16,19 +16,6 @@ module ClustersHelper ...@@ -16,19 +16,6 @@ module ClustersHelper
clusterable.is_a?(Project) clusterable.is_a?(Project)
end end
def js_cluster_agents_list_data(clusterable_project)
{
default_branch_name: clusterable_project.default_branch,
empty_state_image: image_path('illustrations/empty-state/empty-state-agents.svg'),
project_path: clusterable_project.full_path,
multiple_clusters_docs_url: help_page_path('user/project/clusters/multiple_kubernetes_clusters'),
install_docs_url: help_page_path('administration/clusters/kas'),
get_started_docs_url: help_page_path('user/clusters/agent/index', anchor: 'define-a-configuration-repository'),
integration_docs_url: help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent'),
kas_address: Gitlab::Kas.external_url
}
end
def js_clusters_list_data(clusterable) def js_clusters_list_data(clusterable)
{ {
ancestor_help_path: help_page_path('user/group/clusters/index', anchor: 'cluster-precedence'), ancestor_help_path: help_page_path('user/group/clusters/index', anchor: 'cluster-precedence'),
...@@ -45,6 +32,16 @@ module ClustersHelper ...@@ -45,6 +32,16 @@ module ClustersHelper
} }
end end
def js_clusters_data(clusterable)
{
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
}.merge(js_clusters_list_data(clusterable))
end
def js_cluster_form_data(cluster, can_edit) def js_cluster_form_data(cluster, can_edit)
{ {
enabled: cluster.enabled?.to_s, enabled: cluster.enabled?.to_s,
......
...@@ -3,24 +3,10 @@ ...@@ -3,24 +3,10 @@
= render_gcp_signup_offer = render_gcp_signup_offer
.clusters-container.gl-my-2 .clusters-container
- if display_cluster_agents?(clusterable) - if display_cluster_agents?(clusterable)
.js-toggle-container .gl-my-6
%ul.nav-links.nav-tabs.nav{ role: 'tablist' } .js-clusters-main-view{ data: js_clusters_data(clusterable) }
%li.nav-item{ role: 'presentation' }
%a.nav-link.active{ href: "#certificate-clusters-pane", id: "certificate-clusters-tab", data: { toggle: 'tab' }, role: 'tab' }
%span= s_('ClusterIntegration|Clusters connected with a certificate')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: "#agent-clusters-pane", id: "agent-clusters-tab", data: { toggle: 'tab' }, role: 'tab' }
%span= s_('ClusterIntegration|GitLab Agent managed clusters')
.tab-content
.tab-pane.active{ id: 'certificate-clusters-pane', role: 'tabpanel' }
= render 'cluster_list', clusters: @clusters
.tab-pane{ id: 'agent-clusters-pane', role: 'tabpanel' }
#js-cluster-agents-list{ data: js_cluster_agents_list_data(clusterable) }
- else - else
= render 'cluster_list', clusters: @clusters = render 'cluster_list', clusters: @clusters
...@@ -7321,6 +7321,12 @@ msgstr "" ...@@ -7321,6 +7321,12 @@ msgstr ""
msgid "ClusterAgents|Access tokens" msgid "ClusterAgents|Access tokens"
msgstr "" msgstr ""
msgid "ClusterAgents|Actions"
msgstr ""
msgid "ClusterAgents|Agent"
msgstr ""
msgid "ClusterAgents|Agent might not be connected to GitLab" msgid "ClusterAgents|Agent might not be connected to GitLab"
msgstr "" msgstr ""
...@@ -7339,12 +7345,21 @@ msgstr "" ...@@ -7339,12 +7345,21 @@ msgstr ""
msgid "ClusterAgents|An unknown error occurred. Please try again." msgid "ClusterAgents|An unknown error occurred. Please try again."
msgstr "" msgstr ""
msgid "ClusterAgents|Certificate based"
msgstr ""
msgid "ClusterAgents|Configuration" msgid "ClusterAgents|Configuration"
msgstr "" msgstr ""
msgid "ClusterAgents|Connect with Agent"
msgstr ""
msgid "ClusterAgents|Connect with a GitLab Agent" msgid "ClusterAgents|Connect with a GitLab Agent"
msgstr "" msgstr ""
msgid "ClusterAgents|Connect with certificate"
msgstr ""
msgid "ClusterAgents|Connected" msgid "ClusterAgents|Connected"
msgstr "" msgstr ""
...@@ -7354,6 +7369,9 @@ msgstr "" ...@@ -7354,6 +7369,9 @@ msgstr ""
msgid "ClusterAgents|Copy token" msgid "ClusterAgents|Copy token"
msgstr "" msgstr ""
msgid "ClusterAgents|Create new cluster"
msgstr ""
msgid "ClusterAgents|Created by" msgid "ClusterAgents|Created by"
msgstr "" msgstr ""
...@@ -7369,18 +7387,12 @@ msgstr "" ...@@ -7369,18 +7387,12 @@ msgstr ""
msgid "ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}." msgid "ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}."
msgstr "" msgstr ""
msgid "ClusterAgents|For more troubleshooting information go to"
msgstr ""
msgid "ClusterAgents|Go to the repository" msgid "ClusterAgents|Go to the repository"
msgstr "" msgstr ""
msgid "ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}" msgid "ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}"
msgstr "" msgstr ""
msgid "ClusterAgents|Install a new GitLab Agent"
msgstr ""
msgid "ClusterAgents|Install new Agent" msgid "ClusterAgents|Install new Agent"
msgstr "" msgstr ""
...@@ -7393,6 +7405,9 @@ msgstr "" ...@@ -7393,6 +7405,9 @@ msgstr ""
msgid "ClusterAgents|Learn how to create an agent access token" msgid "ClusterAgents|Learn how to create an agent access token"
msgstr "" msgstr ""
msgid "ClusterAgents|Learn how to troubleshoot"
msgstr ""
msgid "ClusterAgents|Learn more about the GitLab Kubernetes Agent." msgid "ClusterAgents|Learn more about the GitLab Kubernetes Agent."
msgstr "" msgstr ""
...@@ -7417,6 +7432,9 @@ msgstr "" ...@@ -7417,6 +7432,9 @@ msgstr ""
msgid "ClusterAgents|Recommended installation method" msgid "ClusterAgents|Recommended installation method"
msgstr "" msgstr ""
msgid "ClusterAgents|Register Agent"
msgstr ""
msgid "ClusterAgents|Registering Agent" msgid "ClusterAgents|Registering Agent"
msgstr "" msgstr ""
...@@ -7603,9 +7621,6 @@ msgstr "" ...@@ -7603,9 +7621,6 @@ msgstr ""
msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters. %{linkStart}More information%{linkEnd}" msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters. %{linkStart}More information%{linkEnd}"
msgstr "" msgstr ""
msgid "ClusterIntegration|Clusters connected with a certificate"
msgstr ""
msgid "ClusterIntegration|Connect cluster with certificate" msgid "ClusterIntegration|Connect cluster with certificate"
msgstr "" msgstr ""
...@@ -7741,9 +7756,6 @@ msgstr "" ...@@ -7741,9 +7756,6 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones" msgid "ClusterIntegration|Fetching zones"
msgstr "" msgstr ""
msgid "ClusterIntegration|GitLab Agent managed clusters"
msgstr ""
msgid "ClusterIntegration|GitLab Integration" msgid "ClusterIntegration|GitLab Integration"
msgstr "" msgstr ""
......
...@@ -24,21 +24,21 @@ RSpec.describe 'Cluster agent registration', :js do ...@@ -24,21 +24,21 @@ RSpec.describe 'Cluster agent registration', :js do
end end
it 'allows the user to select an agent to install, and displays the resulting agent token' do it 'allows the user to select an agent to install, and displays the resulting agent token' do
click_link('GitLab Agent managed clusters') click_button('Actions')
click_button('Connect with a GitLab Agent')
expect(page).to have_content('Install new Agent') expect(page).to have_content('Install new Agent')
click_button('Select an Agent') click_button('Select an Agent')
click_button('example-agent-2') click_button('example-agent-2')
click_button('Next') click_button('Register Agent')
expect(page).to have_content('The token value will not be shown again after you close this window.') expect(page).to have_content('The token value will not be shown again after you close this window.')
expect(page).to have_content('example-agent-token') expect(page).to have_content('example-agent-token')
expect(page).to have_content('docker run --pull=always --rm') expect(page).to have_content('docker run --pull=always --rm')
click_button('Done') within find('.modal-footer') do
click_button('Close')
end
expect(page).to have_link('example-agent-2') expect(page).to have_link('example-agent-2')
expect(page).to have_no_content('Install new Agent')
end end
end end
...@@ -22,8 +22,6 @@ RSpec.describe 'ClusterAgents', :js do ...@@ -22,8 +22,6 @@ RSpec.describe 'ClusterAgents', :js do
end end
it 'displays empty state', :aggregate_failures do it 'displays empty state', :aggregate_failures do
click_link 'GitLab Agent managed clusters'
expect(page).to have_content('Connect with a GitLab Agent') expect(page).to have_content('Connect with a GitLab Agent')
expect(page).to have_selector('.empty-state') expect(page).to have_selector('.empty-state')
end end
...@@ -36,8 +34,6 @@ RSpec.describe 'ClusterAgents', :js do ...@@ -36,8 +34,6 @@ RSpec.describe 'ClusterAgents', :js do
end end
it 'displays a table with agent', :aggregate_failures do it 'displays a table with agent', :aggregate_failures do
click_link 'GitLab Agent managed clusters'
expect(page).to have_content(agent.name) expect(page).to have_content(agent.name)
expect(page).to have_selector('[data-testid="cluster-agent-list-table"] tbody tr', count: 1) expect(page).to have_selector('[data-testid="cluster-agent-list-table"] tbody tr', count: 1)
end end
......
...@@ -19,6 +19,7 @@ RSpec.describe 'AWS EKS Cluster', :js do ...@@ -19,6 +19,7 @@ RSpec.describe 'AWS EKS Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Connect with a certificate' click_link 'Connect with a certificate'
end end
......
...@@ -33,6 +33,7 @@ RSpec.describe 'Gcp Cluster', :js do ...@@ -33,6 +33,7 @@ RSpec.describe 'Gcp Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Connect with a certificate' click_link 'Connect with a certificate'
click_link 'Create new cluster' click_link 'Create new cluster'
click_link 'Google GKE' click_link 'Google GKE'
...@@ -143,8 +144,9 @@ RSpec.describe 'Gcp Cluster', :js do ...@@ -143,8 +144,9 @@ RSpec.describe 'Gcp Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Connect cluster with certificate' click_link 'Certificate based'
click_link 'Connect existing cluster' click_button(class: 'dropdown-toggle-split')
click_link 'Connect with certificate'
end end
it 'user sees the "Environment scope" field' do it 'user sees the "Environment scope" field' do
...@@ -158,6 +160,7 @@ RSpec.describe 'Gcp Cluster', :js do ...@@ -158,6 +160,7 @@ RSpec.describe 'Gcp Cluster', :js do
click_button 'Remove integration and resources' click_button 'Remove integration and resources'
fill_in 'confirm_cluster_name_input', with: cluster.name fill_in 'confirm_cluster_name_input', with: cluster.name
click_button 'Remove integration' click_button 'Remove integration'
click_link 'Certificate based'
end end
it 'user sees creation form with the successful message' do it 'user sees creation form with the successful message' do
...@@ -171,6 +174,7 @@ RSpec.describe 'Gcp Cluster', :js do ...@@ -171,6 +174,7 @@ RSpec.describe 'Gcp Cluster', :js do
context 'when user has not dismissed GCP signup offer' do context 'when user has not dismissed GCP signup offer' do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Certificate based'
end end
it 'user sees offer on cluster index page' do it 'user sees offer on cluster index page' do
...@@ -187,6 +191,7 @@ RSpec.describe 'Gcp Cluster', :js do ...@@ -187,6 +191,7 @@ RSpec.describe 'Gcp Cluster', :js do
context 'when user has dismissed GCP signup offer' do context 'when user has dismissed GCP signup offer' do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Certificate based'
end end
it 'user does not see offer after dismissing' do it 'user does not see offer after dismissing' do
...@@ -202,12 +207,11 @@ RSpec.describe 'Gcp Cluster', :js do ...@@ -202,12 +207,11 @@ RSpec.describe 'Gcp Cluster', :js do
end end
context 'when third party offers are disabled', :clean_gitlab_redis_shared_state do context 'when third party offers are disabled', :clean_gitlab_redis_shared_state do
let(:admin) { create(:admin) } let(:user) { create(:admin) }
before do before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin) gitlab_enable_admin_mode_sign_in(user)
gitlab_enable_admin_mode_sign_in(admin)
visit general_admin_application_settings_path visit general_admin_application_settings_path
end end
......
...@@ -25,6 +25,7 @@ RSpec.describe 'User Cluster', :js do ...@@ -25,6 +25,7 @@ RSpec.describe 'User Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Connect with a certificate' click_link 'Connect with a certificate'
click_link 'Connect existing cluster' click_link 'Connect existing cluster'
end end
...@@ -112,6 +113,7 @@ RSpec.describe 'User Cluster', :js do ...@@ -112,6 +113,7 @@ RSpec.describe 'User Cluster', :js do
click_button 'Remove integration and resources' click_button 'Remove integration and resources'
fill_in 'confirm_cluster_name_input', with: cluster.name fill_in 'confirm_cluster_name_input', with: cluster.name
click_button 'Remove integration' click_button 'Remove integration'
click_link 'Certificate based'
end end
it 'user sees creation form with the successful message' do it 'user sees creation form with the successful message' do
......
...@@ -16,6 +16,7 @@ RSpec.describe 'Clusters', :js do ...@@ -16,6 +16,7 @@ RSpec.describe 'Clusters', :js do
context 'when user does not have a cluster and visits cluster index page' do context 'when user does not have a cluster and visits cluster index page' do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Certificate based'
end end
it 'sees empty state' do it 'sees empty state' do
...@@ -33,16 +34,17 @@ RSpec.describe 'Clusters', :js do ...@@ -33,16 +34,17 @@ RSpec.describe 'Clusters', :js do
before do before do
create(:cluster, :provided_by_user, name: 'default-cluster', environment_scope: '*', projects: [project]) create(:cluster, :provided_by_user, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Certificate based'
click_button(class: 'dropdown-toggle-split')
end end
it 'user sees an add cluster button' do it 'user sees an add cluster button' do
expect(page).to have_selector('.js-add-cluster:not(.readonly)') expect(page).to have_content('Connect with certificate')
end end
context 'when user filled form with environment scope' do context 'when user filled form with environment scope' do
before do before do
click_link 'Connect cluster with certificate' click_link 'Connect with certificate'
click_link 'Connect existing cluster'
fill_in 'cluster_name', with: 'staging-cluster' fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: 'staging/*' fill_in 'cluster_environment_scope', with: 'staging/*'
click_button 'Add Kubernetes cluster' click_button 'Add Kubernetes cluster'
...@@ -70,8 +72,7 @@ RSpec.describe 'Clusters', :js do ...@@ -70,8 +72,7 @@ RSpec.describe 'Clusters', :js do
context 'when user updates duplicated environment scope' do context 'when user updates duplicated environment scope' do
before do before do
click_link 'Connect cluster with certificate' click_link 'Connect with certificate'
click_link 'Connect existing cluster'
fill_in 'cluster_name', with: 'staging-cluster' fill_in 'cluster_name', with: 'staging-cluster'
fill_in 'cluster_environment_scope', with: '*' fill_in 'cluster_environment_scope', with: '*'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'https://0.0.0.0' fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'https://0.0.0.0'
...@@ -108,15 +109,12 @@ RSpec.describe 'Clusters', :js do ...@@ -108,15 +109,12 @@ RSpec.describe 'Clusters', :js do
create(:cluster, :provided_by_gcp, name: 'default-cluster', environment_scope: '*', projects: [project]) create(:cluster, :provided_by_gcp, name: 'default-cluster', environment_scope: '*', projects: [project])
visit project_clusters_path(project) visit project_clusters_path(project)
end click_link 'Certificate based'
it 'user sees a add cluster button' do
expect(page).to have_selector('.js-add-cluster:not(.readonly)')
end end
context 'when user filled form with environment scope' do context 'when user filled form with environment scope' do
before do before do
click_link 'Connect cluster with certificate' click_button(class: 'dropdown-toggle-split')
click_link 'Create new cluster' click_link 'Create new cluster'
click_link 'Google GKE' click_link 'Google GKE'
...@@ -161,7 +159,7 @@ RSpec.describe 'Clusters', :js do ...@@ -161,7 +159,7 @@ RSpec.describe 'Clusters', :js do
context 'when user updates duplicated environment scope' do context 'when user updates duplicated environment scope' do
before do before do
click_link 'Connect cluster with certificate' click_button(class: 'dropdown-toggle-split')
click_link 'Create new cluster' click_link 'Create new cluster'
click_link 'Google GKE' click_link 'Google GKE'
...@@ -192,6 +190,7 @@ RSpec.describe 'Clusters', :js do ...@@ -192,6 +190,7 @@ RSpec.describe 'Clusters', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Certificate based'
end end
it 'user sees a table with one cluster' do it 'user sees a table with one cluster' do
...@@ -214,6 +213,7 @@ RSpec.describe 'Clusters', :js do ...@@ -214,6 +213,7 @@ RSpec.describe 'Clusters', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Certificate based'
click_link 'Connect with a certificate' click_link 'Connect with a certificate'
click_link 'Create new cluster' click_link 'Create new cluster'
end end
......
import { GlAlert, GlEmptyState, GlSprintf } from '@gitlab/ui'; import { GlAlert, GlEmptyState, GlSprintf } from '@gitlab/ui';
import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue'; import AgentEmptyState from '~/clusters_list/components/agent_empty_state.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
const emptyStateImage = '/path/to/image'; const emptyStateImage = '/path/to/image';
const projectPath = 'path/to/project'; const projectPath = 'path/to/project';
const multipleClustersDocsUrl = 'path/to/multipleClustersDocs'; const multipleClustersDocsUrl = helpPagePath('user/project/clusters/multiple_kubernetes_clusters');
const installDocsUrl = 'path/to/installDocs'; const installDocsUrl = helpPagePath('administration/clusters/kas');
const getStartedDocsUrl = 'path/to/getStartedDocs';
const integrationDocsUrl = 'path/to/integrationDocs';
describe('AgentEmptyStateComponent', () => { describe('AgentEmptyStateComponent', () => {
let wrapper; let wrapper;
...@@ -18,10 +17,6 @@ describe('AgentEmptyStateComponent', () => { ...@@ -18,10 +17,6 @@ describe('AgentEmptyStateComponent', () => {
const provideData = { const provideData = {
emptyStateImage, emptyStateImage,
projectPath, projectPath,
multipleClustersDocsUrl,
installDocsUrl,
getStartedDocsUrl,
integrationDocsUrl,
}; };
const findConfigurationsAlert = () => wrapper.findComponent(GlAlert); const findConfigurationsAlert = () => wrapper.findComponent(GlAlert);
...@@ -41,7 +36,6 @@ describe('AgentEmptyStateComponent', () => { ...@@ -41,7 +36,6 @@ describe('AgentEmptyStateComponent', () => {
afterEach(() => { afterEach(() => {
if (wrapper) { if (wrapper) {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
} }
}); });
......
import { GlButton, GlLink, GlIcon } from '@gitlab/ui'; import { GlLink, GlIcon } from '@gitlab/ui';
import AgentTable from '~/clusters_list/components/agent_table.vue'; import AgentTable from '~/clusters_list/components/agent_table.vue';
import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants'; import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
...@@ -47,7 +47,6 @@ const propsData = { ...@@ -47,7 +47,6 @@ const propsData = {
}, },
], ],
}; };
const provideData = { integrationDocsUrl: 'path/to/integrationDocs' };
describe('AgentTable', () => { describe('AgentTable', () => {
let wrapper; let wrapper;
...@@ -60,7 +59,7 @@ describe('AgentTable', () => { ...@@ -60,7 +59,7 @@ describe('AgentTable', () => {
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at); wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
beforeEach(() => { beforeEach(() => {
wrapper = mountExtended(AgentTable, { propsData, provide: provideData }); wrapper = mountExtended(AgentTable, { propsData });
}); });
afterEach(() => { afterEach(() => {
...@@ -70,10 +69,6 @@ describe('AgentTable', () => { ...@@ -70,10 +69,6 @@ describe('AgentTable', () => {
} }
}); });
it('displays header button', () => {
expect(wrapper.find(GlButton).text()).toBe('Install a new GitLab Agent');
});
describe('agent table', () => { describe('agent table', () => {
it.each` it.each`
agentName | link | lineNumber agentName | link | lineNumber
......
...@@ -54,7 +54,6 @@ describe('Agents', () => { ...@@ -54,7 +54,6 @@ describe('Agents', () => {
afterEach(() => { afterEach(() => {
if (wrapper) { if (wrapper) {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
} }
}); });
...@@ -234,7 +233,11 @@ describe('Agents', () => { ...@@ -234,7 +233,11 @@ describe('Agents', () => {
}; };
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(Agents, { mocks, propsData, provide: provideData }); wrapper = shallowMount(Agents, {
mocks,
propsData,
provide: provideData,
});
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
......
import { GlDropdown, GlDropdownItem } 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';
import { INSTALL_AGENT_MODAL_ID, CLUSTERS_ACTIONS } from '~/clusters_list/constants';
describe('ClustersActionsComponent', () => {
let wrapper;
const newClusterPath = 'path/to/create/cluster';
const addClusterPath = 'path/to/connect/existing/cluster';
const provideData = {
newClusterPath,
addClusterPath,
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findNewClusterLink = () => wrapper.findByTestId('new-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
const findConnectNewAgentLink = () => wrapper.findByTestId('connect-new-agent-link');
beforeEach(() => {
wrapper = shallowMountExtended(ClustersActions, {
provide: provideData,
directives: {
GlModalDirective: createMockDirective(),
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders actions menu', () => {
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);
});
});
import { GlTabs, GlTab } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersMainView from '~/clusters_list/components/clusters_main_view.vue';
import { CLUSTERS_TABS } from '~/clusters_list/constants';
const defaultBranchName = 'default-branch';
describe('ClustersMainViewComponent', () => {
let wrapper;
const propsData = {
defaultBranchName,
};
beforeEach(() => {
wrapper = shallowMountExtended(ClustersMainView, {
propsData,
});
});
afterEach(() => {
wrapper.destroy();
});
const findTabs = () => wrapper.findComponent(GlTabs);
const findAllTabs = () => wrapper.findAllComponents(GlTab);
const findGlTabAtIndex = (index) => findAllTabs().at(index);
const findComponent = () => wrapper.findByTestId('clusters-tab-component');
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('passes child-component param to the component', () => {
expect(findComponent().props('defaultBranchName')).toBe(defaultBranchName);
});
describe('tabs', () => {
it.each`
tabTitle | queryParamValue | lineNumber
${'Agent'} | ${'agent'} | ${0}
${'Certificate based'} | ${'certificate_based'} | ${1}
`(
'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);
},
);
});
});
...@@ -3,7 +3,8 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; ...@@ -3,7 +3,8 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue'; import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vue'; import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vue';
import { I18N_INSTALL_AGENT_MODAL } from '~/clusters_list/constants'; import { I18N_INSTALL_AGENT_MODAL, MAX_LIST_COUNT } from '~/clusters_list/constants';
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import createAgentMutation from '~/clusters_list/graphql/mutations/create_agent.mutation.graphql'; import createAgentMutation from '~/clusters_list/graphql/mutations/create_agent.mutation.graphql';
import createAgentTokenMutation from '~/clusters_list/graphql/mutations/create_agent_token.mutation.graphql'; import createAgentTokenMutation from '~/clusters_list/graphql/mutations/create_agent_token.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
...@@ -14,12 +15,16 @@ import { ...@@ -14,12 +15,16 @@ import {
createAgentErrorResponse, createAgentErrorResponse,
createAgentTokenResponse, createAgentTokenResponse,
createAgentTokenErrorResponse, createAgentTokenErrorResponse,
getAgentResponse,
} from '../mocks/apollo'; } from '../mocks/apollo';
import ModalStub from '../stubs'; import ModalStub from '../stubs';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
const projectPath = 'path/to/project';
const defaultBranchName = 'default';
describe('InstallAgentModal', () => { describe('InstallAgentModal', () => {
let wrapper; let wrapper;
let apolloProvider; let apolloProvider;
...@@ -45,10 +50,14 @@ describe('InstallAgentModal', () => { ...@@ -45,10 +50,14 @@ describe('InstallAgentModal', () => {
const createWrapper = () => { const createWrapper = () => {
const provide = { const provide = {
projectPath: 'path/to/project', projectPath,
kasAddress: 'kas.example.com', kasAddress: 'kas.example.com',
}; };
const propsData = {
defaultBranchName,
};
wrapper = shallowMount(InstallAgentModal, { wrapper = shallowMount(InstallAgentModal, {
attachTo: document.body, attachTo: document.body,
stubs: { stubs: {
...@@ -57,11 +66,26 @@ describe('InstallAgentModal', () => { ...@@ -57,11 +66,26 @@ describe('InstallAgentModal', () => {
localVue, localVue,
apolloProvider, apolloProvider,
provide, provide,
propsData,
});
};
const writeQuery = () => {
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getAgentsQuery,
variables: {
projectPath,
defaultBranchName,
first: MAX_LIST_COUNT,
last: null,
},
data: getAgentResponse.data,
}); });
}; };
const mockSelectedAgentResponse = () => { const mockSelectedAgentResponse = () => {
createWrapper(); createWrapper();
writeQuery();
wrapper.vm.setAgentName('agent-name'); wrapper.vm.setAgentName('agent-name');
findActionButton().vm.$emit('click'); findActionButton().vm.$emit('click');
...@@ -95,7 +119,7 @@ describe('InstallAgentModal', () => { ...@@ -95,7 +119,7 @@ describe('InstallAgentModal', () => {
it('renders a disabled next button', () => { it('renders a disabled next button', () => {
expect(findActionButton().isVisible()).toBe(true); expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.next); expect(findActionButton().text()).toBe(i18n.registerAgentButton);
expectDisabledAttribute(findActionButton(), true); expectDisabledAttribute(findActionButton(), true);
}); });
}); });
...@@ -126,7 +150,7 @@ describe('InstallAgentModal', () => { ...@@ -126,7 +150,7 @@ describe('InstallAgentModal', () => {
it('creates an agent and token', () => { it('creates an agent and token', () => {
expect(createAgentHandler).toHaveBeenCalledWith({ expect(createAgentHandler).toHaveBeenCalledWith({
input: { name: 'agent-name', projectPath: 'path/to/project' }, input: { name: 'agent-name', projectPath },
}); });
expect(createAgentTokenHandler).toHaveBeenCalledWith({ expect(createAgentTokenHandler).toHaveBeenCalledWith({
...@@ -134,9 +158,9 @@ describe('InstallAgentModal', () => { ...@@ -134,9 +158,9 @@ describe('InstallAgentModal', () => {
}); });
}); });
it('renders a done button', () => { it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true); expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.done); expect(findActionButton().text()).toBe(i18n.close);
expectDisabledAttribute(findActionButton(), false); expectDisabledAttribute(findActionButton(), false);
}); });
......
const agent = {
id: 'agent-id',
name: 'agent-name',
webPath: 'agent-webPath',
};
const token = {
id: 'token-id',
lastUsedAt: null,
};
const tokens = {
nodes: [token],
};
const pageInfo = {
endCursor: '',
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
};
export const createAgentResponse = { export const createAgentResponse = {
data: { data: {
createClusterAgent: { createClusterAgent: {
clusterAgent: { clusterAgent: {
id: 'agent-id', ...agent,
tokens,
}, },
errors: [], errors: [],
}, },
...@@ -13,7 +33,8 @@ export const createAgentErrorResponse = { ...@@ -13,7 +33,8 @@ export const createAgentErrorResponse = {
data: { data: {
createClusterAgent: { createClusterAgent: {
clusterAgent: { clusterAgent: {
id: 'agent-id', ...agent,
tokens,
}, },
errors: ['could not create agent'], errors: ['could not create agent'],
}, },
...@@ -23,9 +44,7 @@ export const createAgentErrorResponse = { ...@@ -23,9 +44,7 @@ export const createAgentErrorResponse = {
export const createAgentTokenResponse = { export const createAgentTokenResponse = {
data: { data: {
clusterAgentTokenCreate: { clusterAgentTokenCreate: {
token: { token,
id: 'token-id',
},
secret: 'mock-agent-token', secret: 'mock-agent-token',
errors: [], errors: [],
}, },
...@@ -35,11 +54,22 @@ export const createAgentTokenResponse = { ...@@ -35,11 +54,22 @@ export const createAgentTokenResponse = {
export const createAgentTokenErrorResponse = { export const createAgentTokenErrorResponse = {
data: { data: {
clusterAgentTokenCreate: { clusterAgentTokenCreate: {
token: { token,
id: 'token-id',
},
secret: 'mock-agent-token', secret: 'mock-agent-token',
errors: ['could not create agent token'], errors: ['could not create agent token'],
}, },
}, },
}; };
export const getAgentResponse = {
data: {
project: {
clusterAgents: { nodes: [{ ...agent, tokens }], pageInfo },
repository: {
tree: {
trees: { nodes: [{ ...agent, path: null }], pageInfo },
},
},
},
},
};
...@@ -59,35 +59,6 @@ RSpec.describe ClustersHelper do ...@@ -59,35 +59,6 @@ RSpec.describe ClustersHelper do
end end
end end
describe '#js_cluster_agents_list_data' do
let_it_be(:project) { build(:project, :repository) }
subject { helper.js_cluster_agents_list_data(project) }
it 'displays project default branch' do
expect(subject[:default_branch_name]).to eq(project.default_branch)
end
it 'displays image path' do
expect(subject[:empty_state_image]).to match(%r(/illustrations/empty-state/empty-state-agents|svg))
end
it 'displays project path' do
expect(subject[:project_path]).to eq(project.full_path)
end
it 'generates docs urls' do
expect(subject[:multiple_clusters_docs_url]).to eq(help_page_path('user/project/clusters/multiple_kubernetes_clusters'))
expect(subject[:install_docs_url]).to eq(help_page_path('administration/clusters/kas'))
expect(subject[:get_started_docs_url]).to eq(help_page_path('user/clusters/agent/index', anchor: 'define-a-configuration-repository'))
expect(subject[:integration_docs_url]).to eq(help_page_path('user/clusters/agent/index', anchor: 'get-started-with-gitops-and-the-gitlab-agent'))
end
it 'displays kas address' do
expect(subject[:kas_address]).to eq(Gitlab::Kas.external_url)
end
end
describe '#js_clusters_list_data' do describe '#js_clusters_list_data' do
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let_it_be(:project) { build(:project) } let_it_be(:project) { build(:project) }
...@@ -153,6 +124,34 @@ RSpec.describe ClustersHelper do ...@@ -153,6 +124,34 @@ RSpec.describe ClustersHelper do
end end
end end
describe '#js_clusters_data' do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { build(:project) }
let_it_be(:clusterable) { ClusterablePresenter.fabricate(project, current_user: current_user) }
subject { helper.js_clusters_data(clusterable) }
it 'displays project default branch' do
expect(subject[:default_branch_name]).to eq(project.default_branch)
end
it 'displays image path' do
expect(subject[:empty_state_image]).to match(%r(/illustrations/empty-state/empty-state-agents|svg))
end
it 'displays project path' 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
end
describe '#js_cluster_new' do describe '#js_cluster_new' do
subject { helper.js_cluster_new } subject { helper.js_cluster_new }
......
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