Commit c9563929 authored by Phil Hughes's avatar Phil Hughes

Merge branch '340759-implementation-revisit-the-kubernetes-section-ux-2' into 'master'

Develop two variants of the Install new Agent modal

See merge request gitlab-org/gitlab!73636
parents 43d76937 4f3f89bf
<script> <script>
import { GlButton, GlEmptyState, GlLink, GlSprintf, GlAlert, GlModalDirective } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlLink, GlSprintf, GlModalDirective } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper'; 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';
...@@ -8,46 +8,33 @@ export default { ...@@ -8,46 +8,33 @@ export default {
modalId: INSTALL_AGENT_MODAL_ID, modalId: INSTALL_AGENT_MODAL_ID,
multipleClustersDocsUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'), multipleClustersDocsUrl: helpPagePath('user/project/clusters/multiple_kubernetes_clusters'),
installDocsUrl: helpPagePath('administration/clusters/kas'), installDocsUrl: helpPagePath('administration/clusters/kas'),
getStartedDocsUrl: helpPagePath('user/clusters/agent/index', {
anchor: 'define-a-configuration-repository',
}),
components: { components: {
GlButton, GlButton,
GlEmptyState, GlEmptyState,
GlLink, GlLink,
GlSprintf, GlSprintf,
GlAlert,
}, },
directives: { directives: {
GlModalDirective, GlModalDirective,
}, },
inject: ['emptyStateImage', 'projectPath'], inject: ['emptyStateImage'],
props: { props: {
hasConfigurations: {
type: Boolean,
required: true,
},
isChildComponent: { isChildComponent: {
default: false, default: false,
required: false, required: false,
type: Boolean, type: Boolean,
}, },
}, },
computed: {
repositoryPath() {
return `/${this.projectPath}`;
},
},
}; };
</script> </script>
<template> <template>
<gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state"> <gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state">
<template #description> <template #description>
<p class="mw-460 gl-mx-auto gl-text-left"> <p class="gl-text-left">
{{ $options.i18n.introText }} {{ $options.i18n.introText }}
</p> </p>
<p class="mw-460 gl-mx-auto gl-text-left"> <p class="gl-text-left">
<gl-sprintf :message="$options.i18n.multipleClustersText"> <gl-sprintf :message="$options.i18n.multipleClustersText">
<template #link="{ content }"> <template #link="{ content }">
<gl-link <gl-link
...@@ -61,42 +48,17 @@ export default { ...@@ -61,42 +48,17 @@ export default {
</gl-sprintf> </gl-sprintf>
</p> </p>
<p class="mw-460 gl-mx-auto"> <p>
<gl-link :href="$options.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>
<gl-alert
v-if="!hasConfigurations"
variant="warning"
class="gl-mb-5 text-left"
:dismissible="false"
>
{{ $options.i18n.warningText }}
<template #actions>
<gl-button
category="primary"
variant="info"
:href="$options.getStartedDocsUrl"
target="_blank"
class="gl-ml-0!"
>
{{ $options.i18n.readMoreText }}
</gl-button>
<gl-button category="secondary" variant="info" :href="repositoryPath">
{{ $options.i18n.repositoryButtonText }}
</gl-button>
</template>
</gl-alert>
</template> </template>
<template #actions> <template #actions>
<gl-button <gl-button
v-if="!isChildComponent" v-if="!isChildComponent"
v-gl-modal-directive="$options.modalId" v-gl-modal-directive="$options.modalId"
:disabled="!hasConfigurations"
data-testid="integration-primary-button" data-testid="integration-primary-button"
category="primary" category="primary"
variant="confirm" variant="confirm"
......
...@@ -86,9 +86,6 @@ export default { ...@@ -86,9 +86,6 @@ export default {
treePageInfo() { treePageInfo() {
return this.agents?.project?.repository?.tree?.trees?.pageInfo || {}; return this.agents?.project?.repository?.tree?.trees?.pageInfo || {};
}, },
hasConfigurations() {
return Boolean(this.agents?.project?.repository?.tree?.trees?.nodes?.length);
},
}, },
methods: { methods: {
reloadAgents() { reloadAgents() {
...@@ -161,11 +158,7 @@ export default { ...@@ -161,11 +158,7 @@ export default {
</div> </div>
</div> </div>
<agent-empty-state <agent-empty-state v-else :is-child-component="isChildComponent" />
v-else
:has-configurations="hasConfigurations"
:is-child-component="isChildComponent"
/>
</section> </section>
<gl-alert v-else variant="danger" :dismissible="false"> <gl-alert v-else variant="danger" :dismissible="false">
......
<script> <script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants'; import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants';
import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql';
export default { export default {
name: 'AvailableAgentsDropdown', name: 'AvailableAgentsDropdown',
...@@ -10,36 +9,22 @@ export default { ...@@ -10,36 +9,22 @@ export default {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
}, },
inject: ['projectPath'],
props: { props: {
isRegistering: { isRegistering: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
}, availableAgents: {
apollo: { required: true,
agents: { type: Array,
query: agentConfigurations,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
this.populateAvailableAgents(data);
},
}, },
}, },
data() { data() {
return { return {
availableAgents: [],
selectedAgent: null, selectedAgent: null,
}; };
}, },
computed: { computed: {
isLoading() {
return this.$apollo.queries.agents.loading;
},
dropdownText() { dropdownText() {
if (this.isRegistering) { if (this.isRegistering) {
return this.$options.i18n.registeringAgent; return this.$options.i18n.registeringAgent;
...@@ -58,18 +43,11 @@ export default { ...@@ -58,18 +43,11 @@ export default {
isSelected(agent) { isSelected(agent) {
return this.selectedAgent === agent; return this.selectedAgent === agent;
}, },
populateAvailableAgents(data) {
const installedAgents = data?.project?.clusterAgents?.nodes.map((agent) => agent.name) ?? [];
const configuredAgents =
data?.project?.agentConfigurations?.nodes.map((config) => config.agentName) ?? [];
this.availableAgents = configuredAgents.filter((agent) => !installedAgents.includes(agent));
},
}, },
}; };
</script> </script>
<template> <template>
<gl-dropdown :text="dropdownText" :loading="isLoading || isRegistering"> <gl-dropdown :text="dropdownText" :loading="isRegistering">
<gl-dropdown-item <gl-dropdown-item
v-for="agent in availableAgents" v-for="agent in availableAgents"
:key="agent" :key="agent"
......
...@@ -12,16 +12,16 @@ import { helpPagePath } from '~/helpers/help_page_helper'; ...@@ -12,16 +12,16 @@ 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_AGENT_MODAL, KAS_DISABLED_ERROR } from '../constants';
import { addAgentToStore } from '../graphql/cache_update'; import { addAgentToStore, addAgentConfigToStore } 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 getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql';
import AvailableAgentsDropdown from './available_agents_dropdown.vue'; import AvailableAgentsDropdown from './available_agents_dropdown.vue';
export default { export default {
modalId: INSTALL_AGENT_MODAL_ID, modalId: INSTALL_AGENT_MODAL_ID,
i18n: I18N_INSTALL_AGENT_MODAL,
components: { components: {
AvailableAgentsDropdown, AvailableAgentsDropdown,
ClipboardButton, ClipboardButton,
...@@ -34,7 +34,7 @@ export default { ...@@ -34,7 +34,7 @@ export default {
GlModal, GlModal,
GlSprintf, GlSprintf,
}, },
inject: ['projectPath', 'kasAddress'], inject: ['projectPath', 'kasAddress', 'emptyStateImage'],
props: { props: {
defaultBranchName: { defaultBranchName: {
default: '.noBranch', default: '.noBranch',
...@@ -46,6 +46,22 @@ export default { ...@@ -46,6 +46,22 @@ export default {
type: Number, type: Number,
}, },
}, },
apollo: {
agents: {
query: agentConfigurations,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
this.populateAvailableAgents(data);
},
error(error) {
this.kasDisabled = error?.message?.indexOf(KAS_DISABLED_ERROR) >= 0;
},
},
},
data() { data() {
return { return {
registering: false, registering: false,
...@@ -53,6 +69,8 @@ export default { ...@@ -53,6 +69,8 @@ export default {
agentToken: null, agentToken: null,
error: null, error: null,
clusterAgent: null, clusterAgent: null,
availableAgents: [],
kasDisabled: false,
}; };
}, },
computed: { computed: {
...@@ -63,7 +81,7 @@ export default { ...@@ -63,7 +81,7 @@ export default {
return !this.registering && this.agentName !== null; return !this.registering && this.agentName !== null;
}, },
canCancel() { canCancel() {
return !this.registered && !this.registering; return !this.registered && !this.registering && this.isRegisterModal;
}, },
agentRegistrationCommand() { agentRegistrationCommand() {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress); return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
...@@ -76,6 +94,9 @@ export default { ...@@ -76,6 +94,9 @@ export default {
advancedInstallPath() { advancedInstallPath() {
return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' }); return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' });
}, },
enableKasPath() {
return helpPagePath('administration/clusters/kas');
},
getAgentsQueryVariables() { getAgentsQueryVariables() {
return { return {
defaultBranchName: this.defaultBranchName, defaultBranchName: this.defaultBranchName,
...@@ -84,6 +105,29 @@ export default { ...@@ -84,6 +105,29 @@ export default {
projectPath: this.projectPath, projectPath: this.projectPath,
}; };
}, },
installAgentPath() {
return helpPagePath('user/clusters/agent/index', {
anchor: 'define-a-configuration-repository',
});
},
i18n() {
return I18N_AGENT_MODAL[this.modalType];
},
repositoryPath() {
return `/${this.projectPath}`;
},
modalType() {
return !this.availableAgents?.length && !this.registered ? 'install' : 'register';
},
modalSize() {
return this.isInstallModal ? 'sm' : 'md';
},
isInstallModal() {
return this.modalType === 'install';
},
isRegisterModal() {
return this.modalType === 'register';
},
}, },
methods: { methods: {
setAgentName(name) { setAgentName(name) {
...@@ -96,8 +140,16 @@ export default { ...@@ -96,8 +140,16 @@ export default {
this.registering = false; this.registering = false;
this.agentName = null; this.agentName = null;
this.agentToken = null; this.agentToken = null;
this.clusterAgent = null;
this.error = null; this.error = null;
}, },
populateAvailableAgents(data) {
const installedAgents = data?.project?.clusterAgents?.nodes.map((agent) => agent.name) ?? [];
const configuredAgents =
data?.project?.agentConfigurations?.nodes.map((config) => config.agentName) ?? [];
this.availableAgents = configuredAgents.filter((agent) => !installedAgents.includes(agent));
},
createAgentMutation() { createAgentMutation() {
return this.$apollo return this.$apollo
.mutate({ .mutate({
...@@ -117,7 +169,9 @@ export default { ...@@ -117,7 +169,9 @@ export default {
); );
}, },
}) })
.then(({ data: { createClusterAgent } }) => createClusterAgent); .then(({ data: { createClusterAgent } }) => {
return createClusterAgent;
});
}, },
createAgentTokenMutation(agendId) { createAgentTokenMutation(agendId) {
return this.$apollo return this.$apollo
...@@ -129,6 +183,17 @@ export default { ...@@ -129,6 +183,17 @@ export default {
name: this.agentName, name: this.agentName,
}, },
}, },
update: (store, { data: { clusterAgentTokenCreate } }) => {
addAgentConfigToStore(
store,
clusterAgentTokenCreate,
this.clusterAgent,
agentConfigurations,
{
projectPath: this.projectPath,
},
);
},
}) })
.then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate); .then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate);
}, },
...@@ -158,7 +223,7 @@ export default { ...@@ -158,7 +223,7 @@ export default {
if (error) { if (error) {
this.error = error.message; this.error = error.message;
} else { } else {
this.error = this.$options.i18n.unknownError; this.error = this.i18n.unknownError;
} }
} finally { } finally {
this.registering = false; this.registering = false;
...@@ -172,115 +237,142 @@ export default { ...@@ -172,115 +237,142 @@ export default {
<gl-modal <gl-modal
ref="modal" ref="modal"
:modal-id="$options.modalId" :modal-id="$options.modalId"
:title="$options.i18n.modalTitle" :title="i18n.modalTitle"
:size="modalSize"
static static
lazy lazy
@hidden="resetModal" @hidden="resetModal"
> >
<template v-if="!registered"> <template v-if="isRegisterModal">
<p> <template v-if="!registered">
<strong>{{ $options.i18n.selectAgentTitle }}</strong> <p>
</p> <strong>{{ i18n.selectAgentTitle }}</strong>
</p>
<p> <p class="gl-mb-0">{{ i18n.selectAgentBody }}</p>
<gl-sprintf :message="$options.i18n.selectAgentBody"> <p>
<template #link="{ content }"> <gl-link :href="basicInstallPath" target="_blank"> {{ i18n.learnMoreLink }}</gl-link>
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link> </p>
</template>
</gl-sprintf>
</p>
<form> <form>
<gl-form-group label-for="agent-name"> <gl-form-group label-for="agent-name">
<available-agents-dropdown <available-agents-dropdown
class="gl-w-70p" class="gl-w-70p"
:is-registering="registering" :is-registering="registering"
@agentSelected="setAgentName" :available-agents="availableAgents"
/> @agentSelected="setAgentName"
</gl-form-group> />
</form> </gl-form-group>
</form>
<p v-if="error"> <p v-if="error">
<gl-alert <gl-alert :title="i18n.registrationErrorTitle" variant="danger" :dismissible="false">
:title="$options.i18n.registrationErrorTitle" {{ error }}
variant="danger" </gl-alert>
:dismissible="false" </p>
> </template>
{{ error }}
</gl-alert>
</p>
</template>
<template v-else> <template v-else>
<p> <p>
<strong>{{ $options.i18n.tokenTitle }}</strong> <strong>{{ i18n.tokenTitle }}</strong>
</p> </p>
<p> <p>
<gl-sprintf :message="$options.i18n.tokenBody"> <gl-sprintf :message="i18n.tokenBody">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link> <gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</p> </p>
<p> <p>
<gl-alert <gl-alert :title="i18n.tokenSingleUseWarningTitle" variant="warning" :dismissible="false">
:title="$options.i18n.tokenSingleUseWarningTitle" {{ i18n.tokenSingleUseWarningBody }}
variant="warning" </gl-alert>
:dismissible="false" </p>
>
{{ $options.i18n.tokenSingleUseWarningBody }}
</gl-alert>
</p>
<p> <p>
<gl-form-input-group readonly :value="agentToken" :select-on-click="true"> <gl-form-input-group readonly :value="agentToken" :select-on-click="true">
<template #append> <template #append>
<clipboard-button :text="agentToken" :title="$options.i18n.copyToken" /> <clipboard-button :text="agentToken" :title="i18n.copyToken" />
</template> </template>
</gl-form-input-group> </gl-form-input-group>
</p> </p>
<p> <p>
<strong>{{ $options.i18n.basicInstallTitle }}</strong> <strong>{{ i18n.basicInstallTitle }}</strong>
</p> </p>
<p> <p>
{{ $options.i18n.basicInstallBody }} {{ i18n.basicInstallBody }}
</p> </p>
<p> <p>
<code-block :code="agentRegistrationCommand" /> <code-block :code="agentRegistrationCommand" />
</p> </p>
<p> <p>
<strong>{{ $options.i18n.advancedInstallTitle }}</strong> <strong>{{ i18n.advancedInstallTitle }}</strong>
</p> </p>
<p> <p>
<gl-sprintf :message="$options.i18n.advancedInstallBody"> <gl-sprintf :message="i18n.advancedInstallBody">
<template #link="{ content }">
<gl-link :href="advancedInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</template>
<template v-else>
<div class="gl-text-center gl-mb-5">
<img :alt="i18n.altText" :src="emptyStateImage" height="100" />
</div>
<p>{{ i18n.modalBody }}</p>
<p v-if="kasDisabled">
<gl-sprintf :message="i18n.enableKasText">
<template #link="{ content }"> <template #link="{ content }">
<gl-link :href="advancedInstallPath" target="_blank"> {{ content }}</gl-link> <gl-link :href="enableKasPath"> {{ content }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</p> </p>
<p class="gl-mb-0">
<gl-link :href="installAgentPath">
{{ i18n.docsLinkText }}
</gl-link>
</p>
</template> </template>
<template #modal-footer> <template #modal-footer>
<gl-button v-if="canCancel" @click="closeModal">{{ $options.i18n.cancel }} </gl-button> <gl-button v-if="canCancel" @click="closeModal">{{ i18n.cancel }} </gl-button>
<gl-button v-if="registered" variant="confirm" category="primary" @click="closeModal" <gl-button v-if="registered" variant="confirm" category="primary" @click="closeModal"
>{{ $options.i18n.close }} >{{ i18n.close }}
</gl-button> </gl-button>
<gl-button <gl-button
v-else v-else-if="isRegisterModal"
:disabled="!nextButtonDisabled" :disabled="!nextButtonDisabled"
variant="confirm" variant="confirm"
category="primary" category="primary"
@click="registerAgent" @click="registerAgent"
>{{ $options.i18n.registerAgentButton }} >{{ i18n.registerAgentButton }}
</gl-button>
<gl-button
v-if="isInstallModal"
:href="repositoryPath"
variant="confirm"
category="secondary"
data-testid="agent-secondary-button"
>{{ i18n.secondaryButton }}
</gl-button>
<gl-button v-if="isInstallModal" variant="confirm" category="primary" @click="closeModal"
>{{ i18n.done }}
</gl-button> </gl-button>
</template> </template>
</gl-modal> </gl-modal>
......
...@@ -64,45 +64,63 @@ export const STATUSES = { ...@@ -64,45 +64,63 @@ export const STATUSES = {
creating: { title: __('Creating') }, creating: { title: __('Creating') },
}; };
export const I18N_INSTALL_AGENT_MODAL = { export const I18N_AGENT_MODAL = {
registerAgentButton: s__('ClusterAgents|Register Agent'), register: {
close: __('Close'), registerAgentButton: s__('ClusterAgents|Register Agent'),
cancel: __('Cancel'), close: __('Close'),
cancel: __('Cancel'),
modalTitle: s__('ClusterAgents|Install new Agent'), modalTitle: s__('ClusterAgents|Connect with Agent'),
selectAgentTitle: s__('ClusterAgents|Select which Agent you want to install'), selectAgentTitle: s__('ClusterAgents|Select which Agent you want to install'),
selectAgentBody: s__( selectAgentBody: s__(
`ClusterAgents|Select the Agent you want to register with GitLab and install on your cluster. To learn more about the Kubernetes Agent registration process %{linkStart}go to the documentation%{linkEnd}.`, 'ClusterAgents|Select an Agent to register with GitLab and install on your cluster.',
), ),
learnMoreLink: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent registration.'),
copyToken: s__('ClusterAgents|Copy token'), copyToken: s__('ClusterAgents|Copy token'),
tokenTitle: s__('ClusterAgents|Registration token'), tokenTitle: s__('ClusterAgents|Registration token'),
tokenBody: s__( tokenBody: s__(
`ClusterAgents|The registration token will be used to connect the Agent on your cluster to GitLab. To learn more about the registration tokens and how they are used %{linkStart}go to the documentation%{linkEnd}.`, `ClusterAgents|The registration token will be used to connect the Agent on your cluster to GitLab. To learn more about the registration tokens and how they are used %{linkStart}go to the documentation%{linkEnd}.`,
), ),
tokenSingleUseWarningTitle: s__( tokenSingleUseWarningTitle: s__(
'ClusterAgents|The token value will not be shown again after you close this window.', 'ClusterAgents|The token value will not be shown again after you close this window.',
), ),
tokenSingleUseWarningBody: s__( tokenSingleUseWarningBody: s__(
`ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window.`, `ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window.`,
), ),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'), basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
basicInstallBody: __( basicInstallBody: __(
`Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`, `Open a CLI and connect to the cluster you want to install the Agent in. Use this installation method to minimize any manual steps. The token is already included in the command.`,
), ),
advancedInstallTitle: s__('ClusterAgents|Alternative installation methods'), advancedInstallTitle: s__('ClusterAgents|Alternative installation methods'),
advancedInstallBody: s__( advancedInstallBody: s__(
'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.', 'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
), ),
registrationErrorTitle: __('Failed to register Agent'), registrationErrorTitle: __('Failed to register Agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'), unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
},
install: {
modalTitle: s__('ClusterAgents|Install new Agent'),
modalBody: s__(
'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
),
docsLinkText: s__('ClusterAgents|Learn more about installing a GitLab Kubernetes Agent'),
enableKasText: s__(
'ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}',
),
altText: s__('ClusterAgents|GitLab Kubernetes Agent'),
secondaryButton: s__('ClusterAgents|Go to the repository'),
done: __('Done'),
},
}; };
export const KAS_DISABLED_ERROR = 'Gitlab::Kas::Client::ConfigurationError';
export const I18N_AVAILABLE_AGENTS_DROPDOWN = { export const I18N_AVAILABLE_AGENTS_DROPDOWN = {
selectAgent: s__('ClusterAgents|Select an Agent'), selectAgent: s__('ClusterAgents|Select an Agent'),
registeringAgent: s__('ClusterAgents|Registering Agent'), registeringAgent: s__('ClusterAgents|Registering Agent'),
...@@ -149,11 +167,6 @@ export const I18N_AGENTS_EMPTY_STATE = { ...@@ -149,11 +167,6 @@ export const I18N_AGENTS_EMPTY_STATE = {
'ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}', 'ClusterAgents|If you are setting up multiple clusters and are using Auto DevOps, %{linkStart}read about using multiple Kubernetes clusters first.%{linkEnd}',
), ),
learnMoreText: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent.'), learnMoreText: s__('ClusterAgents|Learn more about the GitLab Kubernetes Agent.'),
warningText: s__(
'ClusterAgents|To install an Agent you should create an agent directory in the Repository first. We recommend that you add the Agent configuration to the directory before you start the installation process.',
),
readMoreText: s__('ClusterAgents|Read more about getting started'),
repositoryButtonText: s__('ClusterAgents|Go to the repository'),
primaryButtonText: s__('ClusterAgents|Connect with a GitLab Agent'), primaryButtonText: s__('ClusterAgents|Connect with a GitLab Agent'),
}; };
......
import produce from 'immer'; import produce from 'immer';
import { getAgentConfigPath } from '../clusters_util'; import { getAgentConfigPath } from '../clusters_util';
export const hasErrors = ({ errors = [] }) => errors?.length;
export function addAgentToStore(store, createClusterAgent, query, variables) { export function addAgentToStore(store, createClusterAgent, query, variables) {
const { clusterAgent } = createClusterAgent; if (!hasErrors(createClusterAgent)) {
const sourceData = store.readQuery({ const { clusterAgent } = createClusterAgent;
query, const sourceData = store.readQuery({
variables, query,
}); variables,
});
const data = produce(sourceData, (draftData) => {
const configuration = { const data = produce(sourceData, (draftData) => {
name: clusterAgent.name, const configuration = {
path: getAgentConfigPath(clusterAgent.name), name: clusterAgent.name,
webPath: clusterAgent.webPath, path: getAgentConfigPath(clusterAgent.name),
__typename: 'TreeEntry', webPath: clusterAgent.webPath,
}; __typename: 'TreeEntry',
};
draftData.project.clusterAgents.nodes.push(clusterAgent);
draftData.project.clusterAgents.count += 1; draftData.project.clusterAgents.nodes.push(clusterAgent);
draftData.project.repository.tree.trees.nodes.push(configuration); draftData.project.clusterAgents.count += 1;
}); draftData.project.repository.tree.trees.nodes.push(configuration);
});
store.writeQuery({
query, store.writeQuery({
variables, query,
data, variables,
}); data,
});
}
}
export function addAgentConfigToStore(
store,
clusterAgentTokenCreate,
clusterAgent,
query,
variables,
) {
if (!hasErrors(clusterAgentTokenCreate)) {
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
const configuration = {
agentName: clusterAgent.name,
__typename: 'AgentConfiguration',
};
draftData.project.clusterAgents.nodes.push(clusterAgent);
draftData.project.agentConfigurations.nodes.push(configuration);
});
store.writeQuery({
query,
variables,
data,
});
}
} }
...@@ -7,20 +7,6 @@ ...@@ -7,20 +7,6 @@
} }
} }
.agents-empty-state {
.text-content {
@include gl-max-w-full;
@include media-breakpoint-up(lg) {
max-width: 70%;
}
}
.gl-alert-actions {
@include gl-mt-0;
@include gl-flex-wrap;
}
}
.gl-card-body { .gl-card-body {
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
@include gl-pt-2; @include gl-pt-2;
......
...@@ -14,7 +14,7 @@ module Resolvers ...@@ -14,7 +14,7 @@ module Resolvers
return [] unless can_read_agent_configuration? return [] unless can_read_agent_configuration?
kas_client.list_agent_config_files(project: project) kas_client.list_agent_config_files(project: project)
rescue GRPC::BadStatus => e rescue GRPC::BadStatus, Gitlab::Kas::Client::ConfigurationError => e
raise Gitlab::Graphql::Errors::ResourceNotAvailable, e.class.name raise Gitlab::Graphql::Errors::ResourceNotAvailable, e.class.name
end end
......
...@@ -7423,6 +7423,9 @@ msgstr "" ...@@ -7423,6 +7423,9 @@ msgstr ""
msgid "ClusterAgents|GitLab Agents provide an increased level of security when integrating with clusters. %{linkStart}Learn more about the GitLab Kubernetes Agent.%{linkEnd}" msgid "ClusterAgents|GitLab Agents provide an increased level of security when integrating with clusters. %{linkStart}Learn more about the GitLab Kubernetes Agent.%{linkEnd}"
msgstr "" msgstr ""
msgid "ClusterAgents|GitLab Kubernetes Agent"
msgstr ""
msgid "ClusterAgents|Go to the repository" msgid "ClusterAgents|Go to the repository"
msgstr "" msgstr ""
...@@ -7444,6 +7447,12 @@ msgstr "" ...@@ -7444,6 +7447,12 @@ msgstr ""
msgid "ClusterAgents|Learn how to troubleshoot" msgid "ClusterAgents|Learn how to troubleshoot"
msgstr "" msgstr ""
msgid "ClusterAgents|Learn more about installing a GitLab Kubernetes Agent"
msgstr ""
msgid "ClusterAgents|Learn more about the GitLab Kubernetes Agent registration."
msgstr ""
msgid "ClusterAgents|Learn more about the GitLab Kubernetes Agent." msgid "ClusterAgents|Learn more about the GitLab Kubernetes Agent."
msgstr "" msgstr ""
...@@ -7468,9 +7477,6 @@ msgstr "" ...@@ -7468,9 +7477,6 @@ msgstr ""
msgid "ClusterAgents|Not connected" msgid "ClusterAgents|Not connected"
msgstr "" msgstr ""
msgid "ClusterAgents|Read more about getting started"
msgstr ""
msgid "ClusterAgents|Recommended" msgid "ClusterAgents|Recommended"
msgstr "" msgstr ""
...@@ -7492,7 +7498,7 @@ msgstr "" ...@@ -7492,7 +7498,7 @@ msgstr ""
msgid "ClusterAgents|Select an Agent" msgid "ClusterAgents|Select an Agent"
msgstr "" msgstr ""
msgid "ClusterAgents|Select the Agent you want to register with GitLab and install on your cluster. To learn more about the Kubernetes Agent registration process %{linkStart}go to the documentation%{linkEnd}." msgid "ClusterAgents|Select an Agent to register with GitLab and install on your cluster."
msgstr "" msgstr ""
msgid "ClusterAgents|Select which Agent you want to install" msgid "ClusterAgents|Select which Agent you want to install"
...@@ -7501,6 +7507,9 @@ msgstr "" ...@@ -7501,6 +7507,9 @@ msgstr ""
msgid "ClusterAgents|The Agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}." msgid "ClusterAgents|The Agent has not been connected in a long time. There might be a connectivity issue. Last contact was %{timeAgo}."
msgstr "" msgstr ""
msgid "ClusterAgents|The GitLab Agent also requires %{linkStart}enabling the Agent Server%{linkEnd}"
msgstr ""
msgid "ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window." msgid "ClusterAgents|The recommended installation method provided below includes the token. If you want to follow the alternative installation method provided in the docs make sure you save the token value before you close the window."
msgstr "" msgstr ""
......
...@@ -25,7 +25,7 @@ RSpec.describe 'Cluster agent registration', :js do ...@@ -25,7 +25,7 @@ RSpec.describe 'Cluster agent registration', :js do
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_button('Actions') click_button('Actions')
expect(page).to have_content('Install new Agent') expect(page).to have_content('Register Agent')
click_button('Select an Agent') click_button('Select an Agent')
click_button('example-agent-2') click_button('example-agent-2')
......
import { GlAlert, GlEmptyState, GlSprintf } from '@gitlab/ui'; import { 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 { INSTALL_AGENT_MODAL_ID } from '~/clusters_list/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
const emptyStateImage = '/path/to/image'; const emptyStateImage = '/path/to/image';
const projectPath = 'path/to/project';
const multipleClustersDocsUrl = helpPagePath('user/project/clusters/multiple_kubernetes_clusters'); const multipleClustersDocsUrl = helpPagePath('user/project/clusters/multiple_kubernetes_clusters');
const installDocsUrl = helpPagePath('administration/clusters/kas'); const installDocsUrl = helpPagePath('administration/clusters/kas');
describe('AgentEmptyStateComponent', () => { describe('AgentEmptyStateComponent', () => {
let wrapper; let wrapper;
const propsData = {
hasConfigurations: false,
};
const provideData = { const provideData = {
emptyStateImage, emptyStateImage,
projectPath,
}; };
const findConfigurationsAlert = () => wrapper.findComponent(GlAlert);
const findMultipleClustersDocsLink = () => wrapper.findByTestId('multiple-clusters-docs-link'); const findMultipleClustersDocsLink = () => wrapper.findByTestId('multiple-clusters-docs-link');
const findInstallDocsLink = () => wrapper.findByTestId('install-docs-link'); const findInstallDocsLink = () => wrapper.findByTestId('install-docs-link');
const findIntegrationButton = () => wrapper.findByTestId('integration-primary-button'); const findIntegrationButton = () => wrapper.findByTestId('integration-primary-button');
...@@ -27,8 +22,10 @@ describe('AgentEmptyStateComponent', () => { ...@@ -27,8 +22,10 @@ describe('AgentEmptyStateComponent', () => {
beforeEach(() => { beforeEach(() => {
wrapper = shallowMountExtended(AgentEmptyState, { wrapper = shallowMountExtended(AgentEmptyState, {
propsData,
provide: provideData, provide: provideData,
directives: {
GlModalDirective: createMockDirective(),
},
stubs: { GlEmptyState, GlSprintf }, stubs: { GlEmptyState, GlSprintf },
}); });
}); });
...@@ -39,33 +36,22 @@ describe('AgentEmptyStateComponent', () => { ...@@ -39,33 +36,22 @@ describe('AgentEmptyStateComponent', () => {
} }
}); });
it('renders the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('renders button for the agent registration', () => {
expect(findIntegrationButton().exists()).toBe(true);
});
it('renders correct href attributes for the links', () => { it('renders correct href attributes for the links', () => {
expect(findMultipleClustersDocsLink().attributes('href')).toBe(multipleClustersDocsUrl); expect(findMultipleClustersDocsLink().attributes('href')).toBe(multipleClustersDocsUrl);
expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl); expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl);
}); });
describe('when there are no agent configurations in repository', () => { it('renders correct modal id for the agent registration modal', () => {
it('should render notification message box', () => { const binding = getBinding(findIntegrationButton().element, 'gl-modal-directive');
expect(findConfigurationsAlert().exists()).toBe(true);
});
it('should disable integration button', () => {
expect(findIntegrationButton().attributes('disabled')).toBe('true');
});
});
describe('when there is a list of agent configurations', () => { expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
beforeEach(() => {
propsData.hasConfigurations = true;
wrapper = shallowMountExtended(AgentEmptyState, {
propsData,
provide: provideData,
});
});
it('should render content without notification message box', () => {
expect(findEmptyState().exists()).toBe(true);
expect(findConfigurationsAlert().exists()).toBe(false);
expect(findIntegrationButton().attributes('disabled')).toBeUndefined();
});
}); });
}); });
...@@ -19,7 +19,6 @@ describe('Agents', () => { ...@@ -19,7 +19,6 @@ describe('Agents', () => {
}; };
const provideData = { const provideData = {
projectPath: 'path/to/project', projectPath: 'path/to/project',
kasAddress: 'kas.example.com',
}; };
const createWrapper = ({ props = {}, agents = [], pageInfo = null, trees = [], count = 0 }) => { const createWrapper = ({ props = {}, agents = [], pageInfo = null, trees = [], count = 0 }) => {
...@@ -216,24 +215,6 @@ describe('Agents', () => { ...@@ -216,24 +215,6 @@ describe('Agents', () => {
}); });
}); });
describe('when the agent configurations are present', () => {
const trees = [
{
name: 'agent-1',
path: '.gitlab/agents/agent-1',
webPath: '/project/path/.gitlab/agents/agent-1',
},
];
beforeEach(() => {
return createWrapper({ agents: [], trees });
});
it('should pass the correct hasConfigurations boolean value to empty state component', () => {
expect(findEmptyState().props('hasConfigurations')).toEqual(true);
});
});
describe('when agents query has errored', () => { describe('when agents query has errored', () => {
beforeEach(() => { beforeEach(() => {
return createWrapper({ agents: null }); return createWrapper({ agents: null });
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
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 { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants'; import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants';
import agentConfigurationsQuery from '~/clusters_list/graphql/queries/agent_configurations.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { agentConfigurationsResponse } from './mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('AvailableAgentsDropdown', () => { describe('AvailableAgentsDropdown', () => {
let wrapper; let wrapper;
...@@ -18,46 +11,19 @@ describe('AvailableAgentsDropdown', () => { ...@@ -18,46 +11,19 @@ describe('AvailableAgentsDropdown', () => {
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findConfiguredAgentItem = () => findDropdownItems().at(0); const findConfiguredAgentItem = () => findDropdownItems().at(0);
const createWrapper = ({ propsData = {}, isLoading = false }) => { const createWrapper = ({ propsData }) => {
const provide = { wrapper = shallowMount(AvailableAgentsDropdown, {
projectPath: 'path/to/project', propsData,
}; });
wrapper = (() => {
if (isLoading) {
const mocks = {
$apollo: {
queries: {
agents: {
loading: true,
},
},
},
};
return mount(AvailableAgentsDropdown, { mocks, provide, propsData });
}
const apolloProvider = createMockApollo([
[agentConfigurationsQuery, jest.fn().mockResolvedValue(agentConfigurationsResponse)],
]);
return mount(AvailableAgentsDropdown, {
localVue,
apolloProvider,
provide,
propsData,
});
})();
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('there are agents available', () => { describe('there are agents available', () => {
const propsData = { const propsData = {
availableAgents: ['configured-agent'],
isRegistering: false, isRegistering: false,
}; };
...@@ -69,12 +35,6 @@ describe('AvailableAgentsDropdown', () => { ...@@ -69,12 +35,6 @@ describe('AvailableAgentsDropdown', () => {
expect(findDropdown().props('text')).toBe(i18n.selectAgent); expect(findDropdown().props('text')).toBe(i18n.selectAgent);
}); });
it('shows only agents that are not yet installed', () => {
expect(findDropdownItems()).toHaveLength(1);
expect(findConfiguredAgentItem().text()).toBe('configured-agent');
expect(findConfiguredAgentItem().props('isChecked')).toBe(false);
});
describe('click events', () => { describe('click events', () => {
beforeEach(() => { beforeEach(() => {
findConfiguredAgentItem().vm.$emit('click'); findConfiguredAgentItem().vm.$emit('click');
...@@ -93,6 +53,7 @@ describe('AvailableAgentsDropdown', () => { ...@@ -93,6 +53,7 @@ describe('AvailableAgentsDropdown', () => {
describe('registration in progress', () => { describe('registration in progress', () => {
const propsData = { const propsData = {
availableAgents: ['configured-agent'],
isRegistering: true, isRegistering: true,
}; };
...@@ -108,22 +69,4 @@ describe('AvailableAgentsDropdown', () => { ...@@ -108,22 +69,4 @@ describe('AvailableAgentsDropdown', () => {
expect(findDropdown().props('loading')).toBe(true); expect(findDropdown().props('loading')).toBe(true);
}); });
}); });
describe('agents query is loading', () => {
const propsData = {
isRegistering: false,
};
beforeEach(() => {
createWrapper({ propsData, isLoading: true });
});
it('updates the text in the dropdown', () => {
expect(findDropdown().text()).toBe(i18n.selectAgent);
});
it('displays a loading icon', () => {
expect(findDropdown().props('loading')).toBe(true);
});
});
}); });
import { GlAlert, GlButton, GlFormInputGroup } from '@gitlab/ui'; import { GlAlert, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
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, MAX_LIST_COUNT } from '~/clusters_list/constants'; import { I18N_AGENT_MODAL, MAX_LIST_COUNT } from '~/clusters_list/constants';
import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql'; import getAgentsQuery from '~/clusters_list/graphql/queries/get_agents.query.graphql';
import getAgentConfigurations from '~/clusters_list/graphql/queries/agent_configurations.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';
...@@ -23,6 +25,9 @@ const localVue = createLocalVue(); ...@@ -23,6 +25,9 @@ const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
const projectPath = 'path/to/project'; const projectPath = 'path/to/project';
const kasAddress = 'kas.example.com';
const kasEnabled = true;
const emptyStateImage = 'path/to/image';
const defaultBranchName = 'default'; const defaultBranchName = 'default';
const maxAgents = MAX_LIST_COUNT; const maxAgents = MAX_LIST_COUNT;
...@@ -30,7 +35,16 @@ describe('InstallAgentModal', () => { ...@@ -30,7 +35,16 @@ describe('InstallAgentModal', () => {
let wrapper; let wrapper;
let apolloProvider; let apolloProvider;
const i18n = I18N_INSTALL_AGENT_MODAL; const configurations = [{ agentName: 'agent-name' }];
const apolloQueryResponse = {
data: {
project: {
clusterAgents: { nodes: [] },
agentConfigurations: { nodes: configurations },
},
},
};
const findModal = () => wrapper.findComponent(ModalStub); const findModal = () => wrapper.findComponent(ModalStub);
const findAgentDropdown = () => findModal().findComponent(AvailableAgentsDropdown); const findAgentDropdown = () => findModal().findComponent(AvailableAgentsDropdown);
const findAlert = () => findModal().findComponent(GlAlert); const findAlert = () => findModal().findComponent(GlAlert);
...@@ -40,6 +54,8 @@ describe('InstallAgentModal', () => { ...@@ -40,6 +54,8 @@ describe('InstallAgentModal', () => {
.wrappers.find((button) => button.props('variant') === variant); .wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm'); const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => findButtonByVariant('default'); const findCancelButton = () => findButtonByVariant('default');
const findSecondaryButton = () => wrapper.findByTestId('agent-secondary-button');
const findImage = () => wrapper.findByRole('img', { alt: I18N_AGENT_MODAL.install.altText });
const expectDisabledAttribute = (element, disabled) => { const expectDisabledAttribute = (element, disabled) => {
if (disabled) { if (disabled) {
...@@ -52,7 +68,9 @@ describe('InstallAgentModal', () => { ...@@ -52,7 +68,9 @@ describe('InstallAgentModal', () => {
const createWrapper = () => { const createWrapper = () => {
const provide = { const provide = {
projectPath, projectPath,
kasAddress: 'kas.example.com', kasAddress,
kasEnabled,
emptyStateImage,
}; };
const propsData = { const propsData = {
...@@ -60,7 +78,7 @@ describe('InstallAgentModal', () => { ...@@ -60,7 +78,7 @@ describe('InstallAgentModal', () => {
maxAgents, maxAgents,
}; };
wrapper = shallowMount(InstallAgentModal, { wrapper = shallowMountExtended(InstallAgentModal, {
attachTo: document.body, attachTo: document.body,
stubs: { stubs: {
GlModal: ModalStub, GlModal: ModalStub,
...@@ -85,10 +103,12 @@ describe('InstallAgentModal', () => { ...@@ -85,10 +103,12 @@ describe('InstallAgentModal', () => {
}); });
}; };
const mockSelectedAgentResponse = () => { const mockSelectedAgentResponse = async () => {
createWrapper(); createWrapper();
writeQuery(); writeQuery();
await wrapper.vm.$nextTick();
wrapper.vm.setAgentName('agent-name'); wrapper.vm.setAgentName('agent-name');
findActionButton().vm.$emit('click'); findActionButton().vm.$emit('click');
...@@ -96,121 +116,160 @@ describe('InstallAgentModal', () => { ...@@ -96,121 +116,160 @@ describe('InstallAgentModal', () => {
}; };
beforeEach(() => { beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
]);
createWrapper(); createWrapper();
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
apolloProvider = null; apolloProvider = null;
}); });
describe('initial state', () => { describe('when agent configurations are present', () => {
it('renders the dropdown for available agents', () => { const i18n = I18N_AGENT_MODAL.register;
expect(findAgentDropdown().isVisible()).toBe(true);
expect(findModal().text()).not.toContain(i18n.basicInstallTitle);
expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false);
expect(findModal().findComponent(GlAlert).exists()).toBe(false);
expect(findModal().findComponent(CodeBlock).exists()).toBe(false);
});
it('renders a cancel button', () => { describe('initial state', () => {
expect(findCancelButton().isVisible()).toBe(true); it('renders the dropdown for available agents', () => {
expectDisabledAttribute(findCancelButton(), false); expect(findAgentDropdown().isVisible()).toBe(true);
}); expect(findModal().text()).not.toContain(i18n.basicInstallTitle);
expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false);
expect(findModal().findComponent(GlAlert).exists()).toBe(false);
expect(findModal().findComponent(CodeBlock).exists()).toBe(false);
});
it('renders a disabled next button', () => { it('renders a cancel button', () => {
expect(findActionButton().isVisible()).toBe(true); expect(findCancelButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.registerAgentButton); expectDisabledAttribute(findCancelButton(), false);
expectDisabledAttribute(findActionButton(), true); });
});
});
describe('an agent is selected', () => { it('renders a disabled next button', () => {
beforeEach(() => { expect(findActionButton().isVisible()).toBe(true);
findAgentDropdown().vm.$emit('agentSelected'); expect(findActionButton().text()).toBe(i18n.registerAgentButton);
expectDisabledAttribute(findActionButton(), true);
});
}); });
it('enables the next button', () => { describe('an agent is selected', () => {
expect(findActionButton().isVisible()).toBe(true); beforeEach(() => {
expectDisabledAttribute(findActionButton(), false); findAgentDropdown().vm.$emit('agentSelected');
}); });
});
describe('registering an agent', () => { it('enables the next button', () => {
const createAgentHandler = jest.fn().mockResolvedValue(createAgentResponse); expect(findActionButton().isVisible()).toBe(true);
const createAgentTokenHandler = jest.fn().mockResolvedValue(createAgentTokenResponse); expectDisabledAttribute(findActionButton(), false);
});
});
beforeEach(() => { describe('registering an agent', () => {
apolloProvider = createMockApollo([ const createAgentHandler = jest.fn().mockResolvedValue(createAgentResponse);
[createAgentMutation, createAgentHandler], const createAgentTokenHandler = jest.fn().mockResolvedValue(createAgentTokenResponse);
[createAgentTokenMutation, createAgentTokenHandler],
]);
return mockSelectedAgentResponse(apolloProvider); beforeEach(() => {
}); apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
[createAgentMutation, createAgentHandler],
[createAgentTokenMutation, createAgentTokenHandler],
]);
it('creates an agent and token', () => { return mockSelectedAgentResponse();
expect(createAgentHandler).toHaveBeenCalledWith({
input: { name: 'agent-name', projectPath },
}); });
expect(createAgentTokenHandler).toHaveBeenCalledWith({ it('creates an agent and token', () => {
input: { clusterAgentId: 'agent-id', name: 'agent-name' }, expect(createAgentHandler).toHaveBeenCalledWith({
}); input: { name: 'agent-name', projectPath },
}); });
it('renders a close button', () => { expect(createAgentTokenHandler).toHaveBeenCalledWith({
expect(findActionButton().isVisible()).toBe(true); input: { clusterAgentId: 'agent-id', name: 'agent-name' },
expect(findActionButton().text()).toBe(i18n.close); });
expectDisabledAttribute(findActionButton(), false); });
});
it('shows agent instructions', () => { it('renders a close button', () => {
const modalText = findModal().text(); expect(findActionButton().isVisible()).toBe(true);
expect(modalText).toContain(i18n.basicInstallTitle); expect(findActionButton().text()).toBe(i18n.close);
expect(modalText).toContain(i18n.basicInstallBody); expectDisabledAttribute(findActionButton(), false);
});
const token = findModal().findComponent(GlFormInputGroup); it('shows agent instructions', () => {
expect(token.props('value')).toBe('mock-agent-token'); const modalText = findModal().text();
expect(modalText).toContain(i18n.basicInstallTitle);
expect(modalText).toContain(i18n.basicInstallBody);
const alert = findModal().findComponent(GlAlert); const token = findModal().findComponent(GlFormInputGroup);
expect(alert.props('title')).toBe(i18n.tokenSingleUseWarningTitle); expect(token.props('value')).toBe('mock-agent-token');
const code = findModal().findComponent(CodeBlock).props('code'); const alert = findModal().findComponent(GlAlert);
expect(code).toContain('--agent-token=mock-agent-token'); expect(alert.props('title')).toBe(i18n.tokenSingleUseWarningTitle);
expect(code).toContain('--kas-address=kas.example.com');
});
describe('error creating agent', () => { const code = findModal().findComponent(CodeBlock).props('code');
beforeEach(() => { expect(code).toContain('--agent-token=mock-agent-token');
apolloProvider = createMockApollo([ expect(code).toContain('--kas-address=kas.example.com');
[createAgentMutation, jest.fn().mockResolvedValue(createAgentErrorResponse)], });
]);
return mockSelectedAgentResponse(); describe('error creating agent', () => {
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
[createAgentMutation, jest.fn().mockResolvedValue(createAgentErrorResponse)],
]);
return mockSelectedAgentResponse();
});
it('displays the error message', () => {
expect(findAlert().text()).toBe(
createAgentErrorResponse.data.createClusterAgent.errors[0],
);
});
}); });
it('displays the error message', () => { describe('error creating token', () => {
expect(findAlert().text()).toBe(createAgentErrorResponse.data.createClusterAgent.errors[0]); beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
[createAgentMutation, jest.fn().mockResolvedValue(createAgentResponse)],
[createAgentTokenMutation, jest.fn().mockResolvedValue(createAgentTokenErrorResponse)],
]);
return mockSelectedAgentResponse();
});
it('displays the error message', async () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
}); });
}); });
});
describe('error creating token', () => { describe('when there are no agent configurations present', () => {
beforeEach(() => { const i18n = I18N_AGENT_MODAL.install;
apolloProvider = createMockApollo([ const apolloQueryEmptyResponse = {
[createAgentMutation, jest.fn().mockResolvedValue(createAgentResponse)], data: {
[createAgentTokenMutation, jest.fn().mockResolvedValue(createAgentTokenErrorResponse)], project: {
]); clusterAgents: { nodes: [] },
agentConfigurations: { nodes: [] },
},
},
};
return mockSelectedAgentResponse(); beforeEach(() => {
}); apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryEmptyResponse)],
]);
createWrapper();
});
it('displays the error message', () => { it('renders empty state image', () => {
expect(findAlert().text()).toBe( expect(findImage().attributes('src')).toBe(emptyStateImage);
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0], });
);
}); it('renders a secondary button', () => {
expect(findSecondaryButton().isVisible()).toBe(true);
expect(findSecondaryButton().text()).toBe(i18n.secondaryButton);
}); });
}); });
}); });
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