Commit 388f87fa authored by Anna Vovchenko's avatar Anna Vovchenko Committed by Phil Hughes

Support agent registration without config

As we want to increase GitLab agent adoption,
we add a posibility to register agent without a config file.
The MR adds search input to the agents dropdown
and indicates the agents without config in the agent's table.

Changelog: added
parent d85eee06
<script>
import { GlLink, GlTable, GlIcon, GlSprintf, GlTooltip, GlPopover } from '@gitlab/ui';
import {
GlLink,
GlTable,
GlIcon,
GlSprintf,
GlTooltip,
GlTooltipDirective,
GlPopover,
} from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { helpPagePath } from '~/helpers/help_page_helper';
......@@ -19,12 +27,18 @@ export default {
TimeAgoTooltip,
DeleteAgentButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
AGENT_STATUSES,
troubleshootingLink: helpPagePath('user/clusters/agent/troubleshooting'),
versionUpdateLink: helpPagePath('user/clusters/agent/install/index', {
anchor: 'update-the-agent-version',
}),
configHelpLink: helpPagePath('user/clusters/agent/install/index', {
anchor: 'create-an-agent-without-configuration-file',
}),
inject: ['gitlabVersion'],
props: {
agents: {
......@@ -256,7 +270,16 @@ export default {
{{ getAgentConfigPath(item.name) }}
</gl-link>
<span v-else>{{ getAgentConfigPath(item.name) }}</span>
<span v-else
>{{ $options.i18n.defaultConfigText }}
<gl-link
v-gl-tooltip
:href="$options.configHelpLink"
:title="$options.i18n.defaultConfigTooltip"
:aria-label="$options.i18n.defaultConfigTooltip"
class="gl-vertical-align-middle"
><gl-icon name="question" :size="14" /></gl-link
></span>
</span>
</template>
......
......@@ -116,9 +116,6 @@ export default {
},
},
methods: {
reloadAgents() {
this.$apollo.queries.agents.refetch();
},
nextPage() {
this.cursor = {
first: MAX_LIST_COUNT,
......
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants';
export default {
......@@ -8,6 +14,9 @@ export default {
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlSearchBoxByType,
GlSprintf,
},
props: {
isRegistering: {
......@@ -22,6 +31,7 @@ export default {
data() {
return {
selectedAgent: null,
searchTerm: '',
};
},
computed: {
......@@ -34,22 +44,45 @@ export default {
return this.selectedAgent;
},
shouldRenderCreateButton() {
return this.searchTerm && !this.availableAgents.includes(this.searchTerm);
},
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.availableAgents.filter((resultString) =>
resultString.toLowerCase().includes(lowerCasedSearchTerm),
);
},
},
methods: {
selectAgent(agent) {
this.$emit('agentSelected', agent);
this.selectedAgent = agent;
this.clearSearch();
},
isSelected(agent) {
return this.selectedAgent === agent;
},
clearSearch() {
this.searchTerm = '';
},
focusSearch() {
this.$refs.searchInput.focusInput();
},
handleShow() {
this.clearSearch();
this.focusSearch();
},
},
};
</script>
<template>
<gl-dropdown :text="dropdownText" :loading="isRegistering">
<gl-dropdown :text="dropdownText" :loading="isRegistering" @shown="handleShow">
<template #header>
<gl-search-box-by-type ref="searchInput" v-model.trim="searchTerm" />
</template>
<gl-dropdown-item
v-for="agent in availableAgents"
v-for="agent in filteredResults"
:key="agent"
:is-checked="isSelected(agent)"
is-check-item
......@@ -57,5 +90,16 @@ export default {
>
{{ agent }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
$options.i18n.noResults
}}</gl-dropdown-item>
<template v-if="shouldRenderCreateButton">
<gl-dropdown-divider />
<gl-dropdown-item data-testid="create-config-button" @click="selectAgent(searchTerm)">
<gl-sprintf :message="$options.i18n.createButton">
<template #searchTerm>{{ searchTerm }}</template>
</gl-sprintf>
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
......@@ -35,6 +35,7 @@ const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL });
export default {
modalId: INSTALL_AGENT_MODAL_ID,
i18n: I18N_AGENT_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
EVENT_LABEL_MODAL,
......@@ -45,7 +46,6 @@ export default {
anchor: 'advanced-installation',
}),
enableKasPath: helpPagePath('administration/clusters/kas'),
installAgentPath: helpPagePath('user/clusters/agent/install/index'),
registerAgentPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'register-an-agent-with-gitlab',
}),
......@@ -109,10 +109,10 @@ export default {
return !this.registering && this.agentName !== null;
},
canCancel() {
return !this.registered && !this.registering && this.isAgentRegistrationModal;
return !this.registered && !this.registering && !this.kasDisabled;
},
canRegister() {
return !this.registered && this.isAgentRegistrationModal;
return !this.registered && !this.kasDisabled;
},
agentRegistrationCommand() {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
......@@ -125,32 +125,20 @@ export default {
projectPath: this.projectPath,
};
},
i18n() {
return I18N_AGENT_MODAL[this.modalType];
},
repositoryPath() {
return `/${this.projectPath}`;
},
modalType() {
return !this.availableAgents?.length && !this.registered
? MODAL_TYPE_EMPTY
: MODAL_TYPE_REGISTER;
return this.kasDisabled ? MODAL_TYPE_EMPTY : MODAL_TYPE_REGISTER;
},
modalSize() {
return this.isEmptyStateModal ? 'sm' : 'md';
},
isEmptyStateModal() {
return this.modalType === MODAL_TYPE_EMPTY;
},
isAgentRegistrationModal() {
return this.modalType === MODAL_TYPE_REGISTER;
},
isKasEnabledInEmptyStateModal() {
return this.isEmptyStateModal && !this.kasDisabled;
return this.kasDisabled ? 'sm' : 'md';
},
},
methods: {
setAgentName(name) {
this.error = null;
this.agentName = name;
this.track(EVENT_ACTIONS_SELECT);
},
......@@ -244,7 +232,7 @@ export default {
if (error) {
this.error = error.message;
} else {
this.error = this.i18n.unknownError;
this.error = this.$options.i18n.unknownError;
}
} finally {
this.registering = false;
......@@ -258,22 +246,21 @@ export default {
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="i18n.modalTitle"
:title="$options.i18n.modalTitle"
:size="modalSize"
static
lazy
@hidden="resetModal"
@show="track($options.EVENT_ACTIONS_OPEN, { property: modalType })"
>
<template v-if="isAgentRegistrationModal">
<template v-if="!kasDisabled">
<template v-if="!registered">
<p>
<strong>{{ i18n.selectAgentTitle }}</strong>
</p>
<p class="gl-mb-0">{{ i18n.selectAgentBody }}</p>
<p>
<gl-link :href="$options.registerAgentPath"> {{ i18n.learnMoreLink }}</gl-link>
<p class="gl-mb-0">
<gl-sprintf :message="$options.i18n.modalBody">
<template #link="{ content }">
<gl-link :href="repositoryPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<form>
......@@ -287,8 +274,16 @@ export default {
</gl-form-group>
</form>
<p>
<gl-link :href="$options.registerAgentPath"> {{ $options.i18n.learnMoreLink }}</gl-link>
</p>
<p v-if="error">
<gl-alert :title="i18n.registrationErrorTitle" variant="danger" :dismissible="false">
<gl-alert
:title="$options.i18n.registrationErrorTitle"
variant="danger"
:dismissible="false"
>
{{ error }}
</gl-alert>
</p>
......@@ -296,11 +291,11 @@ export default {
<template v-else>
<p>
<strong>{{ i18n.tokenTitle }}</strong>
<strong>{{ $options.i18n.tokenTitle }}</strong>
</p>
<p>
<gl-sprintf :message="i18n.tokenBody">
<gl-sprintf :message="$options.i18n.tokenBody">
<template #link="{ content }">
<gl-link :href="$options.basicInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
......@@ -308,8 +303,12 @@ export default {
</p>
<p>
<gl-alert :title="i18n.tokenSingleUseWarningTitle" variant="warning" :dismissible="false">
{{ i18n.tokenSingleUseWarningBody }}
<gl-alert
:title="$options.i18n.tokenSingleUseWarningTitle"
variant="warning"
:dismissible="false"
>
{{ $options.i18n.tokenSingleUseWarningBody }}
</gl-alert>
</p>
......@@ -318,7 +317,7 @@ export default {
<template #append>
<modal-copy-button
:text="agentToken"
:title="i18n.copyToken"
:title="$options.i18n.copyToken"
:modal-id="$options.modalId"
/>
</template>
......@@ -326,11 +325,11 @@ export default {
</p>
<p>
<strong>{{ i18n.basicInstallTitle }}</strong>
<strong>{{ $options.i18n.basicInstallTitle }}</strong>
</p>
<p>
{{ i18n.basicInstallBody }}
{{ $options.i18n.basicInstallBody }}
</p>
<p>
......@@ -338,11 +337,11 @@ export default {
</p>
<p>
<strong>{{ i18n.advancedInstallTitle }}</strong>
<strong>{{ $options.i18n.advancedInstallTitle }}</strong>
</p>
<p>
<gl-sprintf :message="i18n.advancedInstallBody">
<gl-sprintf :message="$options.i18n.advancedInstallBody">
<template #link="{ content }">
<gl-link :href="$options.advancedInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
......@@ -353,24 +352,16 @@ export default {
<template v-else>
<div class="gl-text-center gl-mb-5">
<img :alt="i18n.altText" :src="emptyStateImage" height="100" />
<img :alt="$options.i18n.altText" :src="emptyStateImage" height="100" />
</div>
<p v-if="kasDisabled">
<gl-sprintf :message="i18n.enableKasText">
<gl-sprintf :message="$options.i18n.enableKasText">
<template #link="{ content }">
<gl-link :href="$options.enableKasPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<p v-else>
<gl-sprintf :message="i18n.modalBody">
<template #link="{ content }">
<gl-link :href="$options.installAgentPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
<template #modal-footer>
......@@ -382,7 +373,7 @@ export default {
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
@click="closeModal"
>{{ i18n.close }}
>{{ $options.i18n.close }}
</gl-button>
<gl-button
......@@ -391,7 +382,7 @@ export default {
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="cancel"
@click="closeModal"
>{{ i18n.cancel }}
>{{ $options.i18n.cancel }}
</gl-button>
<gl-button
......@@ -403,25 +394,16 @@ export default {
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="register"
@click="registerAgent"
>{{ i18n.registerAgentButton }}
>{{ $options.i18n.registerAgentButton }}
</gl-button>
<gl-button
v-if="isEmptyStateModal"
v-if="kasDisabled"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="done"
@click="closeModal"
>{{ i18n.done }}
</gl-button>
<gl-button
v-if="isKasEnabledInEmptyStateModal"
:href="repositoryPath"
variant="confirm"
category="primary"
data-testid="agent-primary-button"
>{{ i18n.primaryButton }}
>{{ $options.i18n.close }}
</gl-button>
</template>
</gl-modal>
......
......@@ -83,66 +83,58 @@ export const I18N_AGENT_TABLE = {
),
versionMismatchOutdatedTitle: s__('ClusterAgents|Agent version mismatch and update'),
viewDocsText: s__('ClusterAgents|How to update an agent?'),
defaultConfigText: s__('ClusterAgents|Default configuration'),
defaultConfigTooltip: s__('ClusterAgents|What is default configuration?'),
};
export const I18N_AGENT_MODAL = {
agent_registration: {
registerAgentButton: s__('ClusterAgents|Register'),
close: __('Close'),
cancel: __('Cancel'),
registerAgentButton: s__('ClusterAgents|Register'),
close: __('Close'),
cancel: __('Cancel'),
modalTitle: s__('ClusterAgents|Connect a cluster through an agent'),
selectAgentTitle: s__('ClusterAgents|Select an agent to register with GitLab'),
selectAgentBody: s__(
'ClusterAgents|Register an agent to generate a token that will be used to install the agent on your cluster in the next step.',
),
learnMoreLink: s__('ClusterAgents|How to register an agent?'),
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. %{linkStart}What are registration tokens?%{linkEnd}`,
),
modalTitle: s__('ClusterAgents|Connect a cluster through an agent'),
modalBody: s__(
'ClusterAgents|Add an agent configuration file to %{linkStart}this repository%{linkEnd} and select it, or create a new one to register with GitLab:',
),
enableKasText: s__(
"ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it.",
),
altText: s__('ClusterAgents|GitLab Agent for Kubernetes'),
learnMoreLink: s__('ClusterAgents|How do I register an agent?'),
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. %{linkStart}What are registration tokens?%{linkEnd}`,
),
tokenSingleUseWarningTitle: s__(
'ClusterAgents|You cannot see this token again after you close this window.',
),
tokenSingleUseWarningBody: s__(
`ClusterAgents|The recommended installation method includes the token. If you want to follow the advanced installation method provided in the docs, make sure you save the token value before you close this window.`,
),
tokenSingleUseWarningTitle: s__(
'ClusterAgents|You cannot see this token again after you close this window.',
),
tokenSingleUseWarningBody: s__(
`ClusterAgents|The recommended installation method includes the token. If you want to follow the advanced installation method provided in the docs, make sure you save the token value before you close this 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|Advanced installation methods'),
advancedInstallBody: s__(
'ClusterAgents|For the advanced installation method %{linkStart}see the documentation%{linkEnd}.',
),
advancedInstallTitle: s__('ClusterAgents|Advanced installation methods'),
advancedInstallBody: s__(
'ClusterAgents|For the advanced installation method %{linkStart}see the documentation%{linkEnd}.',
),
registrationErrorTitle: s__('ClusterAgents|Failed to register an agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
},
empty_state: {
modalTitle: s__('ClusterAgents|Connect your cluster through an agent'),
modalBody: s__(
"ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}Learn more about installing GitLab Agent.%{linkEnd}",
),
enableKasText: s__(
"ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it.",
),
altText: s__('ClusterAgents|GitLab Agent for Kubernetes'),
primaryButton: s__('ClusterAgents|Go to the repository files'),
done: __('Cancel'),
},
registrationErrorTitle: s__('ClusterAgents|Failed to register an agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
};
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'),
selectAgent: s__('ClusterAgents|Select an agent or enter a name to create new'),
registeringAgent: s__('ClusterAgents|Registering agent'),
noResults: __('No matching results'),
createButton: s__('ClusterAgents|Create agent: %{searchTerm}'),
};
export const AGENT_STATUSES = {
......
import produce from 'immer';
import { getAgentConfigPath } from '../clusters_util';
export const hasErrors = ({ errors = [] }) => errors?.length;
......@@ -12,17 +11,8 @@ export function addAgentToStore(store, createClusterAgent, query, variables) {
});
const data = produce(sourceData, (draftData) => {
const configuration = {
id: clusterAgent.id,
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({
......
......@@ -7,9 +7,7 @@ query getAgents(
$first: Int
$last: Int
$afterAgent: String
$afterTree: String
$beforeAgent: String
$beforeTree: String
) {
project(fullPath: $projectPath) {
id
......@@ -27,17 +25,13 @@ query getAgents(
repository {
tree(path: ".gitlab/agents", ref: $defaultBranchName) {
trees(first: $first, last: $last, after: $afterTree, before: $beforeTree) {
trees {
nodes {
id
name
path
webPath
}
pageInfo {
...PageInfo
}
}
}
}
......
......@@ -26,18 +26,47 @@ Before you can install the agent in your cluster, you need:
To install the agent in your cluster:
1. [Create an agent configuration file called `config.yaml`](#create-an-agent-configuration-file).
1. [Register the agent with GitLab](#register-the-agent-with-gitlab).
1. [Install the agent in your cluster](#install-the-agent-in-the-cluster).
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a GitLab 14.2 [walk-through of this process](https://www.youtube.com/watch?v=XuBpKtsgGkE).
### Register the agent with GitLab
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5786) in GitLab 14.1, you can create a new agent record directly from the GitLab UI.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/347240) in GitLab 14.9, the agent can be registered without creating an agent configuration file.
You must register an agent with GitLab.
Prerequisites:
- For a [GitLab CI/CD workflow](../ci_cd_tunnel.md), ensure that
[GitLab CI/CD is enabled](../../../../ci/enable_or_disable_ci.md#enable-cicd-in-a-project).
To register an agent with GitLab:
1. On the top bar, select **Menu > Projects** and find your project.
1. From the left sidebar, select **Infrastructure > Kubernetes clusters**.
1. Select **Actions**.
1. From the **Select an agent** dropdown list:
- If you want to create a configuration with CI/CD defaults, type a name for the agent.
- If you already have an [agent configuration file](#create-an-agent-configuration-file), select it from the list.
1. Select **Register an agent**.
1. GitLab generates a registration token for this agent. Securely store this secret token. You need it to install the agent in your cluster and to [update the agent](#update-the-agent-version) to another version.
1. Copy the command under **Recommended installation method**. You need it when you use the one-liner installation method to install the agent in your cluster.
### Create an agent configuration file
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in GitLab 13.7, the agent configuration file can be added to multiple directories (or subdirectories) of the repository.
> - Group authorization was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/5784) in GitLab 14.3.
In a GitLab project, in the repository, create a file called `config.yaml` at this path:
You can use an agent configuration file to specify details about your implementation.
Creating a file is optional but is needed if:
- You use [a GitOps workflow](../gitops.md#gitops-configuration-reference) and you want a more advanced configuration.
- You use a GitLab CI/CD workflow. In that workflow, you must [authorize the agent](../ci_cd_tunnel.md#authorize-the-agent).
To create an agent configuration file, go to the GitLab project. In the repository, create a file called `config.yaml` at this path:
```plaintext
.gitlab/agents/<agent-name>/config.yaml
......@@ -53,28 +82,6 @@ The agent bootstraps with the GitLab installation URL and an authentication toke
and you provide the rest of the configuration in your repository, following
Infrastructure as Code (IaaC) best practices.
### Register the agent with GitLab
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5786) in GitLab 14.1, you can create a new agent record directly from the GitLab UI.
Now that you've created your agent configuration file, register it
with GitLab.
When you register the agent, GitLab generates a token that you need to
install the agent in your cluster.
Prerequisite when using a [GitLab CI/CD workflow](../ci_cd_tunnel.md):
- In the project that has the agent configuration file, ensure that [GitLab CI/CD is enabled](../../../../ci/enable_or_disable_ci.md#enable-cicd-in-a-project).
To register the agent with GitLab:
1. On the top bar, select **Menu > Projects** and find the project that has your agent configuration file.
1. From the left sidebar, select **Infrastructure > Kubernetes clusters**.
1. Select **Actions**.
1. From the **Select an agent** dropdown list, select the agent you want to register and select **Register an agent**.
1. GitLab generates a registration token for this agent. Securely store this secret token. You need it to install the agent in your cluster and to [update the agent](#update-the-agent-version) to another version.
1. Copy the command under **Recommended installation method**. You need it when you use the one-liner installation method to install the agent in your cluster.
### Install the agent in the cluster
To connect your cluster to GitLab, install the registered agent
......
......@@ -7657,6 +7657,9 @@ msgstr ""
msgid "ClusterAgents|Actions"
msgstr ""
msgid "ClusterAgents|Add an agent configuration file to %{linkStart}this repository%{linkEnd} and select it, or create a new one to register with GitLab:"
msgstr ""
msgid "ClusterAgents|Advanced installation methods"
msgstr ""
......@@ -7723,9 +7726,6 @@ msgstr ""
msgid "ClusterAgents|Connect with the GitLab Agent"
msgstr ""
msgid "ClusterAgents|Connect your cluster through an agent"
msgstr ""
msgid "ClusterAgents|Connected"
msgstr ""
......@@ -7738,6 +7738,9 @@ msgstr ""
msgid "ClusterAgents|Create a new cluster"
msgstr ""
msgid "ClusterAgents|Create agent: %{searchTerm}"
msgstr ""
msgid "ClusterAgents|Created by"
msgstr ""
......@@ -7747,6 +7750,9 @@ msgstr ""
msgid "ClusterAgents|Date created"
msgstr ""
msgid "ClusterAgents|Default configuration"
msgstr ""
msgid "ClusterAgents|Delete"
msgstr ""
......@@ -7777,10 +7783,7 @@ msgstr ""
msgid "ClusterAgents|Give feedback"
msgstr ""
msgid "ClusterAgents|Go to the repository files"
msgstr ""
msgid "ClusterAgents|How to register an agent?"
msgid "ClusterAgents|How do I register an agent?"
msgstr ""
msgid "ClusterAgents|How to update an agent?"
......@@ -7831,10 +7834,7 @@ msgstr ""
msgid "ClusterAgents|Register"
msgstr ""
msgid "ClusterAgents|Register an agent to generate a token that will be used to install the agent on your cluster in the next step."
msgstr ""
msgid "ClusterAgents|Registering Agent"
msgid "ClusterAgents|Registering agent"
msgstr ""
msgid "ClusterAgents|Registration token"
......@@ -7858,10 +7858,7 @@ msgstr ""
msgid "ClusterAgents|See Agent activity updates such as tokens created or revoked and clusters connected or not connected."
msgstr ""
msgid "ClusterAgents|Select an agent"
msgstr ""
msgid "ClusterAgents|Select an agent to register with GitLab"
msgid "ClusterAgents|Select an agent or enter a name to create new"
msgstr ""
msgid "ClusterAgents|Tell us what you think"
......@@ -7893,9 +7890,6 @@ msgstr ""
msgid "ClusterAgents|To delete the agent, type %{name} to confirm:"
msgstr ""
msgid "ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}Learn more about installing GitLab Agent.%{linkEnd}"
msgstr ""
msgid "ClusterAgents|Token created by %{userName}"
msgstr ""
......@@ -7917,6 +7911,9 @@ msgstr ""
msgid "ClusterAgents|What is GitLab Agent activity?"
msgstr ""
msgid "ClusterAgents|What is default configuration?"
msgstr ""
msgid "ClusterAgents|You cannot see this token again after you close this window."
msgstr ""
......
......@@ -8,6 +8,9 @@ import { stubComponent } from 'helpers/stub_component';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { clusterAgents, connectedTimeNow, connectedTimeInactive } from './mock_data';
const defaultConfigHelpUrl =
'/help/user/clusters/agent/install/index#create-an-agent-without-configuration-file';
const provideData = {
gitlabVersion: '14.8',
};
......@@ -31,8 +34,8 @@ describe('AgentTable', () => {
let wrapper;
const findAgentLink = (at) => wrapper.findAllByTestId('cluster-agent-name-link').at(at);
const findStatusIcon = (at) => wrapper.findAllComponents(GlIcon).at(at);
const findStatusText = (at) => wrapper.findAllByTestId('cluster-agent-connection-status').at(at);
const findStatusIcon = (at) => findStatusText(at).find(GlIcon);
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
const findVersionText = (at) => wrapper.findAllByTestId('cluster-agent-version').at(at);
const findConfiguration = (at) =>
......@@ -141,16 +144,16 @@ describe('AgentTable', () => {
);
it.each`
agentPath | hasLink | lineNumber
${'.gitlab/agents/agent-1'} | ${true} | ${0}
${'.gitlab/agents/agent-2'} | ${false} | ${1}
agentConfig | link | lineNumber
${'.gitlab/agents/agent-1'} | ${'/agent/full/path'} | ${0}
${'Default configuration'} | ${defaultConfigHelpUrl} | ${1}
`(
'displays config file path as "$agentPath" at line $lineNumber',
({ agentPath, hasLink, lineNumber }) => {
({ agentConfig, link, lineNumber }) => {
const findLink = findConfiguration(lineNumber).find(GlLink);
expect(findLink.exists()).toBe(hasLink);
expect(findConfiguration(lineNumber).text()).toBe(agentPath);
expect(findLink.attributes('href')).toBe(link);
expect(findConfiguration(lineNumber).text()).toBe(agentConfig);
},
);
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '~/clusters_list/constants';
......@@ -9,11 +9,14 @@ describe('AvailableAgentsDropdown', () => {
const i18n = I18N_AVAILABLE_AGENTS_DROPDOWN;
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findConfiguredAgentItem = () => findDropdownItems().at(0);
const findFirstAgentItem = () => findDropdownItems().at(0);
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findCreateButton = () => wrapper.findByTestId('create-config-button');
const createWrapper = ({ propsData }) => {
wrapper = shallowMount(AvailableAgentsDropdown, {
wrapper = shallowMountExtended(AvailableAgentsDropdown, {
propsData,
stubs: { GlDropdown },
});
};
......@@ -23,7 +26,7 @@ describe('AvailableAgentsDropdown', () => {
describe('there are agents available', () => {
const propsData = {
availableAgents: ['configured-agent'],
availableAgents: ['configured-agent', 'search-agent', 'test-agent'],
isRegistering: false,
};
......@@ -35,9 +38,38 @@ describe('AvailableAgentsDropdown', () => {
expect(findDropdown().props('text')).toBe(i18n.selectAgent);
});
describe('click events', () => {
describe('search agent', () => {
it('renders search button', () => {
expect(findSearchInput().exists()).toBe(true);
});
it('renders all agents when search term is empty', () => {
expect(findDropdownItems()).toHaveLength(3);
});
it('renders only the agent searched for when the search item exists', async () => {
await findSearchInput().vm.$emit('input', 'search-agent');
expect(findDropdownItems()).toHaveLength(1);
expect(findFirstAgentItem().text()).toBe('search-agent');
});
it('renders create button when search started', async () => {
await findSearchInput().vm.$emit('input', 'new-agent');
expect(findCreateButton().exists()).toBe(true);
});
it("doesn't render create button when search item is found", async () => {
await findSearchInput().vm.$emit('input', 'search-agent');
expect(findCreateButton().exists()).toBe(false);
});
});
describe('select existing agent configuration', () => {
beforeEach(() => {
findConfiguredAgentItem().vm.$emit('click');
findFirstAgentItem().vm.$emit('click');
});
it('emits agentSelected with the name of the clicked agent', () => {
......@@ -46,7 +78,22 @@ describe('AvailableAgentsDropdown', () => {
it('marks the clicked item as selected', () => {
expect(findDropdown().props('text')).toBe('configured-agent');
expect(findConfiguredAgentItem().props('isChecked')).toBe(true);
expect(findFirstAgentItem().props('isChecked')).toBe(true);
});
});
describe('create new agent configuration', () => {
beforeEach(async () => {
await findSearchInput().vm.$emit('input', 'new-agent');
findCreateButton().vm.$emit('click');
});
it('emits agentSelected with the name of the clicked agent', () => {
expect(wrapper.emitted('agentSelected')).toEqual([['new-agent']]);
});
it('marks the clicked item as selected', () => {
expect(findDropdown().props('text')).toBe('new-agent');
});
});
});
......
......@@ -39,6 +39,7 @@ const kasAddress = 'kas.example.com';
const emptyStateImage = 'path/to/image';
const defaultBranchName = 'default';
const maxAgents = MAX_LIST_COUNT;
const i18n = I18N_AGENT_MODAL;
describe('InstallAgentModal', () => {
let wrapper;
......@@ -67,7 +68,7 @@ describe('InstallAgentModal', () => {
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => findButtonByVariant('default');
const findPrimaryButton = () => wrapper.findByTestId('agent-primary-button');
const findImage = () => wrapper.findByRole('img', { alt: I18N_AGENT_MODAL.empty_state.altText });
const findImage = () => wrapper.findByRole('img', { alt: i18n.altText });
const expectDisabledAttribute = (element, disabled) => {
if (disabled) {
......@@ -140,12 +141,13 @@ describe('InstallAgentModal', () => {
apolloProvider = null;
});
describe('when agent configurations are present', () => {
const i18n = I18N_AGENT_MODAL.agent_registration;
describe('when KAS is enabled', () => {
describe('initial state', () => {
it('renders the dropdown for available agents', () => {
expect(findAgentDropdown().isVisible()).toBe(true);
});
it("doesn't render agent installation instructions", () => {
expect(findModal().text()).not.toContain(i18n.basicInstallTitle);
expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false);
expect(findModal().findComponent(GlAlert).exists()).toBe(false);
......@@ -272,44 +274,7 @@ describe('InstallAgentModal', () => {
});
});
describe('when there are no agent configurations present', () => {
const i18n = I18N_AGENT_MODAL.empty_state;
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 primary button', () => {
expect(findPrimaryButton().isVisible()).toBe(true);
expect(findPrimaryButton().text()).toBe(i18n.primaryButton);
});
it('sends the event with the modalType', () => {
findModal().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_OPEN, {
label: EVENT_LABEL_MODAL,
property: MODAL_TYPE_EMPTY,
});
});
});
describe('when KAS is disabled', () => {
const i18n = I18N_AGENT_MODAL.empty_state;
beforeEach(async () => {
apolloProvider = createMockApollo([
[getAgentConfigurations, jest.fn().mockResolvedValue(kasDisabledErrorResponse)],
......@@ -331,11 +296,19 @@ describe('InstallAgentModal', () => {
it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true);
expect(findCancelButton().text()).toBe(i18n.done);
expect(findCancelButton().text()).toBe(i18n.close);
});
it("doesn't render a secondary button", () => {
expect(findPrimaryButton().exists()).toBe(false);
});
it('sends the event with the modalType', () => {
findModal().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_OPEN, {
label: EVENT_LABEL_MODAL,
property: MODAL_TYPE_EMPTY,
});
});
});
});
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