Commit 4f3f89bf authored by Anna Vovchenko's avatar Anna Vovchenko Committed by Phil Hughes

Develop two variants of the Install new Agent modal

parent 556266e4
<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,22 +237,21 @@ 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="isRegisterModal">
<template v-if="!registered">
<p>
<strong>{{ $options.i18n.selectAgentTitle }}</strong>
<strong>{{ i18n.selectAgentTitle }}</strong>
</p>
<p class="gl-mb-0">{{ i18n.selectAgentBody }}</p>
<p>
<gl-sprintf :message="$options.i18n.selectAgentBody">
<template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
<gl-link :href="basicInstallPath" target="_blank"> {{ i18n.learnMoreLink }}</gl-link>
</p>
<form>
......@@ -195,17 +259,14 @@ export default {
<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"
>
<gl-alert :title="i18n.registrationErrorTitle" variant="danger" :dismissible="false">
{{ error }}
</gl-alert>
</p>
......@@ -213,11 +274,11 @@ export default {
<template v-else>
<p>
<strong>{{ $options.i18n.tokenTitle }}</strong>
<strong>{{ i18n.tokenTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.tokenBody">
<gl-sprintf :message="i18n.tokenBody">
<template #link="{ content }">
<gl-link :href="basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
......@@ -225,29 +286,25 @@ export default {
</p>
<p>
<gl-alert
:title="$options.i18n.tokenSingleUseWarningTitle"
variant="warning"
:dismissible="false"
>
{{ $options.i18n.tokenSingleUseWarningBody }}
<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" />
<clipboard-button :text="agentToken" :title="i18n.copyToken" />
</template>
</gl-form-input-group>
</p>
<p>
<strong>{{ $options.i18n.basicInstallTitle }}</strong>
<strong>{{ i18n.basicInstallTitle }}</strong>
</p>
<p>
{{ $options.i18n.basicInstallBody }}
{{ i18n.basicInstallBody }}
</p>
<p>
......@@ -255,32 +312,67 @@ export default {
</p>
<p>
<strong>{{ $options.i18n.advancedInstallTitle }}</strong>
<strong>{{ i18n.advancedInstallTitle }}</strong>
</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 }">
<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,17 +64,19 @@ export const STATUSES = {
creating: { title: __('Creating') },
};
export const I18N_INSTALL_AGENT_MODAL = {
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}.`,
'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'),
......@@ -101,8 +103,24 @@ export const I18N_INSTALL_AGENT_MODAL = {
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) {
if (!hasErrors(createClusterAgent)) {
const { clusterAgent } = createClusterAgent;
const sourceData = store.readQuery({
query,
......@@ -26,4 +29,36 @@ export function addAgentToStore(store, createClusterAgent, query, variables) {
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
......
......@@ -7426,6 +7426,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 ""
......@@ -7447,6 +7450,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 ""
......@@ -7471,9 +7480,6 @@ msgstr ""
msgid "ClusterAgents|Not connected"
msgstr ""
msgid "ClusterAgents|Read more about getting started"
msgstr ""
msgid "ClusterAgents|Recommended"
msgstr ""
......@@ -7495,7 +7501,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"
......@@ -7504,6 +7510,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 correct href attributes for the links', () => {
expect(findMultipleClustersDocsLink().attributes('href')).toBe(multipleClustersDocsUrl);
expect(findInstallDocsLink().attributes('href')).toBe(installDocsUrl);
it('renders the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
describe('when there are no agent configurations in repository', () => {
it('should render notification message box', () => {
expect(findConfigurationsAlert().exists()).toBe(true);
it('renders button for the agent registration', () => {
expect(findIntegrationButton().exists()).toBe(true);
});
it('should disable integration button', () => {
expect(findIntegrationButton().attributes('disabled')).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 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();
});
it('renders correct modal id for the agent registration modal', () => {
const binding = getBinding(findIntegrationButton().element, 'gl-modal-directive');
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,
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,15 +116,20 @@ describe('InstallAgentModal', () => {
};
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
]);
createWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
apolloProvider = null;
});
describe('when agent configurations are present', () => {
const i18n = I18N_AGENT_MODAL.register;
describe('initial state', () => {
it('renders the dropdown for available agents', () => {
expect(findAgentDropdown().isVisible()).toBe(true);
......@@ -143,11 +168,12 @@ describe('InstallAgentModal', () => {
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
[createAgentMutation, createAgentHandler],
[createAgentTokenMutation, createAgentTokenHandler],
]);
return mockSelectedAgentResponse(apolloProvider);
return mockSelectedAgentResponse();
});
it('creates an agent and token', () => {
......@@ -185,6 +211,7 @@ describe('InstallAgentModal', () => {
describe('error creating agent', () => {
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryResponse)],
[createAgentMutation, jest.fn().mockResolvedValue(createAgentErrorResponse)],
]);
......@@ -192,13 +219,16 @@ describe('InstallAgentModal', () => {
});
it('displays the error message', () => {
expect(findAlert().text()).toBe(createAgentErrorResponse.data.createClusterAgent.errors[0]);
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)],
]);
......@@ -206,11 +236,40 @@ describe('InstallAgentModal', () => {
return mockSelectedAgentResponse();
});
it('displays the error message', () => {
it('displays the error message', async () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
});
});
});
describe('when there are no agent configurations present', () => {
const i18n = I18N_AGENT_MODAL.install;
const apolloQueryEmptyResponse = {
data: {
project: {
clusterAgents: { nodes: [] },
agentConfigurations: { nodes: [] },
},
},
};
beforeEach(() => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(apolloQueryEmptyResponse)],
]);
createWrapper();
});
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