diff options
Diffstat (limited to 'app/assets/javascripts/ci/runner')
23 files changed, 607 insertions, 367 deletions
diff --git a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue index d385d32fd9d..c2ec8462a0e 100644 --- a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue +++ b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue @@ -4,10 +4,8 @@ import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { visitUrl } from '~/lib/utils/url_utility'; -import RunnerDeleteButton from '../components/runner_delete_button.vue'; -import RunnerEditButton from '../components/runner_edit_button.vue'; -import RunnerPauseButton from '../components/runner_pause_button.vue'; import RunnerHeader from '../components/runner_header.vue'; +import RunnerHeaderActions from '../components/runner_header_actions.vue'; import RunnerDetailsTabs from '../components/runner_details_tabs.vue'; import { I18N_FETCH_ERROR } from '../constants'; @@ -18,10 +16,8 @@ import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_lo export default { name: 'AdminRunnerShowApp', components: { - RunnerDeleteButton, - RunnerEditButton, - RunnerPauseButton, RunnerHeader, + RunnerHeaderActions, RunnerDetailsTabs, }, props: { @@ -80,9 +76,11 @@ export default { <div> <runner-header v-if="runner" :runner="runner"> <template #actions> - <runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" /> - <runner-pause-button v-if="canUpdate" :runner="runner" /> - <runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" /> + <runner-header-actions + :runner="runner" + :edit-path="runner.editAdminUrl" + @deleted="onDeleted" + /> </template> </runner-header> <runner-details-tabs v-if="runner" :runner="runner" /> diff --git a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue index 4d88feebe53..2168685e703 100644 --- a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue @@ -126,10 +126,6 @@ export default { isSearchFiltered() { return isSearchFiltered(this.search); }, - shouldShowCreateRunnerWorkflow() { - // create_runner_workflow_for_admin feature flag - return this.glFeatures.createRunnerWorkflowForAdmin; - }, }, watch: { search: { @@ -193,14 +189,14 @@ export default { /> <div class="gl-w-full gl-md-w-auto gl-display-flex"> - <gl-button v-if="shouldShowCreateRunnerWorkflow" :href="newRunnerPath" variant="confirm"> + <gl-button :href="newRunnerPath" variant="confirm"> {{ s__('Runners|New instance runner') }} </gl-button> <registration-dropdown class="gl-ml-3" :registration-token="registrationToken" :type="$options.INSTANCE_TYPE" - right + placement="right" /> </div> </div> diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue index 9f4ce14f704..cc31afea88c 100644 --- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue +++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue @@ -1,6 +1,6 @@ <script> import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; -import { sprintf, __ } from '~/locale'; +import { sprintf, __, formatNumber } from '~/locale'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; @@ -49,6 +49,12 @@ export default { managersCount() { return this.runner.managers?.count || 0; }, + firstIpAddress() { + return this.runner.managers?.nodes?.[0]?.ipAddress || null; + }, + additionalIpAddressCount() { + return this.managersCount - 1; + }, jobCount() { return formatJobCount(this.runner.jobCount); }, @@ -63,6 +69,9 @@ export default { return null; }, }, + methods: { + formatNumber, + }, i18n: { I18N_NO_DESCRIPTION, I18N_LOCKED_RUNNER_DESCRIPTION, @@ -120,8 +129,11 @@ export default { </gl-sprintf> </runner-summary-field> - <runner-summary-field v-if="runner.ipAddress" icon="disk" :tooltip="__('IP Address')"> - {{ runner.ipAddress }} + <runner-summary-field v-if="firstIpAddress" icon="disk" :tooltip="__('IP Address')"> + {{ firstIpAddress }} + <template v-if="additionalIpAddressCount" + >(+{{ formatNumber(additionalIpAddressCount) }})</template + > </runner-summary-field> <runner-summary-field icon="pipeline" data-testid="job-count" :tooltip="__('Jobs')"> diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue index 2fdf8456615..0154cd2a3ec 100644 --- a/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue +++ b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue @@ -1,5 +1,11 @@ <script> -import { GlDropdown, GlDropdownForm, GlDropdownItem, GlDropdownDivider, GlIcon } from '@gitlab/ui'; +import { + GlDisclosureDropdown, + GlDropdownForm, + GlDisclosureDropdownItem, + GlDisclosureDropdownGroup, + GlIcon, +} from '@gitlab/ui'; import { s__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; @@ -20,12 +26,15 @@ export default { showInstallationInstructions: s__( 'Runners|Show runner installation and registration instructions', ), + supportForRegistrationTokensDeprecated: s__( + 'Runners|Support for registration tokens is deprecated', + ), }, components: { - GlDropdown, + GlDisclosureDropdown, + GlDisclosureDropdownItem, + GlDisclosureDropdownGroup, GlDropdownForm, - GlDropdownItem, - GlDropdownDivider, GlIcon, RegistrationToken, RunnerInstructionsModal, @@ -51,14 +60,6 @@ export default { }; }, computed: { - isDeprecated() { - // Show a compact version when used as secondary option - // create_runner_workflow_for_admin or create_runner_workflow_for_namespace - return ( - this.glFeatures?.createRunnerWorkflowForAdmin || - this.glFeatures?.createRunnerWorkflowForNamespace - ); - }, actionText() { switch (this.type) { case INSTANCE_TYPE: @@ -71,30 +72,6 @@ export default { return I18N_REGISTER_RUNNER; } }, - dropdownText() { - if (this.isDeprecated) { - return ''; - } - return this.actionText; - }, - dropdownToggleClass() { - if (this.isDeprecated) { - return ['gl-px-3!']; - } - return []; - }, - dropdownCategory() { - if (this.isDeprecated) { - return 'tertiary'; - } - return 'primary'; - }, - dropdownVariant() { - if (this.isDeprecated) { - return 'default'; - } - return 'confirm'; - }, }, methods: { onShowInstructionsClick() { @@ -103,46 +80,51 @@ export default { onTokenReset(token) { this.currentRegistrationToken = token; - this.$refs.runnerRegistrationDropdown.hide(true); + this.$refs.runnerRegistrationDropdown.close(); + }, + onCopy() { + this.$refs.runnerRegistrationDropdown.close(); }, }, }; </script> <template> - <gl-dropdown + <gl-disclosure-dropdown ref="runnerRegistrationDropdown" - menu-class="gl-w-auto!" - :text="dropdownText" - :toggle-class="dropdownToggleClass" - :variant="dropdownVariant" - :category="dropdownCategory" + :toggle-text="actionText" + toggle-class="gl-px-3!" + variant="default" + category="tertiary" v-bind="$attrs" + icon="ellipsis_v" + text-sr-only + no-caret > - <template v-if="isDeprecated" #button-content> - <span class="gl-sr-only">{{ actionText }}</span> - <gl-icon name="ellipsis_v" /> - </template> <gl-dropdown-form class="gl-p-4!"> - <registration-token input-id="token-value" :value="currentRegistrationToken"> - <template v-if="isDeprecated" #label-description> + <registration-token input-id="token-value" :value="currentRegistrationToken" @copy="onCopy"> + <template #label-description> <gl-icon name="warning" class="gl-text-orange-500" /> <span class="gl-text-secondary"> - {{ s__('Runners|Support for registration tokens is deprecated') }} + {{ $options.i18n.supportForRegistrationTokensDeprecated }} </span> </template> </registration-token> </gl-dropdown-form> - <gl-dropdown-divider /> - <gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick"> - {{ $options.i18n.showInstallationInstructions }} - <runner-instructions-modal - ref="runnerInstructionsModal" - :registration-token="currentRegistrationToken" - data-testid="runner-instructions-modal" - /> - </gl-dropdown-item> - <gl-dropdown-divider /> - <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" /> - </gl-dropdown> + <gl-disclosure-dropdown-group bordered> + <gl-disclosure-dropdown-item @action="onShowInstructionsClick"> + <template #list-item> + {{ $options.i18n.showInstallationInstructions }} + <runner-instructions-modal + ref="runnerInstructionsModal" + :registration-token="currentRegistrationToken" + data-testid="runner-instructions-modal" + /> + </template> + </gl-disclosure-dropdown-item> + </gl-disclosure-dropdown-group> + <gl-disclosure-dropdown-group bordered> + <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" /> + </gl-disclosure-dropdown-group> + </gl-disclosure-dropdown> </template> diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_token.vue b/app/assets/javascripts/ci/runner/components/registration/registration_token.vue index b196bccf66f..339c92a427f 100644 --- a/app/assets/javascripts/ci/runner/components/registration/registration_token.vue +++ b/app/assets/javascripts/ci/runner/components/registration/registration_token.vue @@ -31,6 +31,7 @@ export default { onCopy() { // value already in the clipboard, simply notify the user this.$toast?.show(s__('Runners|Registration token copied!')); + this.$emit('copy'); }, }, I18N_COPY_BUTTON_TITLE: s__('Runners|Copy registration token'), diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue index 6ce88fc54de..47ca3ed6227 100644 --- a/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue +++ b/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue @@ -1,5 +1,5 @@ <script> -import { GlDropdownItem, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui'; +import { GlDisclosureDropdownItem, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui'; import { createAlert } from '~/alert'; import { TYPENAME_GROUP, TYPENAME_PROJECT } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; @@ -19,7 +19,7 @@ export default { name: 'RunnerRegistrationTokenReset', i18n, components: { - GlDropdownItem, + GlDisclosureDropdownItem, GlLoadingIcon, GlModal, }, @@ -124,18 +124,20 @@ export default { }; </script> <template> - <gl-dropdown-item v-gl-modal="$options.modalId"> - {{ __('Reset registration token') }} - <gl-modal - size="sm" - :modal-id="$options.modalId" - :action-primary="actionPrimary" - :action-secondary="actionSecondary" - :title="$options.i18n.modalTitle" - @primary="handleModalPrimary" - > - <p>{{ $options.i18n.modalCopy }}</p> - </gl-modal> - <gl-loading-icon v-if="loading" inline /> - </gl-dropdown-item> + <gl-disclosure-dropdown-item v-gl-modal="$options.modalId"> + <template #list-item> + {{ __('Reset registration token') }} + <gl-modal + size="sm" + :modal-id="$options.modalId" + :action-primary="actionPrimary" + :action-secondary="actionSecondary" + :title="$options.i18n.modalTitle" + @primary="handleModalPrimary" + > + <p>{{ $options.i18n.modalCopy }}</p> + </gl-modal> + <gl-loading-icon v-if="loading" inline /> + </template> + </gl-disclosure-dropdown-item> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_delete_action.vue b/app/assets/javascripts/ci/runner/components/runner_delete_action.vue new file mode 100644 index 00000000000..db8133c1ccb --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_delete_action.vue @@ -0,0 +1,126 @@ +<script> +import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql'; +import { createAlert } from '~/alert'; +import { sprintf, s__ } from '~/locale'; +import { captureException } from '~/ci/runner/sentry_utils'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { I18N_DELETED_TOAST } from '../constants'; +import RunnerDeleteModal from './runner_delete_modal.vue'; + +/** + * Component that wraps a delete GraphQL mutation for the + * runner, given its id. + * + * You can use the slot to define a presentation for the + * delete action, like a button or dropdown item. + * + * Usage: + * + * ```vue + * <runner-delete-action + * #default="{ loading, onClick }" + * :runner="runner" + * @done="onDeleted" + * > + * <button :disabled="loading" @click="onClick"> Delete! </button> + * </runner-pause-action> + * ``` + * + */ +export default { + name: 'RunnerDeleteAction', + components: { + RunnerDeleteModal, + }, + props: { + runner: { + type: Object, + required: true, + validator: (runner) => { + return runner?.id && runner?.shortSha; + }, + }, + }, + emits: ['done'], + data() { + return { + loading: false, + }; + }, + computed: { + runnerId() { + return getIdFromGraphQLId(this.runner.id); + }, + runnerName() { + return `#${this.runnerId} (${this.runner.shortSha})`; + }, + runnerManagersCount() { + return this.runner.managers?.count || 0; + }, + runnerDeleteModalId() { + return `delete-runner-modal-${this.runnerId}`; + }, + }, + methods: { + onClick() { + this.$refs.modal.show(); + }, + async onDelete() { + // "loading" stays "true" until this row is removed, + // should only change back if the operation fails. + this.loading = true; + try { + await this.$apollo.mutate({ + mutation: runnerDeleteMutation, + variables: { + input: { + id: this.runner.id, + }, + }, + update: (cache, { data }) => { + const { errors } = data.runnerDelete; + + if (errors?.length) { + this.onError(new Error(errors.join(' '))); + return; + } + + this.$emit('done', { + message: sprintf(I18N_DELETED_TOAST, { name: this.runnerName }), + }); + + // Remove deleted runner from the cache + const cacheId = cache.identify(this.runner); + cache.evict({ id: cacheId }); + cache.gc(); + }, + }); + } catch (e) { + this.onError(e); + } + }, + onError(error) { + this.loading = false; + const { message } = error; + const title = sprintf(s__('Runners|Runner %{runnerName} failed to delete'), { + runnerName: this.runnerName, + }); + + createAlert({ title, message }); + captureException({ error, component: this.$options.name }); + }, + }, +}; +</script> +<template> + <div> + <slot :loading="loading" :on-click="onClick"></slot> + <runner-delete-modal + ref="modal" + :modal-id="runnerDeleteModalId" + :runner-name="runnerName" + :managers-count="runnerManagersCount" + @primary="onDelete" + /> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/components/runner_delete_button.vue b/app/assets/javascripts/ci/runner/components/runner_delete_button.vue index 3560521e8d7..d228a022032 100644 --- a/app/assets/javascripts/ci/runner/components/runner_delete_button.vue +++ b/app/assets/javascripts/ci/runner/components/runner_delete_button.vue @@ -1,30 +1,21 @@ <script> -import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; -import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql'; -import { createAlert } from '~/alert'; -import { sprintf, s__ } from '~/locale'; -import { captureException } from '~/ci/runner/sentry_utils'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { I18N_DELETE_RUNNER, I18N_DELETED_TOAST } from '../constants'; -import RunnerDeleteModal from './runner_delete_modal.vue'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { I18N_DELETE_RUNNER } from '../constants'; +import RunnerDeleteAction from './runner_delete_action.vue'; export default { name: 'RunnerDeleteButton', components: { GlButton, - RunnerDeleteModal, + RunnerDeleteAction, }, directives: { GlTooltip: GlTooltipDirective, - GlModal: GlModalDirective, }, props: { runner: { type: Object, required: true, - validator: (runner) => { - return runner?.id && runner?.shortSha; - }, }, compact: { type: Boolean, @@ -39,17 +30,11 @@ export default { }; }, computed: { - runnerId() { - return getIdFromGraphQLId(this.runner.id); - }, - runnerName() { - return `#${this.runnerId} (${this.runner.shortSha})`; - }, - runnerManagersCount() { - return this.runner.managers?.count || 0; - }, - runnerDeleteModalId() { - return `delete-runner-modal-${this.runnerId}`; + buttonContent() { + if (this.compact) { + return null; + } + return I18N_DELETE_RUNNER; }, icon() { if (this.compact) { @@ -57,12 +42,6 @@ export default { } return ''; }, - buttonContent() { - if (this.compact) { - return null; - } - return I18N_DELETE_RUNNER; - }, buttonClass() { // Ensure a square button is shown when compact: true. // Without this class we will have distorted/rectangular button. @@ -78,83 +57,36 @@ export default { return null; }, tooltip() { - // Only show basic "delete" tooltip when compact. - // Also prevent a "sticky" tooltip: If this button is - // loading, mouseout listeners don't run leaving the tooltip stuck - if (this.compact && !this.deleting) { + if (this.compact) { return I18N_DELETE_RUNNER; } return ''; }, }, methods: { - async onDelete() { - // Deleting stays "true" until this row is removed, - // should only change back if the operation fails. - this.deleting = true; - try { - await this.$apollo.mutate({ - mutation: runnerDeleteMutation, - variables: { - input: { - id: this.runner.id, - }, - }, - update: (cache, { data }) => { - const { errors } = data.runnerDelete; - - if (errors?.length) { - this.onError(new Error(errors.join(' '))); - return; - } - - this.$emit('deleted', { - message: sprintf(I18N_DELETED_TOAST, { name: this.runnerName }), - }); - - // Remove deleted runner from the cache - const cacheId = cache.identify(this.runner); - cache.evict({ id: cacheId }); - cache.gc(); - }, - }); - } catch (e) { - this.onError(e); - } - }, - onError(error) { - this.deleting = false; - const { message } = error; - const title = sprintf(s__('Runners|Runner %{runnerName} failed to delete'), { - runnerName: this.runnerName, - }); - - createAlert({ title, message }); - captureException({ error, component: this.$options.name }); + onDone(event) { + this.$emit('deleted', event); }, }, }; </script> <template> - <div v-gl-tooltip="tooltip" class="btn-group"> - <gl-button - v-gl-modal="runnerDeleteModalId" - :aria-label="ariaLabel" - :icon="icon" - :class="buttonClass" - :loading="deleting" - variant="danger" - category="secondary" - v-bind="$attrs" - > - {{ buttonContent }} - </gl-button> - <runner-delete-modal - :modal-id="runnerDeleteModalId" - :runner-name="runnerName" - :managers-count="runnerManagersCount" - @primary="onDelete" - /> - </div> + <runner-delete-action class="btn-group" :runner="runner" @done="onDone"> + <template #default="{ loading, onClick }"> + <gl-button + v-gl-tooltip="loading ? '' : tooltip" + :aria-label="ariaLabel" + :icon="icon" + :class="buttonClass" + :loading="loading" + variant="danger" + category="secondary" + v-bind="$attrs" + @click="onClick" + > + {{ buttonContent }} + </gl-button> + </template> + </runner-delete-action> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_delete_disclosure_dropdown_item.vue b/app/assets/javascripts/ci/runner/components/runner_delete_disclosure_dropdown_item.vue new file mode 100644 index 00000000000..0a81974a6d0 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_delete_disclosure_dropdown_item.vue @@ -0,0 +1,38 @@ +<script> +import { GlDisclosureDropdownItem } from '@gitlab/ui'; +import { I18N_DELETE } from '../constants'; +import RunnerDeleteAction from './runner_delete_action.vue'; + +export default { + name: 'RunnerDeleteDisclosureDropdownItem', + components: { + GlDisclosureDropdownItem, + RunnerDeleteAction, + }, + props: { + runner: { + type: Object, + required: true, + }, + }, + emits: ['deleted'], + methods: { + onDone(event) { + this.$emit('deleted', event); + }, + }, + I18N_DELETE, +}; +</script> + +<template> + <runner-delete-action :runner="runner" @done="onDone"> + <template #default="{ onClick }"> + <gl-disclosure-dropdown-item @action="onClick"> + <template #list-item> + <span class="gl-text-red-500">{{ $options.I18N_DELETE }}</span> + </template> + </gl-disclosure-dropdown-item> + </template> + </runner-delete-action> +</template> diff --git a/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue b/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue index 93f79fd67ea..124ac0b4e73 100644 --- a/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue +++ b/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue @@ -52,6 +52,9 @@ export default { }, }, methods: { + show() { + this.$refs.modal.show(); + }, onPrimary() { this.$refs.modal.hide(); }, diff --git a/app/assets/javascripts/ci/runner/components/runner_detail.vue b/app/assets/javascripts/ci/runner/components/runner_detail.vue index 9e8055a8432..496985ff7ac 100644 --- a/app/assets/javascripts/ci/runner/components/runner_detail.vue +++ b/app/assets/javascripts/ci/runner/components/runner_detail.vue @@ -40,12 +40,12 @@ export default { <template> <div class="gl-display-contents"> - <dt class="gl-mb-5 gl-mr-6 gl-max-w-26"> + <dt class="gl-mb-5 gl-mr-6 gl-max-w-26" data-testid="label-slot"> <template v-if="label || $scopedSlots.label"> <slot name="label">{{ label }}</slot> </template> </dt> - <dd class="gl-mb-5"> + <dd class="gl-mb-5" data-testid="value-slot"> <template v-if="value || $scopedSlots.value"> <slot name="value">{{ value }}</slot> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_edit_button.vue b/app/assets/javascripts/ci/runner/components/runner_edit_button.vue index 33e0acaf5c0..b4efd72b082 100644 --- a/app/assets/javascripts/ci/runner/components/runner_edit_button.vue +++ b/app/assets/javascripts/ci/runner/components/runner_edit_button.vue @@ -9,15 +9,23 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + props: { + href: { + type: String, + required: false, + default: null, + }, + }, I18N_EDIT, }; </script> <template> <gl-button + v-if="href" v-gl-tooltip="$options.I18N_EDIT" - v-bind="$attrs" :aria-label="$options.I18N_EDIT" + :href="href" icon="pencil" v-on="$listeners" /> diff --git a/app/assets/javascripts/ci/runner/components/runner_edit_disclosure_dropdown_item.vue b/app/assets/javascripts/ci/runner/components/runner_edit_disclosure_dropdown_item.vue new file mode 100644 index 00000000000..d0dcc04c3dc --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_edit_disclosure_dropdown_item.vue @@ -0,0 +1,29 @@ +<script> +import { GlDisclosureDropdownItem } from '@gitlab/ui'; + +import { I18N_EDIT } from '../constants'; + +export default { + name: 'RunnerEditDisclosureDropdownItem', + components: { + GlDisclosureDropdownItem, + }, + props: { + href: { + type: String, + required: false, + default: null, + }, + }, + computed: { + item() { + return { text: I18N_EDIT, href: this.href }; + }, + }, + I18N_EDIT, +}; +</script> + +<template> + <gl-disclosure-dropdown-item v-if="href" :item="item" /> +</template> diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue index f46e894bf2e..55a33ef2074 100644 --- a/app/assets/javascripts/ci/runner/components/runner_header.vue +++ b/app/assets/javascripts/ci/runner/components/runner_header.vue @@ -32,31 +32,29 @@ export default { }; </script> <template> - <div - class="gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-py-5" - > - <div> + <div class="gl-py-5"> + <div class="gl-display-flex gl-justify-content-space-between"> <h1 class="gl-font-size-h-display gl-my-0">{{ name }}</h1> - <div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-mt-3"> - <runner-status-badge :contacted-at="runner.contactedAt" :status="runner.status" /> - <runner-type-badge :type="runner.runnerType" /> - <span v-if="runner.createdAt"> - <gl-sprintf :message="__('%{locked} created %{timeago}')"> - <template #locked> - <gl-icon - v-if="runner.locked" - v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION" - name="lock" - :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION" - /> - </template> - <template #timeago> - <time-ago :time="runner.createdAt" /> - </template> - </gl-sprintf> - </span> - </div> + <slot name="actions"></slot> + </div> + <div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-flex-wrap gl-mt-3"> + <runner-status-badge :contacted-at="runner.contactedAt" :status="runner.status" /> + <runner-type-badge :type="runner.runnerType" /> + <span v-if="runner.createdAt"> + <gl-sprintf :message="__('%{locked} created %{timeago}')"> + <template #locked> + <gl-icon + v-if="runner.locked" + v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION" + name="lock" + :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION" + /> + </template> + <template #timeago> + <time-ago :time="runner.createdAt" /> + </template> + </gl-sprintf> + </span> </div> - <div class="gl-display-flex gl-gap-3 gl-flex-wrap"><slot name="actions"></slot></div> </div> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_header_actions.vue b/app/assets/javascripts/ci/runner/components/runner_header_actions.vue new file mode 100644 index 00000000000..bc6f184bd4d --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_header_actions.vue @@ -0,0 +1,80 @@ +<script> +import { GlDisclosureDropdown } from '@gitlab/ui'; + +import RunnerDeleteButton from './runner_delete_button.vue'; +import RunnerEditButton from './runner_edit_button.vue'; +import RunnerPauseButton from './runner_pause_button.vue'; + +import RunnerEditDisclosureDropdownItem from './runner_edit_disclosure_dropdown_item.vue'; +import RunnerPauseDisclosureDropdownItem from './runner_pause_disclosure_dropdown_item.vue'; +import RunnerDeleteDisclosureDropdownItem from './runner_delete_disclosure_dropdown_item.vue'; + +export default { + name: 'RunnerHeaderActions', + components: { + GlDisclosureDropdown, + + RunnerDeleteButton, + RunnerEditButton, + RunnerPauseButton, + + RunnerEditDisclosureDropdownItem, + RunnerPauseDisclosureDropdownItem, + RunnerDeleteDisclosureDropdownItem, + }, + props: { + runner: { + type: Object, + required: true, + }, + editPath: { + type: String, + required: false, + default: null, + }, + }, + computed: { + canUpdate() { + return this.runner.userPermissions?.updateRunner; + }, + canDelete() { + return this.runner.userPermissions?.deleteRunner; + }, + }, + methods: { + onDeleted(event) { + this.$emit('deleted', event); + }, + }, +}; +</script> + +<template> + <div v-if="canUpdate || canDelete"> + <!-- sm and up screens --> + <div class="gl-display-none gl-sm-display-flex gl-gap-3"> + <runner-edit-button v-if="canUpdate" :href="editPath" /> + <runner-pause-button v-if="canUpdate" :runner="runner" /> + <runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" /> + </div> + + <!-- xs screens --> + <div class="gl-sm-display-none"> + <gl-disclosure-dropdown + icon="ellipsis_v" + :toggle-text="s__('Runner|Runner actions')" + text-sr-only + category="tertiary" + no-caret + > + <runner-edit-disclosure-dropdown-item v-if="canUpdate" :href="editPath" /> + <runner-pause-disclosure-dropdown-item v-if="canUpdate" :runner="runner" /> + <runner-delete-disclosure-dropdown-item + v-if="canDelete" + :runner="runner" + @deleted="onDeleted" + /> + </gl-disclosure-dropdown> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue index d2836962a97..a4a489074c3 100644 --- a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue +++ b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue @@ -11,7 +11,6 @@ import { I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS, I18N_CONTACT_ADMIN_TO_REGISTER, - I18N_FOLLOW_REGISTRATION_INSTRUCTIONS, I18N_NO_RESULTS, I18N_EDIT_YOUR_SEARCH, } from '~/ci/runner/constants'; @@ -44,15 +43,6 @@ export default { default: null, }, }, - computed: { - shouldShowCreateRunnerWorkflow() { - // create_runner_workflow_for_admin or create_runner_workflow_for_namespace - return ( - this.glFeatures?.createRunnerWorkflowForAdmin || - this.glFeatures?.createRunnerWorkflowForNamespace - ); - }, - }, modalId: 'runners-empty-state-instructions-modal', svgHeight: 145, EMPTY_STATE_SVG_URL, @@ -63,7 +53,6 @@ export default { I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS, I18N_CONTACT_ADMIN_TO_REGISTER, - I18N_FOLLOW_REGISTRATION_INSTRUCTIONS, I18N_NO_RESULTS, I18N_EDIT_YOUR_SEARCH, }; @@ -85,39 +74,22 @@ export default { > <template #description> {{ $options.I18N_RUNNERS_ARE_AGENTS }} - <template v-if="shouldShowCreateRunnerWorkflow"> - <gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK"> - <template #link="{ content }"> - <gl-link :href="newRunnerPath">{{ content }}</gl-link> - </template> - </gl-sprintf> - <template v-if="registrationToken"> - <br /> - <gl-link v-gl-modal="$options.modalId">{{ - $options.I18N_STILL_USING_REGISTRATION_TOKENS - }}</gl-link> - <runner-instructions-modal - :modal-id="$options.modalId" - :registration-token="registrationToken" - /> - </template> - <template v-if="!newRunnerPath && !registrationToken"> - {{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }} - </template> - </template> - <gl-sprintf - v-else-if="registrationToken" - :message="$options.I18N_FOLLOW_REGISTRATION_INSTRUCTIONS" - > + <gl-sprintf v-if="newRunnerPath" :message="$options.I18N_CREATE_RUNNER_LINK"> <template #link="{ content }"> - <gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link> - <runner-instructions-modal - :modal-id="$options.modalId" - :registration-token="registrationToken" - /> + <gl-link :href="newRunnerPath">{{ content }}</gl-link> </template> </gl-sprintf> - <template v-else> + <template v-if="registrationToken"> + <br /> + <gl-link v-gl-modal="$options.modalId">{{ + $options.I18N_STILL_USING_REGISTRATION_TOKENS + }}</gl-link> + <runner-instructions-modal + :modal-id="$options.modalId" + :registration-token="registrationToken" + /> + </template> + <template v-if="!newRunnerPath && !registrationToken"> {{ $options.I18N_CONTACT_ADMIN_TO_REGISTER }} </template> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_pause_action.vue b/app/assets/javascripts/ci/runner/components/runner_pause_action.vue new file mode 100644 index 00000000000..184d6a83381 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_pause_action.vue @@ -0,0 +1,89 @@ +<script> +import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql'; +import { createAlert } from '~/alert'; +import { captureException } from '~/ci/runner/sentry_utils'; + +/** + * Renderless component that wraps a GraphQL pause mutation for the + * runner, given its id and current "paused" value. + * + * You can use the slot to define a presentation for the delete action, + * like a button or dropdown item. + + * Usage: + * + * ```vue + * <runner-pause-action + * #default="{ loading, onClick }" + * :runner="runner" + * @done="onToggled" + * > + * <button :disabled="loading" @click="onClick">{{ runner.paused ? 'Go!' : 'Stop!' }}</button> + * </runner-pause-action> + * ``` + * + */ +export default { + name: 'RunnerPauseAction', + props: { + runner: { + type: Object, + required: true, + }, + compact: { + type: Boolean, + required: false, + default: false, + }, + }, + emits: ['done'], + data() { + return { + loading: false, + }; + }, + methods: { + async onClick() { + this.loading = true; + try { + const input = { + id: this.runner.id, + paused: !this.runner.paused, + }; + + const { + data: { + runnerUpdate: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: runnerTogglePausedMutation, + variables: { + input, + }, + }); + + if (errors && errors.length) { + throw new Error(errors.join(' ')); + } + this.$emit('done'); + } catch (e) { + this.onError(e); + } finally { + this.loading = false; + } + }, + onError(error) { + const { message } = error; + + createAlert({ message }); + captureException({ error, component: this.$options.name }); + }, + }, + render() { + return this.$scopedSlots.default({ + onClick: this.onClick, + loading: this.loading, + }); + }, +}; +</script> diff --git a/app/assets/javascripts/ci/runner/components/runner_pause_button.vue b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue index d16c8f98bad..15bb54027c7 100644 --- a/app/assets/javascripts/ci/runner/components/runner_pause_button.vue +++ b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue @@ -1,14 +1,14 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; -import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql'; -import { createAlert } from '~/alert'; -import { captureException } from '~/ci/runner/sentry_utils'; -import { I18N_PAUSE, I18N_PAUSE_TOOLTIP, I18N_RESUME, I18N_RESUME_TOOLTIP } from '../constants'; + +import { I18N_RESUME, I18N_PAUSE, I18N_PAUSE_TOOLTIP, I18N_RESUME_TOOLTIP } from '../constants'; +import RunnerPauseAction from './runner_pause_action.vue'; export default { name: 'RunnerPauseButton', components: { GlButton, + RunnerPauseAction, }, directives: { GlTooltip: GlTooltipDirective, @@ -25,96 +25,47 @@ export default { }, }, emits: ['toggledPaused'], - data() { - return { - updating: false, - }; - }, computed: { isPaused() { return this.runner.paused; }, + tooltip() { + return this.isPaused ? I18N_RESUME_TOOLTIP : I18N_PAUSE_TOOLTIP; + }, icon() { return this.isPaused ? 'play' : 'pause'; }, label() { return this.isPaused ? I18N_RESUME : I18N_PAUSE; }, - buttonContent() { - if (this.compact) { - return null; - } - return this.label; - }, ariaLabel() { if (this.compact) { return this.label; } return null; }, - tooltip() { - // Prevent a "sticky" tooltip: If this button is disabled, - // mouseout listeners don't run leaving the tooltip stuck - if (!this.updating) { - return this.isPaused ? I18N_RESUME_TOOLTIP : I18N_PAUSE_TOOLTIP; - } - return ''; - }, - }, - methods: { - async onToggle() { - this.updating = true; - try { - const input = { - id: this.runner.id, - paused: !this.isPaused, - }; - - const { - data: { - runnerUpdate: { errors }, - }, - } = await this.$apollo.mutate({ - mutation: runnerTogglePausedMutation, - variables: { - input, - }, - }); - - if (errors && errors.length) { - throw new Error(errors.join(' ')); - } - this.$emit('toggledPaused'); - } catch (e) { - this.onError(e); - } finally { - this.updating = false; + buttonContent() { + if (this.compact) { + return null; } - }, - onError(error) { - const { message } = error; - - createAlert({ message }); - captureException({ error, component: this.$options.name }); + return this.label; }, }, }; </script> <template> - <gl-button - v-gl-tooltip="tooltip" - v-bind="$attrs" - :aria-label="ariaLabel" - :icon="icon" - :loading="updating" - @click="onToggle" - v-on="$listeners" - > - <!-- - Use <template v-if> to ensure a square button is shown when compact: true. - Sending empty content will still show a distorted/rectangular button. - --> - <template v-if="buttonContent">{{ buttonContent }}</template> - </gl-button> + <runner-pause-action :runner="runner" @done="$emit('toggledPaused')"> + <template #default="{ loading, onClick }"> + <gl-button + v-gl-tooltip="loading ? '' : tooltip" + :icon="icon" + :aria-label="ariaLabel" + :loading="loading" + @click="onClick" + > + <template v-if="buttonContent">{{ buttonContent }}</template> + </gl-button> + </template> + </runner-pause-action> </template> diff --git a/app/assets/javascripts/ci/runner/components/runner_pause_disclosure_dropdown_item.vue b/app/assets/javascripts/ci/runner/components/runner_pause_disclosure_dropdown_item.vue new file mode 100644 index 00000000000..3dd5e227a4a --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_pause_disclosure_dropdown_item.vue @@ -0,0 +1,34 @@ +<script> +import { GlDisclosureDropdownItem } from '@gitlab/ui'; + +import { I18N_RESUME, I18N_PAUSE } from '../constants'; +import RunnerPauseAction from './runner_pause_action.vue'; + +export default { + name: 'RunnerPauseDisclosureDropdownItem', + components: { + GlDisclosureDropdownItem, + RunnerPauseAction, + }, + props: { + runner: { + type: Object, + required: true, + }, + }, + emits: ['toggledPaused'], + computed: { + item() { + return { text: this.runner.paused ? I18N_RESUME : I18N_PAUSE }; + }, + }, +}; +</script> + +<template> + <runner-pause-action :runner="runner" @done="$emit('toggledPaused')"> + <template #default="{ onClick }"> + <gl-disclosure-dropdown-item :item="item" @action="onClick" /> + </template> + </runner-pause-action> +</template> diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js index 40841696ead..203f97876de 100644 --- a/app/assets/javascripts/ci/runner/constants.js +++ b/app/assets/javascripts/ci/runner/constants.js @@ -1,4 +1,5 @@ import { __, s__ } from '~/locale'; +import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility'; export const RUNNER_TYPENAME = 'CiRunner'; // __typename @@ -90,6 +91,7 @@ export const I18N_PAUSED_DESCRIPTION = s__('Runners|Not accepting jobs'); export const I18N_RESUME = __('Resume'); export const I18N_RESUME_TOOLTIP = s__('Runners|Resume accepting jobs'); +export const I18N_DELETE = s__('Runners|Delete'); export const I18N_DELETE_RUNNER = s__('Runners|Delete runner'); export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted'); @@ -117,9 +119,6 @@ export const I18N_STILL_USING_REGISTRATION_TOKENS = s__('Runners|Still using reg export const I18N_CONTACT_ADMIN_TO_REGISTER = s__( 'Runners|To register new runners, contact your administrator.', ); -export const I18N_FOLLOW_REGISTRATION_INSTRUCTIONS = s__( - 'Runners|Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.', -); // No runners found export const I18N_NO_RESULTS = s__('Runners|No results found'); @@ -271,12 +270,10 @@ export const DEFAULT_PLATFORM = LINUX_PLATFORM; // Runner docs are in a separate repository and are not shipped with GitLab // they are rendered as external URLs. -export const INSTALL_HELP_URL = 'https://docs.gitlab.com/runner/install'; -export const EXECUTORS_HELP_URL = 'https://docs.gitlab.com/runner/executors/'; -export const SERVICE_COMMANDS_HELP_URL = - 'https://docs.gitlab.com/runner/commands/#service-related-commands'; -export const CHANGELOG_URL = 'https://gitlab.com/gitlab-org/gitlab-runner/blob/main/CHANGELOG.md'; -export const DOCKER_HELP_URL = 'https://docs.gitlab.com/runner/install/docker.html'; -export const KUBERNETES_HELP_URL = 'https://docs.gitlab.com/runner/install/kubernetes.html'; -export const RUNNER_MANAGERS_HELP_URL = - 'https://docs.gitlab.com/runner/fleet_scaling/#workers-executors-and-autoscaling-capabilities'; +export const INSTALL_HELP_URL = `${DOCS_URL}/runner/install`; +export const EXECUTORS_HELP_URL = `${DOCS_URL}/runner/executors/`; +export const SERVICE_COMMANDS_HELP_URL = `${DOCS_URL}/runner/commands/#service-related-commands`; +export const CHANGELOG_URL = `https://gitlab.com/gitlab-org/gitlab-runner/blob/main/CHANGELOG.md`; +export const DOCKER_HELP_URL = `${DOCS_URL}/runner/install/docker.html`; +export const KUBERNETES_HELP_URL = `${DOCS_URL}/runner/install/kubernetes.html`; +export const RUNNER_MANAGERS_HELP_URL = `${DOCS_URL}/runner/fleet_scaling/#workers-executors-and-autoscaling-capabilities`; diff --git a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql index c0b888e758b..7ad9605d0a4 100644 --- a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql @@ -6,7 +6,6 @@ fragment ListItemShared on CiRunner { runnerType shortSha version - ipAddress paused locked jobCount @@ -22,8 +21,11 @@ fragment ListItemShared on CiRunner { updateRunner deleteRunner } - managers { + managers(first: 1) { count + nodes { + ipAddress + } } groups(first: 1) { nodes { diff --git a/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue b/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue index e885cf45c5a..4b570db772f 100644 --- a/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue +++ b/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue @@ -4,10 +4,8 @@ import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { visitUrl } from '~/lib/utils/url_utility'; -import RunnerDeleteButton from '../components/runner_delete_button.vue'; -import RunnerEditButton from '../components/runner_edit_button.vue'; -import RunnerPauseButton from '../components/runner_pause_button.vue'; import RunnerHeader from '../components/runner_header.vue'; +import RunnerHeaderActions from '../components/runner_header_actions.vue'; import RunnerDetailsTabs from '../components/runner_details_tabs.vue'; import { I18N_FETCH_ERROR } from '../constants'; @@ -18,10 +16,8 @@ import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_lo export default { name: 'GroupRunnerShowApp', components: { - RunnerDeleteButton, - RunnerEditButton, - RunnerPauseButton, RunnerHeader, + RunnerHeaderActions, RunnerDetailsTabs, }, props: { @@ -85,9 +81,11 @@ export default { <div> <runner-header v-if="runner" :runner="runner"> <template #actions> - <runner-edit-button v-if="canUpdate && editGroupRunnerPath" :href="editGroupRunnerPath" /> - <runner-pause-button v-if="canUpdate" :runner="runner" /> - <runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" /> + <runner-header-actions + :runner="runner" + :edit-path="editGroupRunnerPath" + @deleted="onDeleted" + /> </template> </runner-header> diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue index 74523bc335f..71584c40a38 100644 --- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue @@ -155,10 +155,6 @@ export default { isSearchFiltered() { return isSearchFiltered(this.search); }, - shouldShowCreateRunnerWorkflow() { - // create_runner_workflow_for_namespace feature flag - return this.glFeatures.createRunnerWorkflowForNamespace; - }, }, watch: { search: { @@ -231,11 +227,7 @@ export default { /> <div class="gl-w-full gl-md-w-auto gl-display-flex"> - <gl-button - v-if="shouldShowCreateRunnerWorkflow && newRunnerPath" - :href="newRunnerPath" - variant="confirm" - > + <gl-button v-if="newRunnerPath" :href="newRunnerPath" variant="confirm"> {{ s__('Runners|New group runner') }} </gl-button> <registration-dropdown @@ -243,7 +235,7 @@ export default { class="gl-ml-3" :registration-token="registrationToken" :type="$options.GROUP_TYPE" - right + placement="right" /> </div> </div> |