Commit 8f36ef50 authored by Anna Vovchenko's avatar Anna Vovchenko Committed by Frédéric Caplette

Added possiblity to create new token from the UI

We are adding a possibility to create a new agent token from the UI.
The functionality is available thorugh the "Create token" button on the
tokens tab on Agent details page.

Changelog: added
parent eee5d278
<script>
import {
GlButton,
GlModalDirective,
GlTooltip,
GlModal,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlAlert,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import {
CREATE_TOKEN_MODAL,
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
} from '../constants';
import createNewAgentToken from '../graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '../graphql/queries/get_cluster_agent.query.graphql';
import { addAgentTokenToStore } from '../graphql/cache_update';
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL });
export default {
components: {
AgentToken,
GlButton,
GlTooltip,
GlModal,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlAlert,
},
directives: {
GlModalDirective,
},
mixins: [trackingMixin],
inject: ['agentName', 'projectPath', 'canAdminCluster'],
props: {
clusterAgentId: {
required: true,
type: String,
},
cursor: {
required: true,
type: Object,
},
},
modalId: CREATE_TOKEN_MODAL,
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
EVENT_LABEL_MODAL,
TOKEN_NAME_LIMIT,
i18n: {
createTokenButton: s__('ClusterAgents|Create token'),
modalTitle: s__('ClusterAgents|Create agent access token'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
errorTitle: s__('ClusterAgents|Failed to create a token'),
dropdownDisabledHint: s__(
'ClusterAgents|Requires a Maintainer or greater role to perform these actions',
),
modalCancel: __('Cancel'),
modalClose: __('Close'),
tokenNameLabel: __('Name'),
tokenDescriptionLabel: __('Description (optional)'),
},
data() {
return {
token: {
name: null,
description: null,
},
agentToken: null,
error: null,
loading: false,
variables: {
agentName: this.agentName,
projectPath: this.projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...this.cursor,
},
};
},
computed: {
modalBtnDisabled() {
return this.loading || !this.hasTokenName;
},
hasTokenName() {
return Boolean(this.token.name?.length);
},
},
methods: {
async createToken() {
this.loading = true;
this.error = null;
try {
const { errors: tokenErrors, secret } = await this.createAgentTokenMutation();
if (tokenErrors?.length > 0) {
throw new Error(tokenErrors[0]);
}
this.agentToken = secret;
} catch (error) {
if (error) {
this.error = error.message;
} else {
this.error = this.$options.i18n.unknownError;
}
} finally {
this.loading = false;
}
},
resetModal() {
this.agentToken = null;
this.token.name = null;
this.token.description = null;
this.error = null;
},
closeModal() {
this.$refs.modal.hide();
},
createAgentTokenMutation() {
return this.$apollo
.mutate({
mutation: createNewAgentToken,
variables: {
input: {
clusterAgentId: this.clusterAgentId,
name: this.token.name,
description: this.token.description,
},
},
update: (store, { data: { clusterAgentTokenCreate } }) => {
addAgentTokenToStore(
store,
clusterAgentTokenCreate,
getClusterAgentQuery,
this.variables,
);
},
})
.then(({ data: { clusterAgentTokenCreate } }) => clusterAgentTokenCreate);
},
},
};
</script>
<template>
<div>
<div ref="addToken" class="gl-display-inline-block">
<gl-button
v-gl-modal-directive="$options.modalId"
:disabled="!canAdminCluster"
category="primary"
variant="confirm"
>{{ $options.i18n.createTokenButton }}
</gl-button>
<gl-tooltip
v-if="!canAdminCluster"
:target="() => $refs.addToken"
:title="$options.i18n.dropdownDisabledHint"
/>
</div>
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="$options.i18n.modalTitle"
static
lazy
@hidden="resetModal"
@show="track($options.EVENT_ACTIONS_OPEN)"
>
<gl-alert
v-if="error"
:title="$options.i18n.errorTitle"
:dismissible="false"
variant="danger"
class="gl-mb-5"
>
{{ error }}
</gl-alert>
<template v-if="!agentToken">
<gl-form-group :label="$options.i18n.tokenNameLabel">
<gl-form-input
v-model="token.name"
:max-length="$options.TOKEN_NAME_LIMIT"
:disabled="loading"
required
/>
</gl-form-group>
<gl-form-group :label="$options.i18n.tokenDescriptionLabel">
<gl-form-textarea v-model="token.description" :disabled="loading" name="description" />
</gl-form-group>
</template>
<agent-token v-else :agent-token="agentToken" :modal-id="$options.modalId" />
<template #modal-footer>
<gl-button
v-if="!agentToken && !loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
data-testid="agent-token-close-button"
@click="closeModal"
>{{ $options.i18n.modalCancel }}
</gl-button>
<gl-button
v-if="!agentToken"
:disabled="modalBtnDisabled"
:loading="loading"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="create-token"
variant="confirm"
type="submit"
@click="createToken"
>{{ $options.i18n.createTokenButton }}
</gl-button>
<gl-button
v-else
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="close"
variant="confirm"
@click="closeModal"
>{{ $options.i18n.modalClose }}
</gl-button>
</template>
</gl-modal>
</div>
</template>
......@@ -143,7 +143,7 @@ export default {
<gl-loading-icon v-if="isLoading" size="md" class="gl-m-3" />
<div v-else>
<token-table :tokens="tokens" />
<token-table :tokens="tokens" :cluster-agent-id="clusterAgent.id" :cursor="cursor" />
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination v-bind="tokenPageInfo" @prev="prevPage" @next="nextPage" />
......
<script>
import { GlEmptyState, GlLink, GlTable, GlTooltip, GlTruncate } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { GlEmptyState, GlTable, GlTooltip, GlTruncate } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import CreateTokenButton from './create_token_button.vue';
export default {
components: {
GlEmptyState,
GlLink,
GlTable,
GlTooltip,
GlTruncate,
TimeAgoTooltip,
CreateTokenButton,
},
i18n: {
createdBy: s__('ClusterAgents|Created by'),
......@@ -19,7 +19,6 @@ export default {
dateCreated: s__('ClusterAgents|Date created'),
description: s__('ClusterAgents|Description'),
lastUsed: s__('ClusterAgents|Last contact'),
learnMore: s__('ClusterAgents|Learn how to create an agent access token'),
name: s__('ClusterAgents|Name'),
neverUsed: s__('ClusterAgents|Never'),
noTokens: s__('ClusterAgents|This agent has no tokens'),
......@@ -30,6 +29,14 @@ export default {
required: true,
type: Array,
},
clusterAgentId: {
required: true,
type: String,
},
cursor: {
required: true,
type: Object,
},
},
computed: {
fields() {
......@@ -61,11 +68,6 @@ export default {
},
];
},
learnMoreUrl() {
return helpPagePath('user/clusters/agent/install/index', {
anchor: 'register-an-agent-with-gitlab',
});
},
},
methods: {
createdByName(token) {
......@@ -77,11 +79,11 @@ export default {
<template>
<div v-if="tokens.length">
<div class="gl-text-right gl-my-5">
<gl-link target="_blank" :href="learnMoreUrl">
{{ $options.i18n.learnMore }}
</gl-link>
</div>
<create-token-button
class="gl-text-right gl-my-5"
:cluster-agent-id="clusterAgentId"
:cursor="cursor"
/>
<gl-table
:items="tokens"
......@@ -120,10 +122,9 @@ export default {
</gl-table>
</div>
<gl-empty-state
v-else
:title="$options.i18n.noTokens"
:primary-button-link="learnMoreUrl"
:primary-button-text="$options.i18n.learnMore"
/>
<gl-empty-state v-else :title="$options.i18n.noTokens">
<template #actions>
<create-token-button :cluster-agent-id="clusterAgentId" :cursor="cursor" />
</template>
</gl-empty-state>
</template>
......@@ -37,3 +37,10 @@ export const EVENT_DETAILS = {
export const DEFAULT_ICON = 'token';
export const TOKEN_STATUS_ACTIVE = 'ACTIVE';
export const CREATE_TOKEN_MODAL = 'create-token';
export const EVENT_LABEL_MODAL = 'agent_token_creation_modal';
export const EVENT_ACTIONS_OPEN = 'open_modal';
export const EVENT_ACTIONS_CLICK = 'click_button';
export const TOKEN_NAME_LIMIT = 255;
import produce from 'immer';
export const hasErrors = ({ errors = [] }) => errors?.length;
export function addAgentTokenToStore(store, clusterAgentTokenCreate, query, variables) {
if (!hasErrors(clusterAgentTokenCreate)) {
const { token } = clusterAgentTokenCreate;
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
draftData.project.clusterAgent.tokens.nodes.unshift(token);
draftData.project.clusterAgent.tokens.count += 1;
});
store.writeQuery({
query,
variables,
data,
});
}
}
#import "../fragments/cluster_agent_token.fragment.graphql"
mutation createNewAgentToken($input: ClusterAgentTokenCreateInput!) {
clusterAgentTokenCreate(input: $input) {
secret
token {
...Token
}
errors
}
}
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import AgentShowPage from 'ee_else_ce/clusters/agents/components/show.vue';
import apolloProvider from './graphql/provider';
import createRouter from './router';
......@@ -16,6 +17,8 @@ export default () => {
canAdminVulnerability,
emptyStateSvgPath,
projectPath,
kasAddress,
canAdminCluster,
} = el.dataset;
return new Vue({
......@@ -28,6 +31,8 @@ export default () => {
canAdminVulnerability,
emptyStateSvgPath,
projectPath,
kasAddress,
canAdminCluster: parseBoolean(canAdminCluster),
},
render(createElement) {
return createElement(AgentShowPage);
......
<script>
import { GlAlert, GlFormInputGroup, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { generateAgentRegistrationCommand } from '../clusters_util';
import { I18N_AGENT_TOKEN } from '../constants';
export default {
i18n: I18N_AGENT_TOKEN,
basicInstallPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'install-the-agent-into-the-cluster',
}),
advancedInstallPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'advanced-installation',
}),
components: {
GlAlert,
CodeBlock,
GlFormInputGroup,
GlLink,
GlSprintf,
ModalCopyButton,
},
inject: ['kasAddress'],
props: {
agentToken: {
required: true,
type: String,
},
modalId: {
required: true,
type: String,
},
},
computed: {
agentRegistrationCommand() {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
},
},
};
</script>
<template>
<div>
<p>
<strong>{{ $options.i18n.tokenTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.tokenBody">
<template #link="{ content }">
<gl-link :href="$options.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-form-input-group readonly :value="agentToken" :select-on-click="true">
<template #append>
<modal-copy-button
:text="agentToken"
:title="$options.i18n.copyToken"
:modal-id="modalId"
/>
</template>
</gl-form-input-group>
</p>
<p>
<strong>{{ $options.i18n.basicInstallTitle }}</strong>
</p>
<p>
{{ $options.i18n.basicInstallBody }}
</p>
<p class="gl-display-flex gl-align-items-flex-start">
<code-block class="gl-w-full" :code="agentRegistrationCommand" />
<modal-copy-button
:title="$options.i18n.copyCommand"
:text="agentRegistrationCommand"
:modal-id="modalId"
/>
</p>
<p>
<strong>{{ $options.i18n.advancedInstallTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.advancedInstallBody">
<template #link="{ content }">
<gl-link :href="$options.advancedInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</div>
</template>
<script>
import {
GlAlert,
GlButton,
GlFormGroup,
GlFormInputGroup,
GlLink,
GlModal,
GlSprintf,
} from '@gitlab/ui';
import { GlAlert, GlButton, GlFormGroup, GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import Tracking from '~/tracking';
import { generateAgentRegistrationCommand } from '../clusters_util';
import {
INSTALL_AGENT_MODAL_ID,
I18N_AGENT_MODAL,
......@@ -30,6 +19,7 @@ import createAgentToken from '../graphql/mutations/create_agent_token.mutation.g
import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
import agentConfigurations from '../graphql/queries/agent_configurations.query.graphql';
import AvailableAgentsDropdown from './available_agents_dropdown.vue';
import AgentToken from './agent_token.vue';
const trackingMixin = Tracking.mixin({ label: EVENT_LABEL_MODAL });
......@@ -39,30 +29,22 @@ export default {
EVENT_ACTIONS_OPEN,
EVENT_ACTIONS_CLICK,
EVENT_LABEL_MODAL,
basicInstallPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'install-the-agent-into-the-cluster',
}),
advancedInstallPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'advanced-installation',
}),
enableKasPath: helpPagePath('administration/clusters/kas'),
registerAgentPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'register-an-agent-with-gitlab',
}),
components: {
AvailableAgentsDropdown,
CodeBlock,
AgentToken,
GlAlert,
GlButton,
GlFormGroup,
GlFormInputGroup,
GlLink,
GlModal,
GlSprintf,
ModalCopyButton,
},
mixins: [trackingMixin],
inject: ['projectPath', 'kasAddress', 'emptyStateImage'],
inject: ['projectPath', 'emptyStateImage'],
props: {
defaultBranchName: {
default: '.noBranch',
......@@ -114,9 +96,6 @@ export default {
canRegister() {
return !this.registered && !this.kasDisabled;
},
agentRegistrationCommand() {
return generateAgentRegistrationCommand(this.agentToken, this.kasAddress);
},
getAgentsQueryVariables() {
return {
defaultBranchName: this.defaultBranchName,
......@@ -289,65 +268,7 @@ export default {
</p>
</template>
<template v-else>
<p>
<strong>{{ $options.i18n.tokenTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.tokenBody">
<template #link="{ content }">
<gl-link :href="$options.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-form-input-group readonly :value="agentToken" :select-on-click="true">
<template #append>
<modal-copy-button
:text="agentToken"
:title="$options.i18n.copyToken"
:modal-id="$options.modalId"
/>
</template>
</gl-form-input-group>
</p>
<p>
<strong>{{ $options.i18n.basicInstallTitle }}</strong>
</p>
<p>
{{ $options.i18n.basicInstallBody }}
</p>
<p>
<code-block :code="agentRegistrationCommand" />
</p>
<p>
<strong>{{ $options.i18n.advancedInstallTitle }}</strong>
</p>
<p>
<gl-sprintf :message="$options.i18n.advancedInstallBody">
<template #link="{ content }">
<gl-link :href="$options.advancedInstallPath" target="_blank"> {{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>
<agent-token v-else :agent-token="agentToken" :modal-id="$options.modalId" />
</template>
<template v-else>
......
......@@ -87,26 +87,14 @@ export const I18N_AGENT_TABLE = {
defaultConfigTooltip: s__('ClusterAgents|What is default configuration?'),
};
export const I18N_AGENT_MODAL = {
registerAgentButton: s__('ClusterAgents|Register'),
close: __('Close'),
cancel: __('Cancel'),
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?'),
export const I18N_AGENT_TOKEN = {
copyToken: s__('ClusterAgents|Copy token'),
copyCommand: s__('ClusterAgents|Copy command'),
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.',
),
......@@ -123,7 +111,27 @@ export const I18N_AGENT_MODAL = {
advancedInstallBody: s__(
'ClusterAgents|For the advanced installation method %{linkStart}see the documentation%{linkEnd}.',
),
};
export const I18N_AGENT_MODAL = {
registerAgentButton: s__('ClusterAgents|Register'),
close: __('Close'),
cancel: __('Cancel'),
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}`,
),
registrationErrorTitle: s__('ClusterAgents|Failed to register an agent'),
unknownError: s__('ClusterAgents|An unknown error occurred. Please try again.'),
};
......
......@@ -7,7 +7,9 @@ module Projects::ClusterAgentsHelper
agent_name: agent_name,
can_admin_vulnerability: can?(current_user, :admin_vulnerability, project).to_s,
empty_state_svg_path: image_path('illustrations/operations-dashboard_empty.svg'),
project_path: project.full_path
project_path: project.full_path,
kas_address: Gitlab::Kas.external_url,
can_admin_cluster: can?(current_user, :admin_cluster, project).to_s
}
end
end
---
description: Create token modal opened from the agent's page
category: default
action: open_modal
label_description: agent_token_creation_modal
product_section: ops
product_stage: configure
product_group: group::configure
milestone: "14.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82690
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate
---
description: Button clicked within the create token modal on the agent's page
category: default
action: click_button
label_description: agent_token_creation_modal
property_description: One of "create-token", "close"
product_section: ops
product_stage: configure
product_group: group::configure
milestone: "14.9"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82690
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate
......@@ -75,6 +75,22 @@ observability:
level: debug
```
## Reset the agent token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327152) in GitLab 14.9.
To reset the agent token without downtime:
1. Create a new token:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Infrastructure > Kubernetes clusters**.
1. Select the agent you want to create a token for.
1. On the **Tokens** tab, select **Create token**.
1. Enter token's name and description (optional) and select **Create token**.
1. Securely store the generated token.
1. Use the token to [install the agent in your cluster](install/index.md#install-the-agent-in-the-cluster) and to [update the agent](install/index.md#update-the-agent-version) to another version.
1. Delete the token you're no longer using.
## Remove an agent
You can remove an agent by using the [GitLab UI](#remove-an-agent-through-the-gitlab-ui) or the
......
......@@ -7796,15 +7796,24 @@ msgstr ""
msgid "ClusterAgents|Connection status"
msgstr ""
msgid "ClusterAgents|Copy command"
msgstr ""
msgid "ClusterAgents|Copy token"
msgstr ""
msgid "ClusterAgents|Create a new cluster"
msgstr ""
msgid "ClusterAgents|Create agent access token"
msgstr ""
msgid "ClusterAgents|Create agent: %{searchTerm}"
msgstr ""
msgid "ClusterAgents|Create token"
msgstr ""
msgid "ClusterAgents|Created by"
msgstr ""
......@@ -7832,6 +7841,9 @@ msgstr ""
msgid "ClusterAgents|Event occurred"
msgstr ""
msgid "ClusterAgents|Failed to create a token"
msgstr ""
msgid "ClusterAgents|Failed to register an agent"
msgstr ""
......@@ -7862,9 +7874,6 @@ msgstr ""
msgid "ClusterAgents|Last contact"
msgstr ""
msgid "ClusterAgents|Learn how to create an agent access token"
msgstr ""
msgid "ClusterAgents|Learn how to troubleshoot"
msgstr ""
......
import { GlButton, GlTooltip, GlModal, GlFormInput, GlFormTextarea, GlAlert } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import {
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
TOKEN_NAME_LIMIT,
TOKEN_STATUS_ACTIVE,
MAX_LIST_COUNT,
} from '~/clusters/agents/constants';
import createNewAgentToken from '~/clusters/agents/graphql/mutations/create_new_agent_token.mutation.graphql';
import getClusterAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import CreateTokenButton from '~/clusters/agents/components/create_token_button.vue';
import {
clusterAgentToken,
getTokenResponse,
createAgentTokenErrorResponse,
} from '../../mock_data';
Vue.use(VueApollo);
describe('CreateTokenButton', () => {
let wrapper;
let apolloProvider;
let trackingSpy;
let createResponse;
const clusterAgentId = 'cluster-agent-id';
const cursor = {
first: MAX_LIST_COUNT,
last: null,
};
const agentName = 'cluster-agent';
const projectPath = 'path/to/project';
const defaultProvide = {
agentName,
projectPath,
canAdminCluster: true,
};
const propsData = {
clusterAgentId,
cursor,
};
const findModal = () => wrapper.findComponent(GlModal);
const findBtn = () => wrapper.findComponent(GlButton);
const findInput = () => wrapper.findComponent(GlFormInput);
const findTextarea = () => wrapper.findComponent(GlFormTextarea);
const findAlert = () => wrapper.findComponent(GlAlert);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findAgentInstructions = () => findModal().findComponent(AgentToken);
const findButtonByVariant = (variant) =>
findModal()
.findAll(GlButton)
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => wrapper.findByTestId('agent-token-close-button');
const expectDisabledAttribute = (element, disabled) => {
if (disabled) {
expect(element.attributes('disabled')).toBe('true');
} else {
expect(element.attributes('disabled')).toBeUndefined();
}
};
const createMockApolloProvider = ({ mutationResponse }) => {
createResponse = jest.fn().mockResolvedValue(mutationResponse);
return createMockApollo([[createNewAgentToken, createResponse]]);
};
const writeQuery = () => {
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getClusterAgentQuery,
data: getTokenResponse.data,
variables: {
agentName,
projectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
...cursor,
},
});
};
const createWrapper = async ({ provideData = {} } = {}) => {
wrapper = shallowMountExtended(CreateTokenButton, {
apolloProvider,
provide: {
...defaultProvide,
...provideData,
},
propsData,
stubs: {
GlModal,
GlTooltip,
},
});
wrapper.vm.$refs.modal.hide = jest.fn();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
const mockCreatedResponse = (mutationResponse) => {
apolloProvider = createMockApolloProvider({ mutationResponse });
writeQuery();
createWrapper();
findInput().vm.$emit('input', 'new-token');
findTextarea().vm.$emit('input', 'new-token-description');
findActionButton().vm.$emit('click');
return waitForPromises();
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
apolloProvider = null;
createResponse = null;
});
describe('create agent token action', () => {
it('displays create agent token button', () => {
expect(findBtn().text()).toBe('Create token');
});
describe('when user cannot create token', () => {
beforeEach(() => {
createWrapper({ provideData: { canAdminCluster: false } });
});
it('disabled the button', () => {
expect(findBtn().attributes('disabled')).toBe('true');
});
it('shows a disabled tooltip', () => {
expect(findTooltip().attributes('title')).toBe(
'Requires a Maintainer or greater role to perform these actions',
);
});
});
describe('when user can create a token and clicks the button', () => {
beforeEach(() => {
findBtn().vm.$emit('click');
});
it('displays a token creation modal', () => {
expect(findModal().isVisible()).toBe(true);
});
describe('initial state', () => {
it('renders an input for the token name', () => {
expect(findInput().exists()).toBe(true);
expectDisabledAttribute(findInput(), false);
expect(findInput().attributes('max-length')).toBe(TOKEN_NAME_LIMIT.toString());
});
it('renders a textarea for the token description', () => {
expect(findTextarea().exists()).toBe(true);
expectDisabledAttribute(findTextarea(), false);
});
it('renders a cancel button', () => {
expect(findCancelButton().isVisible()).toBe(true);
expectDisabledAttribute(findCancelButton(), false);
});
it('renders a disabled next button', () => {
expect(findActionButton().text()).toBe('Create token');
expectDisabledAttribute(findActionButton(), true);
});
it('sends tracking event for modal shown', () => {
findModal().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTIONS_OPEN, {
label: EVENT_LABEL_MODAL,
});
});
});
describe('when user inputs the token name', () => {
beforeEach(() => {
expectDisabledAttribute(findActionButton(), true);
findInput().vm.$emit('input', 'new-token');
});
it('enables the next button', () => {
expectDisabledAttribute(findActionButton(), false);
});
});
describe('when user clicks the create-token button', () => {
beforeEach(async () => {
const loadingResponse = new Promise(() => {});
await mockCreatedResponse(loadingResponse);
findInput().vm.$emit('input', 'new-token');
findActionButton().vm.$emit('click');
});
it('disables the create-token button', () => {
expectDisabledAttribute(findActionButton(), true);
});
it('hides the cancel button', () => {
expect(findCancelButton().exists()).toBe(false);
});
});
describe('creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(clusterAgentToken);
});
it('creates a token', () => {
expect(createResponse).toHaveBeenCalledWith({
input: { clusterAgentId, name: 'new-token', description: 'new-token-description' },
});
});
it('shows agent instructions', () => {
expect(findAgentInstructions().exists()).toBe(true);
});
it('renders a close button', () => {
expect(findActionButton().isVisible()).toBe(true);
expect(findActionButton().text()).toBe('Close');
expectDisabledAttribute(findActionButton(), false);
});
});
describe('error creating a new token', () => {
beforeEach(async () => {
await mockCreatedResponse(createAgentTokenErrorResponse);
});
it('displays the error message', async () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
});
});
});
});
});
import { GlEmptyState, GlLink, GlTooltip, GlTruncate } from '@gitlab/ui';
import { GlEmptyState, GlTooltip, GlTruncate } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import TokenTable from '~/clusters/agents/components/token_table.vue';
import CreateTokenButton from '~/clusters/agents/components/create_token_button.vue';
import { useFakeDate } from 'helpers/fake_date';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MAX_LIST_COUNT } from '~/clusters/agents/constants';
describe('ClusterAgentTokenTable', () => {
let wrapper;
......@@ -28,13 +30,26 @@ describe('ClusterAgentTokenTable', () => {
name: 'token-2',
},
];
const clusterAgentId = 'cluster-agent-id';
const cursor = {
first: MAX_LIST_COUNT,
last: null,
};
const provide = {
agentName: 'cluster-agent',
projectPath: 'path/to/project',
canAdminCluster: true,
};
const createComponent = (tokens) => {
wrapper = extendedWrapper(mount(TokenTable, { propsData: { tokens } }));
wrapper = extendedWrapper(
mount(TokenTable, { propsData: { tokens, clusterAgentId, cursor }, provide }),
);
};
const findEmptyState = () => wrapper.find(GlEmptyState);
const findLink = () => wrapper.find(GlLink);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findCreateTokenBtn = () => wrapper.findComponent(CreateTokenButton);
beforeEach(() => {
return createComponent(defaultTokens);
......@@ -44,11 +59,15 @@ describe('ClusterAgentTokenTable', () => {
wrapper.destroy();
});
it('displays a learn more link', () => {
const learnMoreLink = findLink();
it('displays the create token button', () => {
expect(findCreateTokenBtn().exists()).toBe(true);
});
expect(learnMoreLink.exists()).toBe(true);
expect(learnMoreLink.text()).toBe(TokenTable.i18n.learnMore);
it('passes the correct params to the create token component', () => {
expect(findCreateTokenBtn().props()).toMatchObject({
clusterAgentId,
cursor,
});
});
it.each`
......@@ -56,7 +75,7 @@ describe('ClusterAgentTokenTable', () => {
${'token-1'} | ${0}
${'token-2'} | ${1}
`('displays token name "$name" for line "$lineNumber"', ({ name, lineNumber }) => {
const tokens = wrapper.findAll('[data-testid="agent-token-name"]');
const tokens = wrapper.findAllByTestId('agent-token-name');
const token = tokens.at(lineNumber);
expect(token.text()).toBe(name);
......@@ -83,7 +102,7 @@ describe('ClusterAgentTokenTable', () => {
`(
'displays created information "$createdText" for line "$lineNumber"',
({ createdText, lineNumber }) => {
const tokens = wrapper.findAll('[data-testid="agent-token-created-time"]');
const tokens = wrapper.findAllByTestId('agent-token-created-time');
const token = tokens.at(lineNumber);
expect(token.text()).toBe(createdText);
......@@ -97,7 +116,7 @@ describe('ClusterAgentTokenTable', () => {
`(
'displays creator information "$createdBy" for line "$lineNumber"',
({ createdBy, lineNumber }) => {
const tokens = wrapper.findAll('[data-testid="agent-token-created-user"]');
const tokens = wrapper.findAllByTestId('agent-token-created-user');
const token = tokens.at(lineNumber);
expect(token.text()).toBe(createdBy);
......@@ -111,7 +130,7 @@ describe('ClusterAgentTokenTable', () => {
`(
'displays description information "$description" for line "$lineNumber"',
({ description, truncatesText, hasTooltip, lineNumber }) => {
const tokens = wrapper.findAll('[data-testid="agent-token-description"]');
const tokens = wrapper.findAllByTestId('agent-token-description');
const token = tokens.at(lineNumber);
expect(token.text()).toContain(description);
......
......@@ -163,3 +163,60 @@ export const mockAgentHistoryActivityItems = [
body: 'Event occurred',
},
];
export const clusterAgentToken = {
data: {
clusterAgentTokenCreate: {
errors: [],
secret: 'token-secret',
token: {
createdAt: '2022-03-13T18:42:44Z',
createdByUser: {
...user,
},
description: 'token-description',
id: 'token-id',
lastUsedAt: null,
name: 'token-name',
__typename: 'ClusterAgentToken',
},
__typename: 'ClusterAgentTokenCreatePayload',
},
},
};
export const createAgentTokenErrorResponse = {
data: {
clusterAgentTokenCreate: {
token: null,
secret: null,
errors: ['could not create agent token'],
},
},
};
export const getTokenResponse = {
data: {
project: {
id: 'project-1',
clusterAgent: {
id: 'cluster-agent-id',
createdAt: '2022-03-13T18:42:44Z',
createdByUser: {
...user,
},
tokens: {
count: 1,
nodes: [{ ...clusterAgentToken.token }],
pageInfo: {
endCursor: '',
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
},
},
},
__typename: 'Project',
},
},
};
import { GlAlert, GlFormInputGroup } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import { I18N_AGENT_TOKEN, INSTALL_AGENT_MODAL_ID } from '~/clusters_list/constants';
import { generateAgentRegistrationCommand } from '~/clusters_list/clusters_util';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
const kasAddress = 'kas.example.com';
const agentToken = 'agent-token';
const modalId = INSTALL_AGENT_MODAL_ID;
describe('InstallAgentModal', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const findCodeBlock = () => wrapper.findComponent(CodeBlock);
const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
const findInput = () => wrapper.findComponent(GlFormInputGroup);
const createWrapper = () => {
const provide = {
kasAddress,
};
const propsData = {
agentToken,
modalId,
};
wrapper = shallowMountExtended(AgentToken, {
provide,
propsData,
});
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
describe('initial state', () => {
it('shows basic agent installation instructions', () => {
expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.basicInstallTitle);
expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.basicInstallBody);
});
it('shows advanced agent installation instructions', () => {
expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.advancedInstallTitle);
});
it('shows agent token as an input value', () => {
expect(findInput().props('value')).toBe('agent-token');
});
it('renders a copy button', () => {
expect(findCopyButton().props()).toMatchObject({
title: 'Copy command',
text: generateAgentRegistrationCommand(agentToken, kasAddress),
modalId,
});
});
it('shows warning alert', () => {
expect(findAlert().props('title')).toBe(I18N_AGENT_TOKEN.tokenSingleUseWarningTitle);
});
it('shows code block with agent installation command', () => {
expect(findCodeBlock().props('code')).toContain('--agent-token=agent-token');
expect(findCodeBlock().props('code')).toContain('--kas-address=kas.example.com');
});
});
});
......@@ -6,6 +6,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import AvailableAgentsDropdown from '~/clusters_list/components/available_agents_dropdown.vue';
import InstallAgentModal from '~/clusters_list/components/install_agent_modal.vue';
import AgentToken from '~/clusters_list/components/agent_token.vue';
import {
I18N_AGENT_MODAL,
MAX_LIST_COUNT,
......@@ -21,7 +22,6 @@ import createAgentMutation from '~/clusters_list/graphql/mutations/create_agent.
import createAgentTokenMutation from '~/clusters_list/graphql/mutations/create_agent_token.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import {
createAgentResponse,
createAgentErrorResponse,
......@@ -61,6 +61,7 @@ describe('InstallAgentModal', () => {
const findModal = () => wrapper.findComponent(ModalStub);
const findAgentDropdown = () => findModal().findComponent(AvailableAgentsDropdown);
const findAlert = () => findModal().findComponent(GlAlert);
const findAgentInstructions = () => findModal().findComponent(AgentToken);
const findButtonByVariant = (variant) =>
findModal()
.findAll(GlButton)
......@@ -151,7 +152,6 @@ describe('InstallAgentModal', () => {
expect(findModal().text()).not.toContain(i18n.basicInstallTitle);
expect(findModal().findComponent(GlFormInputGroup).exists()).toBe(false);
expect(findModal().findComponent(GlAlert).exists()).toBe(false);
expect(findModal().findComponent(CodeBlock).exists()).toBe(false);
});
it('renders a cancel button', () => {
......@@ -222,19 +222,7 @@ describe('InstallAgentModal', () => {
});
it('shows agent instructions', () => {
const modalText = findModal().text();
expect(modalText).toContain(i18n.basicInstallTitle);
expect(modalText).toContain(i18n.basicInstallBody);
const token = findModal().findComponent(GlFormInputGroup);
expect(token.props('value')).toBe('mock-agent-token');
const alert = findModal().findComponent(GlAlert);
expect(alert.props('title')).toBe(i18n.tokenSingleUseWarningTitle);
const code = findModal().findComponent(CodeBlock).props('code');
expect(code).toContain('--agent-token=mock-agent-token');
expect(code).toContain('--kas-address=kas.example.com');
expect(findAgentInstructions().exists()).toBe(true);
});
describe('error creating agent', () => {
......
......@@ -8,6 +8,7 @@ RSpec.describe Projects::ClusterAgentsHelper do
let_it_be(:current_user) { create(:user) }
let(:user_can_admin_vulerability) { true }
let(:user_can_admin_cluster) { false }
let(:agent_name) { 'agent-name' }
before do
......@@ -16,6 +17,10 @@ RSpec.describe Projects::ClusterAgentsHelper do
.to receive(:can?)
.with(current_user, :admin_vulnerability, project)
.and_return(user_can_admin_vulerability)
allow(helper)
.to receive(:can?)
.with(current_user, :admin_cluster, project)
.and_return(user_can_admin_cluster)
end
subject { helper.js_cluster_agent_details_data(agent_name, project) }
......@@ -26,8 +31,18 @@ RSpec.describe Projects::ClusterAgentsHelper do
project_path: project.full_path,
activity_empty_state_image: kind_of(String),
empty_state_svg_path: kind_of(String),
can_admin_vulnerability: "true"
can_admin_vulnerability: "true",
kas_address: Gitlab::Kas.external_url,
can_admin_cluster: "false"
})
}
context 'user has admin cluster permissions' do
let(:user_can_admin_cluster) { true }
it 'displays that the user can admin cluster' do
expect(subject[:can_admin_cluster]).to eq("true")
end
end
end
end
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