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>
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 { INSTALL_AGENT_MODAL_ID, I18N_AGENTS_EMPTY_STATE } from '../constants';
......@@ -8,46 +8,33 @@ export default {
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',
}),
components: {
GlButton,
GlEmptyState,
GlLink,
GlSprintf,
GlAlert,
},
directives: {
GlModalDirective,
},
inject: ['emptyStateImage', 'projectPath'],
inject: ['emptyStateImage'],
props: {
hasConfigurations: {
type: Boolean,
required: true,
},
isChildComponent: {
default: false,
required: false,
type: Boolean,
},
},
computed: {
repositoryPath() {
return `/${this.projectPath}`;
},
},
};
</script>
<template>
<gl-empty-state :svg-path="emptyStateImage" title="" class="agents-empty-state">
<template #description>
<p class="mw-460 gl-mx-auto gl-text-left">
<p class="gl-text-left">
{{ $options.i18n.introText }}
</p>
<p class="mw-460 gl-mx-auto gl-text-left">
<p class="gl-text-left">
<gl-sprintf :message="$options.i18n.multipleClustersText">
<template #link="{ content }">
<gl-link
......@@ -61,42 +48,17 @@ export default {
</gl-sprintf>
</p>
<p class="mw-460 gl-mx-auto">
<p>
<gl-link :href="$options.installDocsUrl" target="_blank" data-testid="install-docs-link">
{{ $options.i18n.learnMoreText }}
</gl-link>
</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 #actions>
<gl-button
v-if="!isChildComponent"
v-gl-modal-directive="$options.modalId"
:disabled="!hasConfigurations"
data-testid="integration-primary-button"
category="primary"
variant="confirm"
......
......@@ -86,9 +86,6 @@ export default {
treePageInfo() {
return this.agents?.project?.repository?.tree?.trees?.pageInfo || {};
},
hasConfigurations() {
return Boolean(this.agents?.project?.repository?.tree?.trees?.nodes?.length);
},
},
methods: {
reloadAgents() {
......@@ -161,11 +158,7 @@ export default {
</div>
</div>
<agent-empty-state
v-else
:has-configurations="hasConfigurations"
:is-child-component="isChildComponent"
/>
<agent-empty-state v-else :is-child-component="isChildComponent" />
</section>
<gl-alert v-else variant="danger" :dismissible="false">
......
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants';
import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql';
export default {
name: 'AvailableAgentsDropdown',
......@@ -10,36 +9,22 @@ export default {
GlDropdown,
GlDropdownItem,
},
inject: ['projectPath'],
props: {
isRegistering: {
required: true,
type: Boolean,
},
},
apollo: {
agents: {
query: agentConfigurations,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
this.populateAvailableAgents(data);
},
availableAgents: {
required: true,
type: Array,
},
},
data() {
return {
availableAgents: [],
selectedAgent: null,
};
},
computed: {
isLoading() {
return this.$apollo.queries.agents.loading;
},
dropdownText() {
if (this.isRegistering) {
return this.$options.i18n.registeringAgent;
......@@ -58,18 +43,11 @@ export default {
isSelected(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>
<template>
<gl-dropdown :text="dropdownText" :loading="isLoading || isRegistering">
<gl-dropdown :text="dropdownText" :loading="isRegistering">
<gl-dropdown-item
v-for="agent in availableAgents"
:key="agent"
......
......@@ -12,16 +12,16 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { generateAgentRegistrationCommand } from '../clusters_util';
import { INSTALL_AGENT_MODAL_ID, I18N_INSTALL_AGENT_MODAL } from '../constants';
import { addAgentToStore } from '../graphql/cache_update';
import { INSTALL_AGENT_MODAL_ID, I18N_AGENT_MODAL, KAS_DISABLED_ERROR } from '../constants';
import { addAgentToStore, addAgentConfigToStore } from '../graphql/cache_update';
import createAgent from '../graphql/mutations/create_agent.mutation.graphql';
import createAgentToken from '../graphql/mutations/create_agent_token.mutation.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';
export default {
modalId: INSTALL_AGENT_MODAL_ID,
i18n: I18N_INSTALL_AGENT_MODAL,
components: {
AvailableAgentsDropdown,
ClipboardButton,
......@@ -34,7 +34,7 @@ export default {
GlModal,
GlSprintf,
},
inject: ['projectPath', 'kasAddress'],
inject: ['projectPath', 'kasAddress', 'emptyStateImage'],
props: {
defaultBranchName: {
default: '.noBranch',
......@@ -46,6 +46,22 @@ export default {
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() {
return {
registering: false,
......@@ -53,6 +69,8 @@ export default {
agentToken: null,
error: null,
clusterAgent: null,
availableAgents: [],
kasDisabled: false,
};
},
computed: {
......@@ -63,7 +81,7 @@ export default {
return !this.registering && this.agentName !== null;
},
canCancel() {
return !this.registered && !this.registering;
return !this.registered && !this.registering && this.isRegisterModal;
},
agentRegistrationCommand() {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
......@@ -76,6 +94,9 @@ export default {
advancedInstallPath() {
return helpPagePath('user/clusters/agent/install/index', { anchor: 'advanced-installation' });
},
enableKasPath() {
return helpPagePath('administration/clusters/kas');
},
getAgentsQueryVariables() {
return {
defaultBranchName: this.defaultBranchName,
......@@ -84,6 +105,29 @@ export default {
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: {
setAgentName(name) {
......@@ -96,8 +140,16 @@ export default {
this.registering = false;
this.agentName = null;
this.agentToken = null;
this.clusterAgent = 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() {
return this.$apollo
.mutate({
......@@ -117,7 +169,9 @@ export default {
);
},
})
.then(({ data: { createClusterAgent } }) => createClusterAgent);
.then(({ data: { createClusterAgent } }) => {
return createClusterAgent;
});
},
createAgentTokenMutation(agendId) {
return this.$apollo
......@@ -129,6 +183,17 @@ export default {
name: this.agentName,
},
},
update: (store, { data: { clusterAgentTokenCreate } }) => {
addAgentConfigToStore(
store,
clusterAgentTokenCreate,
this.clusterAgent,
agentConfigurations,
{
projectPath: this.projectPath,
},
);
},
})
.then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate);
},
......@@ -158,7 +223,7 @@ export default {
if (error) {
this.error = error.message;
} else {
this.error = this.$options.i18n.unknownError;
this.error = this.i18n.unknownError;
}
} finally {
this.registering = false;
......@@ -172,115 +237,142 @@ export default {
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.modalTitle"
:title="i18n.modalTitle"
:size="modalSize"
static
lazy
@hidden="resetModal"
>
<template v-if="!registered">
<p>
<strong>{{ $options.i18n.selectAgentTitle }}</strong>
</p>
<template v-if="isRegisterModal">
<template v-if="!registered">
<p>
<strong>{{ i18n.selectAgentTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.selectAgentBody">
<template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p class="gl-mb-0">{{ i18n.selectAgentBody }}</p>
<p>
<gl-link :href="basicInstallPath" target="_blank"> {{ i18n.learnMoreLink }}</gl-link>
</p>
<form>
<gl-form-group label-for="agent-name">
<available-agents-dropdown
class="gl-w-70p"
:is-registering="registering"
@agentSelected="setAgentName"
/>
</gl-form-group>
</form>
<form>
<gl-form-group label-for="agent-name">
<available-agents-dropdown
class="gl-w-70p"
:is-registering="registering"
:available-agents="availableAgents"
@agentSelected="setAgentName"
/>
</gl-form-group>
</form>
<p v-if="error">
<gl-alert
:title="$options.i18n.registrationErrorTitle"
variant="danger"
:dismissible="false"
>
{{ error }}
</gl-alert>
</p>
</template>
<p v-if="error">
<gl-alert :title="i18n.registrationErrorTitle" variant="danger" :dismissible="false">
{{ error }}
</gl-alert>
</p>
</template>
<template v-else>
<p>
<strong>{{ $options.i18n.tokenTitle }}</strong>
</p>
<template v-else>
<p>
<strong>{{ i18n.tokenTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.tokenBody">
<template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf :message="i18n.tokenBody">
<template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p>
<gl-alert
:title="$options.i18n.tokenSingleUseWarningTitle"
variant="warning"
:dismissible="false"
>
{{ $options.i18n.tokenSingleUseWarningBody }}
</gl-alert>
</p>
<p>
<gl-alert :title="i18n.tokenSingleUseWarningTitle" variant="warning" :dismissible="false">
{{ i18n.tokenSingleUseWarningBody }}
</gl-alert>
</p>
<p>
<gl-form-input-group readonly :value="agentToken" :select-on-click="true">
<template #append>
<clipboard-button :text="agentToken" :title="$options.i18n.copyToken" />
</template>
</gl-form-input-group>
</p>
<p>
<gl-form-input-group readonly :value="agentToken" :select-on-click="true">
<template #append>
<clipboard-button :text="agentToken" :title="i18n.copyToken" />
</template>
</gl-form-input-group>
</p>
<p>
<strong>{{ $options.i18n.basicInstallTitle }}</strong>
</p>
<p>
<strong>{{ i18n.basicInstallTitle }}</strong>
</p>
<p>
{{ $options.i18n.basicInstallBody }}
</p>
<p>
{{ i18n.basicInstallBody }}
</p>
<p>
<code-block :code="agentRegistrationCommand" />
</p>
<p>
<code-block :code="agentRegistrationCommand" />
</p>
<p>
<strong>{{ $options.i18n.advancedInstallTitle }}</strong>
</p>
<p>
<strong>{{ i18n.advancedInstallTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.advancedInstallBody">
<p>
<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 }">
<gl-link :href="advancedInstallPath" target="_blank"> {{ content }}</gl-link>
<gl-link :href="enableKasPath"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p class="gl-mb-0">
<gl-link :href="installAgentPath">
{{ i18n.docsLinkText }}
</gl-link>
</p>
</template>
<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"
>{{ $options.i18n.close }}
>{{ i18n.close }}
</gl-button>
<gl-button
v-else
v-else-if="isRegisterModal"
:disabled="!nextButtonDisabled"
variant="confirm"
category="primary"
@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>
</template>
</gl-modal>
......
......@@ -64,45 +64,63 @@ export const STATUSES = {
creating: { title: __('Creating') },
};
export const I18N_INSTALL_AGENT_MODAL = {
registerAgentButton: s__('ClusterAgents|Register Agent'),
close: __('Close'),
cancel: __('Cancel'),
export const I18N_AGENT_MODAL = {
register: {
registerAgentButton: s__('ClusterAgents|Register Agent'),
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'),
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}.`,
),
selectAgentTitle: s__('ClusterAgents|Select which Agent you want to install'),
selectAgentBody: s__(
'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'),
tokenTitle: s__('ClusterAgents|Registration token'),
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}.`,
),
copyToken: s__('ClusterAgents|Copy token'),
tokenTitle: s__('ClusterAgents|Registration token'),
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}.`,
),
tokenSingleUseWarningTitle: s__(
'ClusterAgents|The token value will not be shown again after you close this window.',
),
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.`,
),
tokenSingleUseWarningTitle: s__(
'ClusterAgents|The token value will not be shown again after you close this window.',
),
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.`,
),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
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.`,
),
basicInstallTitle: s__('ClusterAgents|Recommended installation method'),
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.`,
),
advancedInstallTitle: s__('ClusterAgents|Alternative installation methods'),
advancedInstallBody: s__(
'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
),
advancedInstallTitle: s__('ClusterAgents|Alternative installation methods'),
advancedInstallBody: s__(
'ClusterAgents|For alternative installation methods %{linkStart}go to the documentation%{linkEnd}.',
),
registrationErrorTitle: __('Failed to register Agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
registrationErrorTitle: __('Failed to register Agent'),
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 = {
selectAgent: s__('ClusterAgents|Select an Agent'),
registeringAgent: s__('ClusterAgents|Registering Agent'),
......@@ -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}',
),
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'),
};
......
import produce from 'immer';
import { getAgentConfigPath } from '../clusters_util';
export const hasErrors = ({ errors = [] }) => errors?.length;
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.clusterAgents.count += 1;
draftData.project.repository.tree.trees.nodes.push(configuration);
});
store.writeQuery({
query,
variables,
data,
});
if (!hasErrors(createClusterAgent)) {
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.clusterAgents.count += 1;
draftData.project.repository.tree.trees.nodes.push(configuration);
});
store.writeQuery({
query,
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 @@
}
}
.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 {
@include media-breakpoint-up(sm) {
@include gl-pt-2;
......
......@@ -14,7 +14,7 @@ module Resolvers
return [] unless can_read_agent_configuration?
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
end
......
......@@ -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}"
msgstr ""
msgid "ClusterAgents|GitLab Kubernetes Agent"
msgstr ""
msgid "ClusterAgents|Go to the repository"
msgstr ""
......@@ -7444,6 +7447,12 @@ msgstr ""
msgid "ClusterAgents|Learn how to troubleshoot"
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."
msgstr ""
......@@ -7468,9 +7477,6 @@ msgstr ""
msgid "ClusterAgents|Not connected"
msgstr ""
msgid "ClusterAgents|Read more about getting started"
msgstr ""
msgid "ClusterAgents|Recommended"
msgstr ""
......@@ -7492,7 +7498,7 @@ msgstr ""
msgid "ClusterAgents|Select an Agent"
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 ""
msgid "ClusterAgents|Select which Agent you want to install"
......@@ -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}."
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."
msgstr ""
......
......@@ -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
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('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 { INSTALL_AGENT_MODAL_ID } from '~/clusters_list/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { helpPagePath } from '~/helpers/help_page_helper';
const emptyStateImage = '/path/to/image';
const projectPath = 'path/to/project';
const multipleClustersDocsUrl = helpPagePath('user/project/clusters/multiple_kubernetes_clusters');
const installDocsUrl = helpPagePath('administration/clusters/kas');
describe('AgentEmptyStateComponent', () => {
let wrapper;
const propsData = {
hasConfigurations: false,
};
const provideData = {
emptyStateImage,
projectPath,
};
const findConfigurationsAlert = () => wrapper.findComponent(GlAlert);
const findMultipleClustersDocsLink = () => wrapper.findByTestId('multiple-clusters-docs-link');
const findInstallDocsLink = () => wrapper.findByTestId('install-docs-link');
const findIntegrationButton = () => wrapper.findByTestId('integration-primary-button');
......@@ -27,8 +22,10 @@ describe('AgentEmptyStateComponent', () => {
beforeEach(() => {
wrapper = shallowMountExtended(AgentEmptyState, {
propsData,
provide: provideData,
directives: {
GlModalDirective: createMockDirective(),
},
stubs: { GlEmptyState, GlSprintf },
});
});
......@@ -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', () => {
expect(findMultipleClustersDocsLink().attributes('href')).toBe(multipleClustersDocsUrl);
expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl);
});
describe('when there are no agent configurations in repository', () => {
it('should render notification message box', () => {
expect(findConfigurationsAlert().exists()).toBe(true);
});
it('should disable integration button', () => {
expect(findIntegrationButton().attributes('disabled')).toBe('true');
});
});
it('renders correct modal id for the agent registration modal', () => {
const binding = getBinding(findIntegrationButton().element, 'gl-modal-directive');
describe('when there is a list of agent configurations', () => {
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();
});
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
});
......@@ -19,7 +19,6 @@ describe('Agents', () => {
};
const provideData = {
projectPath: 'path/to/project',
kasAddress: 'kas.example.com',
};
const createWrapper = ({ props = {}, agents = [], pageInfo = null, trees = [], count = 0 }) => {
......@@ -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', () => {
beforeEach(() => {
return createWrapper({ agents: null });
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
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', () => {
let wrapper;
......@@ -18,46 +11,19 @@ describe('AvailableAgentsDropdown', () => {
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findConfiguredAgentItem = () => findDropdownItems().at(0);
const createWrapper = ({ propsData = {}, isLoading = false }) => {
const provide = {
projectPath: 'path/to/project',
};
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,
});
})();
const createWrapper = ({ propsData }) => {
wrapper = shallowMount(AvailableAgentsDropdown, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('there are agents available', () => {
const propsData = {
availableAgents: ['configured-agent'],
isRegistering: false,
};
......@@ -69,12 +35,6 @@ describe('AvailableAgentsDropdown', () => {
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', () => {
beforeEach(() => {
findConfiguredAgentItem().vm.$emit('click');
......@@ -93,6 +53,7 @@ describe('AvailableAgentsDropdown', () => {
describe('registration in progress', () => {
const propsData = {
availableAgents: ['configured-agent'],
isRegistering: true,
};
......@@ -108,22 +69,4 @@ describe('AvailableAgentsDropdown', () => {
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 { createLocalVue, shallowMount } from '@vue/test-utils';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.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 getAgentConfigurations from '~/clusters_list/graphql/queries/agent_configurations.query.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 createMockApollo from 'helpers/mock_apollo_helper';
......@@ -23,6 +25,9 @@ const localVue = createLocalVue();
localVue.use(VueApollo);
const projectPath = 'path/to/project';
const kasAddress = 'kas.example.com';
const kasEnabled = true;
const emptyStateImage = 'path/to/image';
const defaultBranchName = 'default';
const maxAgents = MAX_LIST_COUNT;
......@@ -30,7 +35,16 @@ describe('InstallAgentModal', () => {
let wrapper;
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 findAgentDropdown = () => findModal().findComponent(AvailableAgentsDropdown);
const findAlert = () => findModal().findComponent(GlAlert);
......@@ -40,6 +54,8 @@ describe('InstallAgentModal', () => {
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
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) => {
if (disabled) {
......@@ -52,7 +68,9 @@ describe('InstallAgentModal', () => {
const createWrapper = () => {
const provide = {
projectPath,
kasAddress: 'kas.example.com',
kasAddress,
kasEnabled,
emptyStateImage,
};
const propsData = {
......@@ -60,7 +78,7 @@ describe('InstallAgentModal', () => {
maxAgents,
};
wrapper = shallowMount(InstallAgentModal, {
wrapper = shallowMountExtended(InstallAgentModal, {
attachTo: document.body,
stubs: {
GlModal: ModalStub,
......@@ -85,10 +103,12 @@ describe('InstallAgentModal', () => {
});
};
const mockSelectedAgentResponse = () => {
const mockSelectedAgentResponse = async () => {
createWrapper();
writeQuery();
await wrapper.vm.$nextTick();
wrapper.vm.setAgentName('agent-name');
findActionButton().vm.$emit('click');
......@@ -96,121 +116,160 @@ describe('InstallAgentModal', () => {
};
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
]);
createWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
apolloProvider = null;
});
describe('initial state', () => {
it('renders the dropdown for available agents', () => {
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);
});
describe('when agent configurations are present', () => {
const i18n = I18N_AGENT_MODAL.register;
it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true);
expectDisabledAttribute(findCancelButton(), false);
});
describe('initial state', () => {
it('renders the dropdown for available agents', () => {
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', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.registerAgentButton);
expectDisabledAttribute(findActionButton(), true);
});
});
it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true);
expectDisabledAttribute(findCancelButton(), false);
});
describe('an agent is selected', () => {
beforeEach(() => {
findAgentDropdown().vm.$emit('agentSelected');
it('renders a disabled next button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.registerAgentButton);
expectDisabledAttribute(findActionButton(), true);
});
});
it('enables the next button', () => {
expect(findActionButton().isVisible()).toBe(true);
expectDisabledAttribute(findActionButton(), false);
});
});
describe('an agent is selected', () => {
beforeEach(() => {
findAgentDropdown().vm.$emit('agentSelected');
});
describe('registering an agent', () => {
const createAgentHandler = jest.fn().mockResolvedValue(createAgentResponse);
const createAgentTokenHandler = jest.fn().mockResolvedValue(createAgentTokenResponse);
it('enables the next button', () => {
expect(findActionButton().isVisible()).toBe(true);
expectDisabledAttribute(findActionButton(), false);
});
});
beforeEach(() => {
apolloProvider = createMockApollo([
[createAgentMutation, createAgentHandler],
[createAgentTokenMutation, createAgentTokenHandler],
]);
describe('registering an agent', () => {
const createAgentHandler = jest.fn().mockResolvedValue(createAgentResponse);
const createAgentTokenHandler = jest.fn().mockResolvedValue(createAgentTokenResponse);
return mockSelectedAgentResponse(apolloProvider);
});
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
[createAgentMutation, createAgentHandler],
[createAgentTokenMutation, createAgentTokenHandler],
]);
it('creates an agent and token', () => {
expect(createAgentHandler).toHaveBeenCalledWith({
input: { name: 'agent-name', projectPath },
return mockSelectedAgentResponse();
});
expect(createAgentTokenHandler).toHaveBeenCalledWith({
input: { clusterAgentId: 'agent-id', name: 'agent-name' },
});
});
it('creates an agent and token', () => {
expect(createAgentHandler).toHaveBeenCalledWith({
input: { name: 'agent-name', projectPath },
});
it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.close);
expectDisabledAttribute(findActionButton(), false);
});
expect(createAgentTokenHandler).toHaveBeenCalledWith({
input: { clusterAgentId: 'agent-id', name: 'agent-name' },
});
});
it('shows agent instructions', () => {
const modalText = findModal().text();
expect(modalText).toContain(i18n.basicInstallTitle);
expect(modalText).toContain(i18n.basicInstallBody);
it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe(i18n.close);
expectDisabledAttribute(findActionButton(), false);
});
const token = findModal().findComponent(GlFormInputGroup);
expect(token.props('value')).toBe('mock-agent-token');
it('shows agent instructions', () => {
const modalText = findModal().text();
expect(modalText).toContain(i18n.basicInstallTitle);
expect(modalText).toContain(i18n.basicInstallBody);
const alert = findModal().findComponent(GlAlert);
expect(alert.props('title')).toBe(i18n.tokenSingleUseWarningTitle);
const token = findModal().findComponent(GlFormInputGroup);
expect(token.props('value')).toBe('mock-agent-token');
const code = findModal().findComponent(CodeBlock).props('code');
expect(code).toContain('--agent-token=mock-agent-token');
expect(code).toContain('--kas-address=kas.example.com');
});
const alert = findModal().findComponent(GlAlert);
expect(alert.props('title')).toBe(i18n.tokenSingleUseWarningTitle);
describe('error creating agent', () => {
beforeEach(() => {
apolloProvider = createMockApollo([
[createAgentMutation, jest.fn().mockResolvedValue(createAgentErrorResponse)],
]);
const code = findModal().findComponent(CodeBlock).props('code');
expect(code).toContain('--agent-token=mock-agent-token');
expect(code).toContain('--kas-address=kas.example.com');
});
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', () => {
expect(findAlert().text()).toBe(createAgentErrorResponse.data.createClusterAgent.errors[0]);
describe('error creating token', () => {
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', () => {
beforeEach(() => {
apolloProvider = createMockApollo([
[createAgentMutation, jest.fn().mockResolvedValue(createAgentResponse)],
[createAgentTokenMutation, jest.fn().mockResolvedValue(createAgentTokenErrorResponse)],
]);
describe('when there are no agent configurations present', () => {
const i18n = I18N_AGENT_MODAL.install;
const apolloQueryEmptyResponse = {
data: {
project: {
clusterAgents: { nodes: [] },
agentConfigurations: { nodes: [] },
},
},
};
return mockSelectedAgentResponse();
});
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryEmptyResponse)],
]);
createWrapper();
});
it('displays the error message', () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
it('renders empty state image', () => {
expect(findImage().attributes('src')).toBe(emptyStateImage);
});
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