diff options
Diffstat (limited to 'app/assets/javascripts/runner/components/registration')
3 files changed, 308 insertions, 0 deletions
diff --git a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue new file mode 100644 index 00000000000..3fbe3c1be74 --- /dev/null +++ b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue @@ -0,0 +1,112 @@ +<script> +import { + GlFormGroup, + GlDropdown, + GlDropdownForm, + GlDropdownItem, + GlDropdownDivider, +} from '@gitlab/ui'; +import { s__ } from '~/locale'; +import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants'; +import RegistrationToken from './registration_token.vue'; +import RegistrationTokenResetDropdownItem from './registration_token_reset_dropdown_item.vue'; + +export default { + i18n: { + showInstallationInstructions: s__( + 'Runners|Show runner installation and registration instructions', + ), + registrationToken: s__('Runners|Registration token'), + }, + components: { + GlFormGroup, + GlDropdown, + GlDropdownForm, + GlDropdownItem, + GlDropdownDivider, + RegistrationToken, + RunnerInstructionsModal, + RegistrationTokenResetDropdownItem, + }, + props: { + registrationToken: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + validator(type) { + return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type); + }, + }, + }, + data() { + return { + currentRegistrationToken: this.registrationToken, + instructionsModalOpened: false, + }; + }, + computed: { + dropdownText() { + switch (this.type) { + case INSTANCE_TYPE: + return s__('Runners|Register an instance runner'); + case GROUP_TYPE: + return s__('Runners|Register a group runner'); + case PROJECT_TYPE: + return s__('Runners|Register a project runner'); + default: + return s__('Runners|Register a runner'); + } + }, + }, + methods: { + onShowInstructionsClick() { + // Rendering the modal on demand, to avoid + // loading instructions prematurely from API. + this.instructionsModalOpened = true; + + this.$nextTick(() => { + // $refs.runnerInstructionsModal is defined in + // the tick after the modal is rendered + this.$refs.runnerInstructionsModal.show(); + }); + }, + onTokenReset(token) { + this.currentRegistrationToken = token; + + this.$refs.runnerRegistrationDropdown.hide(true); + }, + }, +}; +</script> + +<template> + <gl-dropdown + ref="runnerRegistrationDropdown" + menu-class="gl-w-auto!" + :text="dropdownText" + variant="confirm" + v-bind="$attrs" + > + <gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick"> + {{ $options.i18n.showInstallationInstructions }} + <runner-instructions-modal + v-if="instructionsModalOpened" + ref="runnerInstructionsModal" + :registration-token="registrationToken" + data-testid="runner-instructions-modal" + /> + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-form class="gl-p-4!"> + <gl-form-group class="gl-mb-0" :label="$options.i18n.registrationToken"> + <registration-token :value="currentRegistrationToken" /> + </gl-form-group> + </gl-dropdown-form> + <gl-dropdown-divider /> + <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" /> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/runner/components/registration/registration_token.vue b/app/assets/javascripts/runner/components/registration/registration_token.vue new file mode 100644 index 00000000000..d54a66ff0e4 --- /dev/null +++ b/app/assets/javascripts/runner/components/registration/registration_token.vue @@ -0,0 +1,83 @@ +<script> +import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { s__, __ } from '~/locale'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; + +export default { + components: { + GlButtonGroup, + GlButton, + ModalCopyButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + value: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + isMasked: true, + }; + }, + computed: { + maskLabel() { + if (this.isMasked) { + return __('Click to reveal'); + } + return __('Click to hide'); + }, + maskIcon() { + if (this.isMasked) { + return 'eye'; + } + return 'eye-slash'; + }, + displayedValue() { + if (this.isMasked && this.value?.length) { + return '*'.repeat(this.value.length); + } + return this.value; + }, + }, + methods: { + onToggleMasked() { + this.isMasked = !this.isMasked; + }, + onCopied() { + // value already in the clipboard, simply notify the user + this.$toast?.show(s__('Runners|Registration token copied!')); + }, + }, + i18n: { + copyLabel: s__('Runners|Copy registration token'), + }, +}; +</script> +<template> + <gl-button-group> + <gl-button class="gl-font-monospace" data-testid="token-value" label> + {{ displayedValue }} + </gl-button> + <gl-button + v-gl-tooltip + :aria-label="maskLabel" + :title="maskLabel" + :icon="maskIcon" + class="gl-w-auto! gl-flex-shrink-0!" + data-testid="toggle-masked" + @click.stop="onToggleMasked" + /> + <modal-copy-button + class="gl-w-auto! gl-flex-shrink-0!" + :aria-label="$options.i18n.copyLabel" + :title="$options.i18n.copyLabel" + :text="value" + @success="onCopied" + /> + </gl-button-group> +</template> diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue new file mode 100644 index 00000000000..3bb15bff8d8 --- /dev/null +++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue @@ -0,0 +1,113 @@ +<script> +import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { __, s__ } from '~/locale'; +import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql'; +import { captureException } from '~/runner/sentry_utils'; +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants'; + +export default { + name: 'RunnerRegistrationTokenReset', + components: { + GlDropdownItem, + GlLoadingIcon, + }, + inject: { + groupId: { + default: null, + }, + projectId: { + default: null, + }, + }, + props: { + type: { + type: String, + required: true, + validator(type) { + return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type); + }, + }, + }, + data() { + return { + loading: false, + }; + }, + computed: { + resetTokenInput() { + switch (this.type) { + case INSTANCE_TYPE: + return { + type: this.type, + }; + case GROUP_TYPE: + return { + id: convertToGraphQLId(TYPE_GROUP, this.groupId), + type: this.type, + }; + case PROJECT_TYPE: + return { + id: convertToGraphQLId(TYPE_PROJECT, this.projectId), + type: this.type, + }; + default: + return null; + } + }, + }, + methods: { + async resetToken() { + // TODO Replace confirmation with gl-modal + // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810 + // eslint-disable-next-line no-alert + if (!window.confirm(__('Are you sure you want to reset the registration token?'))) { + return; + } + + this.loading = true; + try { + const { + data: { + runnersRegistrationTokenReset: { token, errors }, + }, + } = await this.$apollo.mutate({ + mutation: runnersRegistrationTokenResetMutation, + variables: { + input: this.resetTokenInput, + }, + }); + if (errors && errors.length) { + throw new Error(errors.join(' ')); + } + this.onSuccess(token); + } catch (e) { + this.onError(e); + } finally { + this.loading = false; + } + }, + onError(error) { + const { message } = error; + createFlash({ message }); + + this.reportToSentry(error); + }, + onSuccess(token) { + this.$toast?.show(s__('Runners|New registration token generated!')); + this.$emit('tokenReset', token); + }, + reportToSentry(error) { + captureException({ error, component: this.$options.name }); + }, + }, +}; +</script> +<template> + <gl-dropdown-item @click.capture.native.stop="resetToken"> + {{ __('Reset registration token') }} + <gl-loading-icon v-if="loading" inline /> + </gl-dropdown-item> +</template> |