diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
commit | 43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch) | |
tree | dceebdc68925362117480a5d672bcff122fb625b /app/assets/javascripts/ci/runner | |
parent | 20c84b99005abd1c82101dfeff264ac50d2df211 (diff) |
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/ci/runner')
54 files changed, 1736 insertions, 189 deletions
diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue index 5401c7c1c28..e4d47fba464 100644 --- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue +++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue @@ -1,67 +1,61 @@ <script> -import { GlSprintf, GlLink, GlModalDirective } from '@gitlab/ui'; -import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated +import { s__ } from '~/locale'; + +import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue'; +import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue'; import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue'; -import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; -import { DEFAULT_PLATFORM, DEFAULT_ACCESS_LEVEL } from '../constants'; +import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue'; +import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, INSTANCE_TYPE } from '../constants'; +import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; export default { name: 'AdminNewRunnerApp', components: { - GlLink, - GlSprintf, - RunnerInstructionsModal, + RegistrationCompatibilityAlert, + RegistrationFeedbackBanner, RunnerPlatformsRadioGroup, - RunnerFormFields, - }, - directives: { - GlModal: GlModalDirective, - }, - props: { - legacyRegistrationToken: { - type: String, - required: true, - }, + RunnerCreateForm, }, data() { return { platform: DEFAULT_PLATFORM, - runner: { - description: '', - maintenanceNote: '', - paused: false, - accessLevel: DEFAULT_ACCESS_LEVEL, - runUntagged: false, - tagList: '', - maximumTimeout: ' ', - }, }; }, - modalId: 'runners-legacy-registration-instructions-modal', + methods: { + onSaved(runner) { + const params = { [PARAM_KEY_PLATFORM]: this.platform }; + const ephemeralRegisterUrl = setUrlParams(params, runner.ephemeralRegisterUrl); + + saveAlertToLocalStorage({ + message: s__('Runners|Runner created.'), + variant: VARIANT_SUCCESS, + }); + redirectTo(ephemeralRegisterUrl); // eslint-disable-line import/no-deprecated + }, + onError(error) { + createAlert({ message: error.message }); + }, + }, + INSTANCE_TYPE, }; </script> <template> <div> + <registration-feedback-banner /> + <h1 class="gl-font-size-h2">{{ s__('Runners|New instance runner') }}</h1> + + <registration-compatibility-alert :alert-key="$options.INSTANCE_TYPE" /> + <p> - <gl-sprintf - :message=" - s__( - 'Runners|Create an instance runner to generate a command that registers the runner with all its configurations. %{linkStart}Prefer to use a registration token to create a runner?%{linkEnd}', - ) - " - > - <template #link="{ content }"> - <gl-link v-gl-modal="$options.modalId" data-testid="legacy-instructions-link">{{ - content - }}</gl-link> - <runner-instructions-modal - :modal-id="$options.modalId" - :registration-token="legacyRegistrationToken" - /> - </template> - </gl-sprintf> + {{ + s__( + 'Runners|Create an instance runner to generate a command that registers the runner with all its configurations.', + ) + }} </p> <hr aria-hidden="true" /> @@ -73,6 +67,6 @@ export default { <hr aria-hidden="true" /> - <runner-form-fields v-model="runner" /> + <runner-create-form :runner-type="$options.INSTANCE_TYPE" @saved="onSaved" @error="onError" /> </div> </template> diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/index.js b/app/assets/javascripts/ci/runner/admin_new_runner/index.js index 502d9d33b4d..434c1197f71 100644 --- a/app/assets/javascripts/ci/runner/admin_new_runner/index.js +++ b/app/assets/javascripts/ci/runner/admin_new_runner/index.js @@ -12,8 +12,6 @@ export const initAdminNewRunner = (selector = '#js-admin-new-runner') => { return null; } - const { legacyRegistrationToken } = el.dataset; - const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), }); @@ -22,11 +20,7 @@ export const initAdminNewRunner = (selector = '#js-admin-new-runner') => { el, apolloProvider, render(h) { - return h(AdminNewRunnerApp, { - props: { - legacyRegistrationToken, - }, - }); + return h(AdminNewRunnerApp); }, }); }; diff --git a/app/assets/javascripts/ci/runner/admin_register_runner/admin_register_runner_app.vue b/app/assets/javascripts/ci/runner/admin_register_runner/admin_register_runner_app.vue new file mode 100644 index 00000000000..cd38dc07157 --- /dev/null +++ b/app/assets/javascripts/ci/runner/admin_register_runner/admin_register_runner_app.vue @@ -0,0 +1,69 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { getParameterByName, updateHistory, mergeUrlParams } from '~/lib/utils/url_utility'; +import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM } from '../constants'; +import RegistrationInstructions from '../components/registration/registration_instructions.vue'; +import PlatformsDrawer from '../components/registration/platforms_drawer.vue'; + +export default { + name: 'AdminRegisterRunnerApp', + components: { + GlButton, + RegistrationInstructions, + PlatformsDrawer, + }, + props: { + runnerId: { + type: String, + required: true, + }, + runnersPath: { + type: String, + required: true, + }, + }, + data() { + return { + platform: getParameterByName(PARAM_KEY_PLATFORM) || DEFAULT_PLATFORM, + isDrawerOpen: false, + }; + }, + watch: { + platform(platform) { + updateHistory({ + url: mergeUrlParams({ [PARAM_KEY_PLATFORM]: platform }, window.location.href), + }); + }, + }, + methods: { + onSelectPlatform(platform) { + this.platform = platform; + }, + onToggleDrawer(val = !this.isDrawerOpen) { + this.isDrawerOpen = val; + }, + }, +}; +</script> +<template> + <div> + <registration-instructions + :runner-id="runnerId" + :platform="platform" + @toggleDrawer="onToggleDrawer" + > + <template #runner-list-name>{{ s__('Runners|Admin area › Runners') }}</template> + </registration-instructions> + + <platforms-drawer + :platform="platform" + :open="isDrawerOpen" + @selectPlatform="onSelectPlatform" + @close="onToggleDrawer(false)" + /> + + <gl-button :href="runnersPath" variant="confirm">{{ + s__('Runners|Go to runners page') + }}</gl-button> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/admin_register_runner/index.js b/app/assets/javascripts/ci/runner/admin_register_runner/index.js new file mode 100644 index 00000000000..bd43a5e8ce9 --- /dev/null +++ b/app/assets/javascripts/ci/runner/admin_register_runner/index.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage'; +import AdminRegisterRunnerApp from './admin_register_runner_app.vue'; + +Vue.use(VueApollo); + +export const initAdminRegisterRunner = (selector = '#js-admin-register-runner') => { + showAlertFromLocalStorage(); + + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { runnerId, runnersPath } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(AdminRegisterRunnerApp, { + props: { + runnerId, + runnersPath, + }, + }); + }, + }); +}; 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 8d4303778af..668a55d2437 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 @@ -1,8 +1,8 @@ <script> -import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; -import { redirectTo } from '~/lib/utils/url_utility'; +import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated import RunnerDeleteButton from '../components/runner_delete_button.vue'; import RunnerEditButton from '../components/runner_edit_button.vue'; @@ -71,7 +71,7 @@ export default { }, onDeleted({ message }) { saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS }); - redirectTo(this.runnersPath); + redirectTo(this.runnersPath); // eslint-disable-line import/no-deprecated }, }, }; 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 ce2c511ddd4..4d88feebe53 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 @@ -1,6 +1,6 @@ <script> import { GlButton, GlLink } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { updateHistory } from '~/lib/utils/url_utility'; import { fetchPolicies } from '~/lib/graphql'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -54,7 +54,6 @@ export default { RunnerJobStatusBadge, }, mixins: [glFeatureFlagMixin()], - inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'], props: { newRunnerPath: { type: String, @@ -128,8 +127,8 @@ export default { return isSearchFiltered(this.search); }, shouldShowCreateRunnerWorkflow() { - // create_runner_workflow feature flag - return this.glFeatures.createRunnerWorkflow; + // create_runner_workflow_for_admin feature flag + return this.glFeatures.createRunnerWorkflowForAdmin; }, }, watch: { @@ -193,16 +192,17 @@ export default { nav-class="gl-border-none!" /> - <gl-button v-if="shouldShowCreateRunnerWorkflow" :href="newRunnerPath" variant="confirm"> - {{ s__('Runners|New instance runner') }} - </gl-button> - <registration-dropdown - v-else - class="gl-w-full gl-sm-w-auto gl-mr-auto" - :registration-token="registrationToken" - :type="$options.INSTANCE_TYPE" - right - /> + <div class="gl-w-full gl-md-w-auto gl-display-flex"> + <gl-button v-if="shouldShowCreateRunnerWorkflow" :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 + /> + </div> </div> <runner-filtered-search-bar @@ -219,8 +219,6 @@ export default { :registration-token="registrationToken" :is-search-filtered="isSearchFiltered" :new-runner-path="newRunnerPath" - :svg-path="emptyStateSvgPath" - :filtered-svg-path="emptyStateFilteredSvgPath" /> <template v-else> <runner-list diff --git a/app/assets/javascripts/ci/runner/admin_runners/index.js b/app/assets/javascripts/ci/runner/admin_runners/index.js index 881dc3613e9..54eb37f8c90 100644 --- a/app/assets/javascripts/ci/runner/admin_runners/index.js +++ b/app/assets/javascripts/ci/runner/admin_runners/index.js @@ -35,8 +35,6 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { registrationToken, onlineContactTimeoutSecs, staleTimeoutSecs, - emptyStateSvgPath, - emptyStateFilteredSvgPath, } = el.dataset; const { cacheConfig, typeDefs, localMutations } = createLocalState(); @@ -53,8 +51,6 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { localMutations, onlineContactTimeoutSecs, staleTimeoutSecs, - emptyStateSvgPath, - emptyStateFilteredSvgPath, }, render(h) { return h(AdminRunnersApp, { 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 97dfbe1a051..f24fb5575ae 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,8 @@ <script> import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { sprintf, __ } 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'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import RunnerName from '../runner_name.vue'; @@ -14,6 +16,7 @@ import { I18N_VERSION_LABEL, I18N_LAST_CONTACT_LABEL, I18N_CREATED_AT_LABEL, + I18N_CREATED_AT_BY_LABEL, } from '../../constants'; import RunnerSummaryField from './runner_summary_field.vue'; @@ -28,6 +31,7 @@ export default { RunnerTypeBadge, RunnerUpgradeStatusIcon: () => import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'), + UserAvatarLink, TooltipOnTruncate, }, directives: { @@ -43,6 +47,16 @@ export default { jobCount() { return formatJobCount(this.runner.jobCount); }, + createdBy() { + return this.runner?.createdBy; + }, + createdByImgAlt() { + const name = this.createdBy?.name; + if (name) { + return sprintf(__("%{name}'s avatar"), { name }); + } + return null; + }, }, i18n: { I18N_NO_DESCRIPTION, @@ -50,13 +64,14 @@ export default { I18N_VERSION_LABEL, I18N_LAST_CONTACT_LABEL, I18N_CREATED_AT_LABEL, + I18N_CREATED_AT_BY_LABEL, }, }; </script> <template> <div> - <div> + <div class="gl-mb-3"> <slot :runner="runner" name="runner-name"> <runner-name :runner="runner" /> </slot> @@ -69,22 +84,23 @@ export default { <runner-type-badge :type="runner.runnerType" size="sm" class="gl-vertical-align-middle" /> </div> - <div class="gl-ml-auto gl-display-inline-flex gl-max-w-full gl-py-2"> - <div class="gl-flex-shrink-0"> - <runner-upgrade-status-icon :runner="runner" /> - <gl-sprintf v-if="runner.version" :message="$options.i18n.I18N_VERSION_LABEL"> - <template #version>{{ runner.version }}</template> - </gl-sprintf> - </div> - <div class="gl-text-secondary gl-mx-2" aria-hidden="true">·</div> + <div class="gl-mb-3 gl-ml-auto gl-display-inline-flex gl-max-w-full"> + <template v-if="runner.version"> + <div class="gl-flex-shrink-0"> + <runner-upgrade-status-icon :runner="runner" /> + <gl-sprintf :message="$options.i18n.I18N_VERSION_LABEL"> + <template #version>{{ runner.version }}</template> + </gl-sprintf> + </div> + <div class="gl-text-secondary gl-mx-2" aria-hidden="true">·</div> + </template> <tooltip-on-truncate - v-if="runner.description" class="gl-text-truncate gl-display-block" + :class="{ 'gl-text-secondary': !runner.description }" :title="runner.description" > - {{ runner.description }} + {{ runner.description || $options.i18n.I18N_NO_DESCRIPTION }} </tooltip-on-truncate> - <span v-else class="gl-text-secondary">{{ $options.i18n.I18N_NO_DESCRIPTION }}</span> </div> <div> @@ -106,14 +122,33 @@ export default { </runner-summary-field> <runner-summary-field icon="calendar"> - <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_LABEL"> - <template #timeAgo> - <time-ago v-if="runner.createdAt" :time="runner.createdAt" /> - </template> - </gl-sprintf> + <template v-if="createdBy"> + <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_BY_LABEL"> + <template #timeAgo> + <time-ago v-if="runner.createdAt" :time="runner.createdAt" /> + </template> + <template #avatar> + <user-avatar-link + :link-href="createdBy.webUrl" + :img-src="createdBy.avatarUrl" + img-css-classes="gl-vertical-align-top" + :img-size="16" + :img-alt="createdByImgAlt" + :tooltip-text="createdBy.username" + /> + </template> + </gl-sprintf> + </template> + <template v-else> + <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_LABEL"> + <template #timeAgo> + <time-ago v-if="runner.createdAt" :time="runner.createdAt" /> + </template> + </gl-sprintf> + </template> </runner-summary-field> </div> - <runner-tags class="gl-display-block gl-pt-2" :tag-list="runner.tagList" size="sm" /> + <runner-tags class="gl-display-block" :tag-list="runner.tagList" size="sm" /> </div> </template> diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue index 1bbbd55089a..742259ee491 100644 --- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue +++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue @@ -24,7 +24,7 @@ export default { </script> <template> - <div v-gl-tooltip="tooltip" class="gl-display-inline-block gl-text-secondary gl-my-2 gl-mr-2"> + <div v-gl-tooltip="tooltip" class="gl-display-inline-block gl-text-secondary gl-mb-3 gl-mr-4"> <gl-icon v-if="icon" :name="icon" /> <!-- display tooltip as a label for screen readers --> <span class="gl-sr-only">{{ tooltip }}</span> diff --git a/app/assets/javascripts/ci/runner/components/registration/cli_command.vue b/app/assets/javascripts/ci/runner/components/registration/cli_command.vue new file mode 100644 index 00000000000..95b135c83a7 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/cli_command.vue @@ -0,0 +1,42 @@ +<script> +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +export default { + components: { + ClipboardButton, + }, + props: { + prompt: { + type: String, + required: false, + default: '', + }, + command: { + type: [Array, String], + required: false, + default: '', + }, + }, + computed: { + lines() { + if (typeof this.command === 'string') { + return [this.command]; + } + return this.command; + }, + clipboard() { + return this.lines.join(''); + }, + }, +}; +</script> +<template> + <div class="gl-display-flex gl-gap-3 gl-align-items-flex-start"> + <!-- eslint-disable vue/require-v-for-key--> + <pre + class="gl-w-full" + ><span v-if="prompt" class="gl-user-select-none">{{ prompt }} </span><template v-for="line in lines">{{ line }}<br class="gl-user-select-none"/></template></pre> + <!-- eslint-enable vue/require-v-for-key--> + <clipboard-button :text="clipboard" :title="__('Copy')" /> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue b/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue new file mode 100644 index 00000000000..ff182c61ccf --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue @@ -0,0 +1,135 @@ +<script> +import { GlDrawer, GlFormGroup, GlFormSelect, GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; +import { getContentWrapperHeight } from '~/lib/utils/dom_utils'; +import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; + +import { + DEFAULT_PLATFORM, + MACOS_PLATFORM, + LINUX_PLATFORM, + WINDOWS_PLATFORM, + INSTALL_HELP_URL, +} from '../../constants'; +import { installScript, platformArchitectures } from './utils'; + +import CliCommand from './cli_command.vue'; + +export default { + components: { + GlDrawer, + GlFormGroup, + GlFormSelect, + GlIcon, + GlLink, + GlSprintf, + CliCommand, + }, + props: { + open: { + type: Boolean, + required: true, + }, + platform: { + type: String, + required: false, + default: DEFAULT_PLATFORM, + }, + }, + data() { + return { + selectedPlatform: this.platform, + selectedArchitecture: null, + }; + }, + computed: { + drawerHeightOffset() { + return getContentWrapperHeight('.content-wrapper'); + }, + architectureOptions() { + return platformArchitectures({ platform: this.selectedPlatform }); + }, + script() { + return installScript({ + platform: this.selectedPlatform, + architecture: this.selectedArchitecture, + }); + }, + }, + watch: { + selectedPlatform() { + this.selectedArchitecture = + this.architectureOptions.find((value) => value === this.selectedArchitecture) || + this.architectureOptions[0]; + + this.$emit('selectPlatform', this.selectedPlatform); + }, + }, + created() { + [this.selectedArchitecture] = this.architectureOptions; + }, + methods: { + onClose() { + this.$emit('close'); + }, + }, + platformOptions: [ + /* eslint-disable @gitlab/require-i18n-strings */ + { value: LINUX_PLATFORM, text: 'Linux' }, + { value: MACOS_PLATFORM, text: 'macOS' }, + { value: WINDOWS_PLATFORM, text: 'Windows' }, + /* eslint-enable @gitlab/require-i18n-strings */ + ], + INSTALL_HELP_URL, + DRAWER_Z_INDEX, +}; +</script> +<template> + <gl-drawer + :open="open" + :header-height="drawerHeightOffset" + :z-index="$options.DRAWER_Z_INDEX" + data-testid="runner-platforms-drawer" + @close="onClose" + > + <template #title> + <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24"> + {{ s__('Runners|Install GitLab Runner') }} + </h2> + </template> + <div> + <p>{{ s__('Runners|Select platform specifications to install GitLab Runner.') }}</p> + + <gl-form-group :label="s__('Runners|Environment')" label-for="runner-environment-select"> + <gl-form-select + id="runner-environment-select" + v-model="selectedPlatform" + :options="$options.platformOptions" + /> + </gl-form-group> + + <gl-form-group :label="s__('Runners|Architecture')" label-for="runner-architecture-select"> + <gl-form-select + id="runner-architecture-select" + v-model="selectedArchitecture" + :options="architectureOptions" + /> + </gl-form-group> + + <cli-command :command="script" /> + + <p> + <gl-sprintf + :message=" + s__('Runners|See more %{linkStart}installation methods and architectures%{linkEnd}.') + " + > + <template #link="{ content }"> + <gl-link :href="$options.INSTALL_HELP_URL"> + {{ content }} <gl-icon name="external-link" /> + </gl-link> + </template> + </gl-sprintf> + </p> + </div> + </gl-drawer> +</template> diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_compatibility_alert.vue b/app/assets/javascripts/ci/runner/components/registration/registration_compatibility_alert.vue new file mode 100644 index 00000000000..4ebb5754e61 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/registration_compatibility_alert.vue @@ -0,0 +1,50 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import DismissibleFeedbackAlert from '~/vue_shared/components/dismissible_feedback_alert.vue'; +import { s__ } from '~/locale'; +import { CHANGELOG_URL } from '../../constants'; + +export default { + name: 'RegistrationCompatibilityAlert', + components: { + GlLink, + GlSprintf, + DismissibleFeedbackAlert, + }, + props: { + alertKey: { + type: String, + required: true, + }, + }, + computed: { + alertFeatureName() { + return `new_runner_compatibility_${this.alertKey}`; + }, + }, + CHANGELOG_URL, + i18n: { + title: s__( + 'Runners|This registration process is only supported in GitLab Runner 15.10 or later', + ), + message: s__( + 'Runners|This registration process is not supported in GitLab Runner 15.9 or earlier and only available as an experimental feature in GitLab Runner 15.10 and 15.11. You should upgrade to %{linkStart}GitLab Runner 16.0%{linkEnd} or later to use a stable version of this registration process.', + ), + }, +}; +</script> + +<template> + <dismissible-feedback-alert + :feature-name="alertFeatureName" + class="gl-mb-4" + variant="warning" + :title="$options.i18n.title" + > + <gl-sprintf :message="$options.i18n.message"> + <template #link="{ content }"> + <gl-link :href="$options.CHANGELOG_URL" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </dismissible-feedback-alert> +</template> 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 212ad5fa5a0..2fdf8456615 100644 --- a/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue +++ b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue @@ -1,8 +1,17 @@ <script> -import { GlDropdown, GlDropdownForm, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; +import { GlDropdown, GlDropdownForm, GlDropdownItem, GlDropdownDivider, 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'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants'; +import { + INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, + I18N_REGISTER_INSTANCE_TYPE, + I18N_REGISTER_GROUP_TYPE, + I18N_REGISTER_PROJECT_TYPE, + I18N_REGISTER_RUNNER, +} from '../../constants'; import RegistrationToken from './registration_token.vue'; import RegistrationTokenResetDropdownItem from './registration_token_reset_dropdown_item.vue'; @@ -17,10 +26,12 @@ export default { GlDropdownForm, GlDropdownItem, GlDropdownDivider, + GlIcon, RegistrationToken, RunnerInstructionsModal, RegistrationTokenResetDropdownItem, }, + mixins: [glFeatureFlagMixin()], props: { registrationToken: { type: String, @@ -40,17 +51,49 @@ export default { }; }, computed: { - dropdownText() { + 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: - return s__('Runners|Register an instance runner'); + return I18N_REGISTER_INSTANCE_TYPE; case GROUP_TYPE: - return s__('Runners|Register a group runner'); + return I18N_REGISTER_GROUP_TYPE; case PROJECT_TYPE: - return s__('Runners|Register a project runner'); + return I18N_REGISTER_PROJECT_TYPE; default: - return s__('Runners|Register a runner'); + 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: { @@ -71,9 +114,26 @@ export default { ref="runnerRegistrationDropdown" menu-class="gl-w-auto!" :text="dropdownText" - variant="confirm" + :toggle-class="dropdownToggleClass" + :variant="dropdownVariant" + :category="dropdownCategory" v-bind="$attrs" > + <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> + <gl-icon name="warning" class="gl-text-orange-500" /> + <span class="gl-text-secondary"> + {{ s__('Runners|Support for registration tokens is deprecated') }} + </span> + </template> + </registration-token> + </gl-dropdown-form> + <gl-dropdown-divider /> <gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick"> {{ $options.i18n.showInstallationInstructions }} <runner-instructions-modal @@ -83,10 +143,6 @@ export default { /> </gl-dropdown-item> <gl-dropdown-divider /> - <gl-dropdown-form class="gl-p-4!"> - <registration-token input-id="token-value" :value="currentRegistrationToken" /> - </gl-dropdown-form> - <gl-dropdown-divider /> <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" /> </gl-dropdown> </template> diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue b/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue new file mode 100644 index 00000000000..fe19977f783 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue @@ -0,0 +1,41 @@ +<script> +import ILLUSTRATION_URL from '@gitlab/svgs/dist/illustrations/multi-editor_all_changes_committed_empty.svg?url'; +import { GlBanner } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; + +const FEEDBACK_ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/387993'; + +export default { + components: { + GlBanner, + UserCalloutDismisser, + }, + i18n: { + title: s__("Runners|We've made some changes and want your feedback"), + body: s__( + "Runners|We've been making improvements to how you register runners so that it's more secure and efficient. Tell us how we're doing.", + ), + button: s__('Runners|Add your feedback to this issue'), + }, + ILLUSTRATION_URL, + FEEDBACK_ISSUE_URL, +}; +</script> +<template> + <user-callout-dismisser feature-name="create_runner_workflow_banner"> + <template #default="{ dismiss, shouldShowCallout }"> + <gl-banner + v-if="shouldShowCallout" + class="gl-my-6" + :title="$options.i18n.title" + :svg-path="$options.ILLUSTRATION_URL" + :button-text="$options.i18n.button" + :button-link="$options.FEEDBACK_ISSUE_URL" + @close="dismiss" + > + <p>{{ $options.i18n.body }}</p> + </gl-banner> + </template> + </user-callout-dismisser> +</template> diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue new file mode 100644 index 00000000000..69021dde0e9 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue @@ -0,0 +1,256 @@ +<script> +import { GlIcon, GlLink, GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { createAlert } from '~/alert'; +import { s__, sprintf } from '~/locale'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants'; + +import runnerForRegistrationQuery from '../../graphql/register/runner_for_registration.query.graphql'; +import { + STATUS_ONLINE, + EXECUTORS_HELP_URL, + SERVICE_COMMANDS_HELP_URL, + RUNNER_REGISTRATION_POLLING_INTERVAL_MS, + I18N_FETCH_ERROR, + I18N_REGISTRATION_SUCCESS, +} from '../../constants'; +import { captureException } from '../../sentry_utils'; + +import CliCommand from './cli_command.vue'; +import { commandPrompt, registerCommand, runCommand } from './utils'; + +export default { + name: 'RegistrationInstructions', + components: { + GlIcon, + GlLink, + GlSkeletonLoader, + GlSprintf, + ClipboardButton, + CliCommand, + }, + props: { + runnerId: { + type: String, + required: true, + }, + platform: { + type: String, + required: true, + }, + }, + data() { + return { + runner: null, + token: null, + }; + }, + apollo: { + runner: { + query: runnerForRegistrationQuery, + variables() { + return { + id: convertToGraphQLId(TYPENAME_CI_RUNNER, this.runnerId), + }; + }, + manual: true, + result({ data }) { + if (data?.runner) { + const { ephemeralAuthenticationToken, ...runner } = data.runner; + this.runner = runner; + + // The token is available in the API for a limited amount of time + // preserve its original value if it is missing after polling. + this.token = ephemeralAuthenticationToken || this.token; + } + }, + error(error) { + createAlert({ message: I18N_FETCH_ERROR }); + captureException({ error, component: this.$options.name }); + }, + pollInterval() { + if (this.isRunnerOnline) { + // stop polling + return 0; + } + return RUNNER_REGISTRATION_POLLING_INTERVAL_MS; + }, + }, + }, + computed: { + loading() { + return this.$apollo.queries.runner.loading; + }, + description() { + return this.runner?.description; + }, + heading() { + if (this.description) { + return sprintf( + s__('Runners|Register "%{runnerDescription}" runner'), + { + runnerDescription: this.description, + }, + false, + ); + } + return s__('Runners|Register runner'); + }, + tokenMessage() { + if (this.token) { + return s__( + 'Runners|The %{boldStart}runner token%{boldEnd} %{token} displays %{boldStart}only for a short time%{boldEnd}, and is stored in the %{codeStart}config.toml%{codeEnd} after you register the runner. It will not be visible once the runner is registered.', + ); + } + return s__( + 'Runners|The %{boldStart}runner token%{boldEnd} is no longer visible, it is stored in the %{codeStart}config.toml%{codeEnd} if you have registered the runner.', + ); + }, + commandPrompt() { + return commandPrompt({ platform: this.platform }); + }, + registerCommand() { + return registerCommand({ + platform: this.platform, + token: this.token, + }); + }, + runCommand() { + return runCommand({ platform: this.platform }); + }, + isRunnerOnline() { + return this.runner?.status === STATUS_ONLINE; + }, + }, + created() { + window.addEventListener('beforeunload', this.onBeforeunload); + }, + destroyed() { + window.removeEventListener('beforeunload', this.onBeforeunload); + }, + methods: { + toggleDrawer() { + this.$emit('toggleDrawer'); + }, + onBeforeunload(event) { + if (this.isRunnerOnline) { + return undefined; + } + + const str = s__('Runners|You may lose access to the runner token if you leave this page.'); + event.preventDefault(); + // eslint-disable-next-line no-param-reassign + event.returnValue = str; // Chrome requires returnValue to be set + return str; + }, + }, + EXECUTORS_HELP_URL, + SERVICE_COMMANDS_HELP_URL, + I18N_REGISTRATION_SUCCESS, +}; +</script> +<template> + <div> + <h1 class="gl-font-size-h1">{{ heading }}</h1> + + <p> + <gl-sprintf + :message=" + s__( + 'Runners|GitLab Runner must be installed before you can register a runner. %{linkStart}How do I install GitLab Runner?%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link data-testid="runner-install-link" @click="toggleDrawer">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + + <section> + <h2 class="gl-font-size-h2">{{ s__('Runners|Step 1') }}</h2> + <p> + {{ + s__( + 'Runners|Copy and paste the following command into your command line to register the runner.', + ) + }} + </p> + <gl-skeleton-loader v-if="loading" /> + <template v-else> + <cli-command :prompt="commandPrompt" :command="registerCommand" /> + <p> + <gl-icon name="information-o" class="gl-text-blue-600!" /> + <gl-sprintf :message="tokenMessage"> + <template #token> + <code data-testid="runner-token">{{ token }}</code> + <clipboard-button + :text="token" + :title="__('Copy')" + size="small" + category="tertiary" + class="gl-border-none!" + /> + </template> + <template #bold="{ content }" + ><span class="gl-font-weight-bold">{{ content }}</span></template + > + <template #code="{ content }" + ><code>{{ content }}</code></template + > + </gl-sprintf> + </p> + </template> + </section> + <section> + <h2 class="gl-font-size-h2">{{ s__('Runners|Step 2') }}</h2> + <p> + <gl-sprintf + :message=" + s__( + 'Runners|Choose an executor when prompted by the command line. Executors run builds in different environments. %{linkStart}Not sure which one to select?%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link :href="$options.EXECUTORS_HELP_URL" target="_blank"> + {{ content }} <gl-icon name="external-link" /> + </gl-link> + </template> + </gl-sprintf> + </p> + </section> + <section> + <h2 class="gl-font-size-h2">{{ s__('Runners|Step 3 (optional)') }}</h2> + <p>{{ s__('Runners|Manually verify that the runner is available to pick up jobs.') }}</p> + <cli-command :prompt="commandPrompt" :command="runCommand" /> + <p> + <gl-sprintf + :message=" + s__( + 'Runners|This may not be needed if you manage your runner as a %{linkStart}system or user service%{linkEnd}.', + ) + " + > + <template #link="{ content }"> + <gl-link :href="$options.SERVICE_COMMANDS_HELP_URL" target="_blank"> + {{ content }} <gl-icon name="external-link" /> + </gl-link> + </template> + </gl-sprintf> + </p> + </section> + <section v-if="isRunnerOnline"> + <h2 class="gl-font-size-h2">🎉 {{ $options.I18N_REGISTRATION_SUCCESS }}</h2> + + <p class="gl-pl-6"> + <gl-sprintf :message="s__('Runners|To view the runner, go to %{runnerListName}.')"> + <template #runnerListName> + <span class="gl-font-weight-bold"><slot name="runner-list-name"></slot></span> + </template> + </gl-sprintf> + </p> + </section> + </div> +</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 6b4e6a929b7..b196bccf66f 100644 --- a/app/assets/javascripts/ci/runner/components/registration/registration_token.vue +++ b/app/assets/javascripts/ci/runner/components/registration/registration_token.vue @@ -45,5 +45,9 @@ export default { :copy-button-title="$options.I18N_COPY_BUTTON_TITLE" :form-input-group-props="formInputGroupProps" @copy="onCopy" - /> + > + <template v-for="slot in Object.keys($scopedSlots)" #[slot]> + <slot :name="slot"></slot> + </template> + </input-copy-toggle-visibility> </template> 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 ac2793654c8..6ce88fc54de 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,6 +1,6 @@ <script> import { GlDropdownItem, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { TYPENAME_GROUP, TYPENAME_PROJECT } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { __, s__ } from '~/locale'; @@ -73,13 +73,13 @@ export default { actionPrimary() { return { text: i18n.modalAction, - attributes: [{ variant: 'danger' }], + attributes: { variant: 'danger' }, }; }, actionSecondary() { return { text: i18n.modalCancel, - attributes: [{ variant: 'default' }], + attributes: { variant: 'default' }, }; }, }, diff --git a/app/assets/javascripts/ci/runner/components/registration/scripts/linux/install.sh b/app/assets/javascripts/ci/runner/components/registration/scripts/linux/install.sh new file mode 100644 index 00000000000..a8ba2592128 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/scripts/linux/install.sh @@ -0,0 +1,12 @@ +# Download the binary for your system +sudo curl -L --output /usr/local/bin/gitlab-runner ${GITLAB_CI_RUNNER_DOWNLOAD_LOCATION} + +# Give it permission to execute +sudo chmod +x /usr/local/bin/gitlab-runner + +# Create a GitLab Runner user +sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash + +# Install and run as a service +sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner +sudo gitlab-runner start diff --git a/app/assets/javascripts/ci/runner/components/registration/scripts/osx/install.sh b/app/assets/javascripts/ci/runner/components/registration/scripts/osx/install.sh new file mode 100644 index 00000000000..76c893bacfc --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/scripts/osx/install.sh @@ -0,0 +1,11 @@ +# Download the binary for your system +sudo curl --output /usr/local/bin/gitlab-runner ${GITLAB_CI_RUNNER_DOWNLOAD_LOCATION} + +# Give it permission to execute +sudo chmod +x /usr/local/bin/gitlab-runner + +# The rest of the commands execute as the user who will run the runner +# Register the runner (steps below), then run +cd ~ +gitlab-runner install +gitlab-runner start diff --git a/app/assets/javascripts/ci/runner/components/registration/scripts/windows/install.ps1 b/app/assets/javascripts/ci/runner/components/registration/scripts/windows/install.ps1 new file mode 100644 index 00000000000..019363fc3f7 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/scripts/windows/install.ps1 @@ -0,0 +1,13 @@ +# Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator +# Create a folder somewhere on your system, for example: C:\GitLab-Runner +New-Item -Path 'C:\GitLab-Runner' -ItemType Directory + +# Change to the folder +cd 'C:\GitLab-Runner' + +# Download binary +Invoke-WebRequest -Uri "${GITLAB_CI_RUNNER_DOWNLOAD_LOCATION}" -OutFile "gitlab-runner.exe" + +# Register the runner (steps below), then run +.\gitlab-runner.exe install +.\gitlab-runner.exe start diff --git a/app/assets/javascripts/ci/runner/components/registration/utils.js b/app/assets/javascripts/ci/runner/components/registration/utils.js new file mode 100644 index 00000000000..c8a75506c9c --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/utils.js @@ -0,0 +1,81 @@ +import { + DEFAULT_PLATFORM, + LINUX_PLATFORM, + MACOS_PLATFORM, + WINDOWS_PLATFORM, + DOWNLOAD_LOCATIONS, +} from '../../constants'; +import linuxInstall from './scripts/linux/install.sh?raw'; +import osxInstall from './scripts/osx/install.sh?raw'; +import windowsInstall from './scripts/windows/install.ps1?raw'; + +const OS = { + [LINUX_PLATFORM]: { + shell: 'bash', + commandPrompt: '$', + executable: 'gitlab-runner', + }, + [MACOS_PLATFORM]: { + shell: 'bash', + commandPrompt: '$', + executable: 'gitlab-runner', + }, + [WINDOWS_PLATFORM]: { + shell: 'powershell', + commandPrompt: '>', + executable: '.\\gitlab-runner.exe', + }, +}; + +export const commandPrompt = ({ platform }) => { + return (OS[platform] || OS[DEFAULT_PLATFORM]).commandPrompt; +}; + +export const executable = ({ platform }) => { + return (OS[platform] || OS[DEFAULT_PLATFORM]).executable; +}; + +export const registerCommand = ({ platform, url = gon.gitlab_url, token }) => { + const lines = [`${executable({ platform })} register`]; // eslint-disable-line @gitlab/require-i18n-strings + if (url) { + lines.push(` --url ${url}`); + } + if (token) { + lines.push(` --token ${token}`); + } + return lines; +}; + +export const runCommand = ({ platform }) => { + return `${executable({ platform })} run`; // eslint-disable-line @gitlab/require-i18n-strings +}; + +const importInstallScript = ({ platform = DEFAULT_PLATFORM }) => { + switch (platform) { + case LINUX_PLATFORM: + return linuxInstall; + case MACOS_PLATFORM: + return osxInstall; + case WINDOWS_PLATFORM: + return windowsInstall; + default: + return ''; + } +}; + +export const platformArchitectures = ({ platform }) => { + return DOWNLOAD_LOCATIONS[platform].map(({ arch }) => arch); +}; + +export const installScript = ({ platform, architecture }) => { + const downloadLocation = DOWNLOAD_LOCATIONS[platform].find(({ arch }) => arch === architecture) + .url; + + return importInstallScript({ platform }) + .replace( + // eslint-disable-next-line no-template-curly-in-string + '${GITLAB_CI_RUNNER_DOWNLOAD_LOCATION}', + downloadLocation, + ) + .trim(); +}; diff --git a/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue b/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue index 8dde3ac4e19..e7b26ec6d4e 100644 --- a/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue +++ b/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue @@ -1,6 +1,6 @@ <script> import { GlButton, GlModalDirective, GlModal, GlSprintf } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { __, s__, n__, sprintf } from '~/locale'; import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql'; import BulkRunnerDelete from '../graphql/list/bulk_runner_delete.mutation.graphql'; diff --git a/app/assets/javascripts/ci/runner/components/runner_create_form.vue b/app/assets/javascripts/ci/runner/components/runner_create_form.vue new file mode 100644 index 00000000000..6107b4dd3ea --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_create_form.vue @@ -0,0 +1,120 @@ +<script> +import { GlForm, GlButton } from '@gitlab/ui'; +import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue'; +import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql'; +import { modelToUpdateMutationVariables } from 'ee_else_ce/ci/runner/runner_update_form_utils'; +import { captureException } from '../sentry_utils'; +import { + RUNNER_TYPES, + DEFAULT_ACCESS_LEVEL, + PROJECT_TYPE, + GROUP_TYPE, + INSTANCE_TYPE, +} from '../constants'; + +export default { + name: 'RunnerCreateForm', + components: { + GlForm, + GlButton, + RunnerFormFields, + }, + props: { + runnerType: { + type: String, + required: true, + validator: (t) => RUNNER_TYPES.includes(t), + }, + groupId: { + type: String, + required: false, + default: null, + }, + projectId: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + saving: false, + runner: { + description: '', + maintenanceNote: '', + paused: false, + accessLevel: DEFAULT_ACCESS_LEVEL, + runUntagged: false, + tagList: '', + maximumTimeout: '', + }, + }; + }, + computed: { + mutationInput() { + const { input } = modelToUpdateMutationVariables(this.runner); + + if (this.runnerType === GROUP_TYPE) { + return { + ...input, + runnerType: GROUP_TYPE, + groupId: this.groupId, + }; + } + if (this.runnerType === PROJECT_TYPE) { + return { + ...input, + runnerType: PROJECT_TYPE, + projectId: this.projectId, + }; + } + return { + ...input, + runnerType: INSTANCE_TYPE, + }; + }, + }, + methods: { + async onSubmit() { + this.saving = true; + try { + const { + data: { + runnerCreate: { errors, runner }, + }, + } = await this.$apollo.mutate({ + mutation: runnerCreateMutation, + variables: { + input: this.mutationInput, + }, + }); + + if (errors?.length) { + this.$emit('error', new Error(errors.join(' '))); + } else { + this.onSuccess(runner); + } + } catch (error) { + captureException({ error, component: this.$options.name }); + this.$emit('error', error); + } finally { + this.saving = false; + } + }, + onSuccess(runner) { + this.$emit('saved', runner); + }, + }, +}; +</script> +<template> + <gl-form @submit.prevent="onSubmit"> + <runner-form-fields v-model="runner" /> + + <div class="gl-display-flex"> + <gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="saving"> + {{ __('Submit') }} + </gl-button> + </div> + </gl-form> +</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 f02e6bce5c3..020487fc727 100644 --- a/app/assets/javascripts/ci/runner/components/runner_delete_button.vue +++ b/app/assets/javascripts/ci/runner/components/runner_delete_button.vue @@ -1,7 +1,7 @@ <script> import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { sprintf, s__ } from '~/locale'; import { captureException } from '~/ci/runner/sentry_utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs.vue b/app/assets/javascripts/ci/runner/components/runner_jobs.vue index 9003eba3636..62e09346c2c 100644 --- a/app/assets/javascripts/ci/runner/components/runner_jobs.vue +++ b/app/assets/javascripts/ci/runner/components/runner_jobs.vue @@ -1,12 +1,13 @@ <script> import { GlSkeletonLoader } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import runnerJobsQuery from '../graphql/show/runner_jobs.query.graphql'; import { I18N_FETCH_ERROR, I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '../constants'; import { captureException } from '../sentry_utils'; import { getPaginationVariables } from '../utils'; import RunnerJobsTable from './runner_jobs_table.vue'; import RunnerPagination from './runner_pagination.vue'; +import RunnerJobsEmptyState from './runner_jobs_empty_state.vue'; export default { name: 'RunnerJobs', @@ -14,7 +15,9 @@ export default { GlSkeletonLoader, RunnerJobsTable, RunnerPagination, + RunnerJobsEmptyState, }, + props: { runner: { type: Object, @@ -75,7 +78,7 @@ export default { <gl-skeleton-loader /> </div> <runner-jobs-table v-else-if="jobs.items.length" :jobs="jobs.items" /> - <p v-else>{{ $options.I18N_NO_JOBS_FOUND }}</p> + <runner-jobs-empty-state v-else /> <runner-pagination :disabled="loading" :page-info="jobs.pageInfo" @input="onPaginationInput" /> </div> diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue new file mode 100644 index 00000000000..c30a824120d --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/runner_jobs_empty_state.vue @@ -0,0 +1,27 @@ +<script> +import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/pipelines_empty.svg?url'; + +import { GlEmptyState } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + i18n: { + title: s__('Runners|This runner has not run any jobs'), + description: s__( + 'Runners|Make sure the runner is online and available to run jobs (not paused). Jobs display here when the runner picks them up.', + ), + }, + components: { + GlEmptyState, + }, + EMPTY_STATE_SVG_URL, +}; +</script> + +<template> + <gl-empty-state :svg-path="$options.EMPTY_STATE_SVG_URL" :title="$options.i18n.title"> + <template #description> + <p>{{ $options.i18n.description }}</p> + </template> + </gl-empty-state> +</template> diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue index ebcda4f0ac3..5d8e9dcdee2 100644 --- a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue +++ b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue @@ -2,7 +2,7 @@ import { GlTableLite } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { durationTimeFormatted } from '~/lib/utils/datetime_utility'; +import { formatTime } from '~/lib/utils/datetime_utility'; import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue'; import RunnerTags from '~/ci/runner/components/runner_tags.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -50,11 +50,11 @@ export default { }, duration(job) { const { duration } = job; - return duration ? durationTimeFormatted(duration) : ''; + return duration ? formatTime(duration * 1000) : ''; }, queued(job) { const { queuedDuration } = job; - return queuedDuration ? durationTimeFormatted(queuedDuration) : ''; + return queuedDuration ? formatTime(queuedDuration * 1000) : ''; }, }, fields: [ 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 d2f7912fabb..8606c22db34 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 @@ -1,4 +1,7 @@ <script> +import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/pipelines_empty.svg?url'; +import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/magnifying-glass.svg?url'; + import { GlEmptyState, GlLink, GlSprintf, GlModalDirective } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; @@ -20,16 +23,6 @@ export default { required: false, default: false, }, - svgPath: { - type: String, - required: false, - default: '', - }, - filteredSvgPath: { - type: String, - required: false, - default: '', - }, registrationToken: { type: String, required: false, @@ -43,12 +36,18 @@ export default { }, computed: { shouldShowCreateRunnerWorkflow() { - // create_runner_workflow feature flag - return this.newRunnerPath && this.glFeatures?.createRunnerWorkflow; + // create_runner_workflow_for_admin or create_runner_workflow_for_namespace + return ( + this.newRunnerPath && + (this.glFeatures?.createRunnerWorkflowForAdmin || + this.glFeatures?.createRunnerWorkflowForNamespace) + ); }, }, modalId: 'runners-empty-state-instructions-modal', svgHeight: 145, + EMPTY_STATE_SVG_URL, + FILTERED_SVG_URL, }; </script> @@ -56,14 +55,14 @@ export default { <gl-empty-state v-if="isSearchFiltered" :title="s__('Runners|No results found')" - :svg-path="filteredSvgPath" + :svg-path="$options.FILTERED_SVG_URL" :svg-height="$options.svgHeight" :description="s__('Runners|Edit your search and try again')" /> <gl-empty-state v-else :title="s__('Runners|Get started with runners')" - :svg-path="svgPath" + :svg-path="$options.EMPTY_STATE_SVG_URL" :svg-height="$options.svgHeight" > <template v-if="registrationToken" #description> 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 2c80518e772..a27af232e97 100644 --- a/app/assets/javascripts/ci/runner/components/runner_pause_button.vue +++ b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue @@ -1,7 +1,7 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { captureException } from '~/ci/runner/sentry_utils'; import { I18N_PAUSE, I18N_PAUSE_TOOLTIP, I18N_RESUME, I18N_RESUME_TOOLTIP } from '../constants'; diff --git a/app/assets/javascripts/ci/runner/components/runner_platforms_radio.vue b/app/assets/javascripts/ci/runner/components/runner_platforms_radio.vue index d70c51e83f9..48c5d2f9721 100644 --- a/app/assets/javascripts/ci/runner/components/runner_platforms_radio.vue +++ b/app/assets/javascripts/ci/runner/components/runner_platforms_radio.vue @@ -30,6 +30,9 @@ export default { isChecked() { return this.value && this.value === this.checked; }, + imgClass() { + return 'gl-h-6 gl-mt-n2 gl-mr-2'; + }, }, methods: { onInput($event) { @@ -47,23 +50,22 @@ export default { <template> <div - class="runner-platforms-radio gl-display-flex gl-border gl-rounded-base gl-px-5 gl-py-6" + class="runner-platforms-radio gl-border gl-rounded-base gl-px-5 gl-pt-6 gl-pb-5" :class="{ 'gl-bg-blue-50 gl-border-blue-500': isChecked, 'gl-cursor-pointer': value }" @click="onInput(value)" > <gl-form-radio v-if="value" - class="gl-min-h-5" :checked="checked" :value="value" @input="onInput($event)" @change="onChange($event)" > - <img v-if="image" :src="image" aria-hidden="true" class="gl-h-5 gl-mr-2" /> + <img v-if="image" :src="image" :class="imgClass" aria-hidden="true" /> <span class="gl-font-weight-bold"><slot></slot></span> </gl-form-radio> - <div v-else class="gl-h-5"> - <img v-if="image" :src="image" aria-hidden="true" class="gl-h-5 gl-mr-2" /> + <div v-else class="gl-mb-3"> + <img v-if="image" :src="image" :class="imgClass" aria-hidden="true" /> <span class="gl-font-weight-bold"><slot></slot></span> </div> </div> diff --git a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue index 273226141d2..a49641194a7 100644 --- a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue +++ b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue @@ -1,6 +1,6 @@ <script> -import AWS_LOGO_URL from '@gitlab/svgs/dist/illustrations/logos/aws.svg?url'; import DOCKER_LOGO_URL from '@gitlab/svgs/dist/illustrations/third-party-logos/ci_cd-template-logos/docker.png'; +import LINUX_LOGO_URL from '@gitlab/svgs/dist/illustrations/third-party-logos/linux.svg?url'; import KUBERNETES_LOGO_URL from '@gitlab/svgs/dist/illustrations/logos/kubernetes.svg?url'; import { GlFormRadioGroup, GlIcon, GlLink } from '@gitlab/ui'; @@ -8,7 +8,6 @@ import { LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM, - AWS_PLATFORM, DOCKER_HELP_URL, KUBERNETES_HELP_URL, } from '../constants'; @@ -40,11 +39,10 @@ export default { }, }, LINUX_PLATFORM, + LINUX_LOGO_URL, MACOS_PLATFORM, WINDOWS_PLATFORM, - AWS_PLATFORM, - AWS_LOGO_URL, DOCKER_HELP_URL, DOCKER_LOGO_URL, KUBERNETES_HELP_URL, @@ -59,7 +57,11 @@ export default { <div class="gl-display-flex gl-flex-wrap gl-gap-5"> <!-- eslint-disable @gitlab/vue-require-i18n-strings --> - <runner-platforms-radio v-model="model" :value="$options.LINUX_PLATFORM"> + <runner-platforms-radio + v-model="model" + :image="$options.LINUX_LOGO_URL" + :value="$options.LINUX_PLATFORM" + > Linux </runner-platforms-radio> <runner-platforms-radio v-model="model" :value="$options.MACOS_PLATFORM"> @@ -72,20 +74,6 @@ export default { </div> <div class="gl-mt-3 gl-mb-6"> - <label>{{ s__('Runners|Cloud templates') }}</label> - <!-- eslint-disable @gitlab/vue-require-i18n-strings --> - <div class="gl-display-flex gl-flex-wrap gl-gap-5"> - <runner-platforms-radio - v-model="model" - :image="$options.AWS_LOGO_URL" - :value="$options.AWS_PLATFORM" - > - AWS - </runner-platforms-radio> - </div> - </div> - - <div class="gl-mt-3 gl-mb-6"> <label>{{ s__('Runners|Containers') }}</label> <div class="gl-display-flex gl-flex-wrap gl-gap-5"> diff --git a/app/assets/javascripts/ci/runner/components/runner_projects.vue b/app/assets/javascripts/ci/runner/components/runner_projects.vue index 4a6e90b44a9..4cfc57340f5 100644 --- a/app/assets/javascripts/ci/runner/components/runner_projects.vue +++ b/app/assets/javascripts/ci/runner/components/runner_projects.vue @@ -1,7 +1,7 @@ <script> import { GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; import { sprintf, formatNumber } from '~/locale'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import runnerProjectsQuery from '../graphql/show/runner_projects.query.graphql'; import { I18N_ASSIGNED_PROJECTS, diff --git a/app/assets/javascripts/ci/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue index a9790d06ca7..2d34c551d6d 100644 --- a/app/assets/javascripts/ci/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue @@ -13,8 +13,8 @@ import { modelToUpdateMutationVariables, runnerToModel, } from 'ee_else_ce/ci/runner/runner_update_form_utils'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; -import { redirectTo } from '~/lib/utils/url_utility'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated import { __ } from '~/locale'; import { captureException } from '~/ci/runner/sentry_utils'; import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; @@ -101,7 +101,7 @@ export default { }, onSuccess() { saveAlertToLocalStorage({ message: __('Changes saved.'), variant: VARIANT_SUCCESS }); - redirectTo(this.runnerPath); + redirectTo(this.runnerPath); // eslint-disable-line import/no-deprecated }, onError(message) { this.saving = false; diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue index 6e7c41885f8..1de7775090a 100644 --- a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue +++ b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue @@ -1,6 +1,6 @@ <script> import { GlFilteredSearchSuggestion, GlToken } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js index 318eb7e74bd..4e36a410a66 100644 --- a/app/assets/javascripts/ci/runner/constants.js +++ b/app/assets/javascripts/ci/runner/constants.js @@ -71,6 +71,12 @@ export const I18N_STALE_NEVER_CONTACTED_TOOLTIP = s__( 'Runners|Runner is stale; it has never contacted this instance', ); +// Registration dropdown +export const I18N_REGISTER_INSTANCE_TYPE = s__('Runners|Register an instance runner'); +export const I18N_REGISTER_GROUP_TYPE = s__('Runners|Register a group runner'); +export const I18N_REGISTER_PROJECT_TYPE = s__('Runners|Register a project runner'); +export const I18N_REGISTER_RUNNER = s__('Runners|Register a runner'); + // Actions export const I18N_EDIT = __('Edit'); @@ -93,6 +99,7 @@ export const I18N_LOCKED_RUNNER_DESCRIPTION = s__( export const I18N_VERSION_LABEL = s__('Runners|Version %{version}'); export const I18N_LAST_CONTACT_LABEL = s__('Runners|Last contact: %{timeAgo}'); export const I18N_CREATED_AT_LABEL = s__('Runners|Created %{timeAgo}'); +export const I18N_CREATED_AT_BY_LABEL = s__('Runners|Created %{timeAgo} by %{avatar}'); export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited'); export const I18N_ADMIN = s__('Runners|Administrator'); @@ -105,10 +112,15 @@ export const I18N_JOBS = s__('Runners|Jobs'); export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})'); export const I18N_FILTER_PROJECTS = s__('Runners|Filter projects'); export const I18N_CLEAR_FILTER_PROJECTS = __('Clear'); -export const I18N_NONE = __('None'); export const I18N_NO_JOBS_FOUND = s__('Runners|This runner has not run any jobs.'); export const I18N_NO_PROJECTS_FOUND = __('No projects found'); +// Runner registration + +export const I18N_REGISTRATION_SUCCESS = s__("Runners|You've created a new runner!"); + +export const RUNNER_REGISTRATION_POLLING_INTERVAL_MS = 2000; + // Styles export const RUNNER_TAG_BADGE_VARIANT = 'info'; @@ -129,11 +141,14 @@ export const PARAM_KEY_SORT = 'sort'; export const PARAM_KEY_AFTER = 'after'; export const PARAM_KEY_BEFORE = 'before'; +export const PARAM_KEY_PLATFORM = 'platform'; + // CiRunnerType export const INSTANCE_TYPE = 'INSTANCE_TYPE'; export const GROUP_TYPE = 'GROUP_TYPE'; export const PROJECT_TYPE = 'PROJECT_TYPE'; +export const RUNNER_TYPES = [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE]; // CiRunnerStatus @@ -180,11 +195,64 @@ export const GROUP_FILTERED_SEARCH_NAMESPACE = 'group_runners'; export const LINUX_PLATFORM = 'linux'; export const MACOS_PLATFORM = 'osx'; export const WINDOWS_PLATFORM = 'windows'; -export const AWS_PLATFORM = 'aws'; + +export const DOWNLOAD_LOCATIONS = { + [LINUX_PLATFORM]: [ + { + arch: 'amd64', + url: + 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64', + }, + { + arch: '386', + url: + 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386', + }, + { + arch: 'arm', + url: + 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm', + }, + { + arch: 'arm64', + url: + 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64', + }, + ], + [MACOS_PLATFORM]: [ + { + arch: 'amd64', + url: + 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64', + }, + { + arch: 'arm64', + url: + 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64', + }, + ], + [WINDOWS_PLATFORM]: [ + { + arch: 'amd64', + url: + 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe', + }, + { + arch: '386', + url: + 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe', + }, + ], +}; 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'; diff --git a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql index 29abddf84f5..d18b80511fb 100644 --- a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql @@ -10,5 +10,5 @@ fragment RunnerFieldsShared on CiRunner { maximumTimeout tagList createdAt - status(legacyMode: null) + status } 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 6f72509f599..4eebcd01be6 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 @@ -1,3 +1,5 @@ +#import "~/graphql_shared/fragments/user.fragment.graphql" + fragment ListItemShared on CiRunner { id description @@ -10,8 +12,11 @@ fragment ListItemShared on CiRunner { jobCount tagList createdAt + createdBy { + ...User + } contactedAt - status(legacyMode: null) + status jobExecutionStatus userPermissions { updateRunner diff --git a/app/assets/javascripts/ci/runner/graphql/new/runner_create.mutation.graphql b/app/assets/javascripts/ci/runner/graphql/new/runner_create.mutation.graphql new file mode 100644 index 00000000000..07236808dca --- /dev/null +++ b/app/assets/javascripts/ci/runner/graphql/new/runner_create.mutation.graphql @@ -0,0 +1,9 @@ +mutation runnerCreate($input: RunnerCreateInput!) { + runnerCreate(input: $input) { + runner { + id + ephemeralRegisterUrl + } + errors + } +} diff --git a/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql b/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql new file mode 100644 index 00000000000..f6cee807620 --- /dev/null +++ b/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql @@ -0,0 +1,8 @@ +query getRunnerForRegistration($id: CiRunnerID!) { + runner(id: $id) { + id + description + ephemeralAuthenticationToken + status + } +} diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql index b5689ff7687..bd53fb29bd0 100644 --- a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql @@ -15,7 +15,7 @@ fragment RunnerDetailsShared on CiRunner { jobCount tagList createdAt - status(legacyMode: null) + status contactedAt tokenExpiresAt version diff --git a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue new file mode 100644 index 00000000000..67d29daf66f --- /dev/null +++ b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue @@ -0,0 +1,83 @@ +<script> +import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated +import { s__ } from '~/locale'; + +import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue'; +import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue'; +import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue'; +import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue'; +import { DEFAULT_PLATFORM, GROUP_TYPE, PARAM_KEY_PLATFORM } from '../constants'; +import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; + +export default { + name: 'GroupNewRunnerApp', + components: { + RegistrationCompatibilityAlert, + RegistrationFeedbackBanner, + RunnerPlatformsRadioGroup, + RunnerCreateForm, + }, + props: { + groupId: { + type: String, + required: true, + }, + }, + data() { + return { + platform: DEFAULT_PLATFORM, + }; + }, + methods: { + onSaved(runner) { + const params = { [PARAM_KEY_PLATFORM]: this.platform }; + const ephemeralRegisterUrl = setUrlParams(params, runner.ephemeralRegisterUrl); + + saveAlertToLocalStorage({ + message: s__('Runners|Runner created.'), + variant: VARIANT_SUCCESS, + }); + redirectTo(ephemeralRegisterUrl); // eslint-disable-line import/no-deprecated + }, + onError(error) { + createAlert({ message: error.message }); + }, + }, + GROUP_TYPE, +}; +</script> + +<template> + <div> + <registration-feedback-banner /> + + <h1 class="gl-font-size-h2">{{ s__('Runners|New group runner') }}</h1> + + <registration-compatibility-alert :alert-key="groupId" /> + + <p> + {{ + s__( + 'Runners|Create a group runner to generate a command that registers the runner with all its configurations.', + ) + }} + </p> + + <hr aria-hidden="true" /> + + <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + {{ s__('Runners|Platform') }} + </h2> + <runner-platforms-radio-group v-model="platform" /> + + <hr aria-hidden="true" /> + + <runner-create-form + :runner-type="$options.GROUP_TYPE" + :group-id="groupId" + @saved="onSaved" + @error="onError" + /> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/group_new_runner/index.js b/app/assets/javascripts/ci/runner/group_new_runner/index.js new file mode 100644 index 00000000000..9e056081e03 --- /dev/null +++ b/app/assets/javascripts/ci/runner/group_new_runner/index.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import GroupNewRunnerApp from './group_new_runner_app.vue'; + +Vue.use(VueApollo); + +export const initGroupNewRunner = (selector = '#js-group-new-runner') => { + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { groupId } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(GroupNewRunnerApp, { + props: { + groupId, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/ci/runner/group_register_runner/group_register_runner_app.vue b/app/assets/javascripts/ci/runner/group_register_runner/group_register_runner_app.vue new file mode 100644 index 00000000000..533d31b70a3 --- /dev/null +++ b/app/assets/javascripts/ci/runner/group_register_runner/group_register_runner_app.vue @@ -0,0 +1,69 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { getParameterByName, updateHistory, mergeUrlParams } from '~/lib/utils/url_utility'; +import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM } from '../constants'; +import RegistrationInstructions from '../components/registration/registration_instructions.vue'; +import PlatformsDrawer from '../components/registration/platforms_drawer.vue'; + +export default { + name: 'GroupRegisterRunnerApp', + components: { + GlButton, + RegistrationInstructions, + PlatformsDrawer, + }, + props: { + runnerId: { + type: String, + required: true, + }, + runnersPath: { + type: String, + required: true, + }, + }, + data() { + return { + platform: getParameterByName(PARAM_KEY_PLATFORM) || DEFAULT_PLATFORM, + isDrawerOpen: false, + }; + }, + watch: { + platform(platform) { + updateHistory({ + url: mergeUrlParams({ [PARAM_KEY_PLATFORM]: platform }, window.location.href), + }); + }, + }, + methods: { + onSelectPlatform(platform) { + this.platform = platform; + }, + onToggleDrawer(val = !this.isDrawerOpen) { + this.isDrawerOpen = val; + }, + }, +}; +</script> +<template> + <div> + <registration-instructions + :runner-id="runnerId" + :platform="platform" + @toggleDrawer="onToggleDrawer" + > + <template #runner-list-name>{{ s__('Runners|Group area › Runners') }}</template> + </registration-instructions> + + <platforms-drawer + :platform="platform" + :open="isDrawerOpen" + @selectPlatform="onSelectPlatform" + @close="onToggleDrawer(false)" + /> + + <gl-button :href="runnersPath" variant="confirm">{{ + s__('Runners|Go to runners page') + }}</gl-button> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/group_register_runner/index.js b/app/assets/javascripts/ci/runner/group_register_runner/index.js new file mode 100644 index 00000000000..a00db8853a2 --- /dev/null +++ b/app/assets/javascripts/ci/runner/group_register_runner/index.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage'; +import GroupRegisterRunnerApp from './group_register_runner_app.vue'; + +Vue.use(VueApollo); + +export const initGroupRegisterRunner = (selector = '#js-group-register-runner') => { + showAlertFromLocalStorage(); + + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { runnerId, runnersPath } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(GroupRegisterRunnerApp, { + props: { + runnerId, + runnersPath, + }, + }); + }, + }); +}; 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 273a9aa823c..1318bf5a2e6 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 @@ -1,8 +1,8 @@ <script> -import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; -import { redirectTo } from '~/lib/utils/url_utility'; +import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated import RunnerDeleteButton from '../components/runner_delete_button.vue'; import RunnerEditButton from '../components/runner_edit_button.vue'; @@ -76,7 +76,7 @@ export default { }, onDeleted({ message }) { saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS }); - redirectTo(this.runnersPath); + redirectTo(this.runnersPath); // eslint-disable-line import/no-deprecated }, }, }; 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 e66a1c7b1aa..74523bc335f 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 @@ -1,6 +1,6 @@ <script> -import { GlLink } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { GlButton, GlLink } from '@gitlab/ui'; +import { createAlert } from '~/alert'; import { updateHistory } from '~/lib/utils/url_utility'; import { fetchPolicies } from '~/lib/graphql'; import { upgradeStatusTokenConfig } from 'ee_else_ce/ci/runner/components/search_tokens/upgrade_status_token_config'; @@ -42,6 +42,7 @@ import { captureException } from '../sentry_utils'; export default { name: 'GroupRunnersApp', components: { + GlButton, GlLink, RegistrationDropdown, RunnerFilteredSearchBar, @@ -56,8 +57,12 @@ export default { RunnerJobStatusBadge, }, mixins: [glFeatureFlagMixin()], - inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'], props: { + newRunnerPath: { + type: String, + required: false, + default: null, + }, registrationToken: { type: String, required: false, @@ -150,6 +155,10 @@ export default { isSearchFiltered() { return isSearchFiltered(this.search); }, + shouldShowCreateRunnerWorkflow() { + // create_runner_workflow_for_namespace feature flag + return this.glFeatures.createRunnerWorkflowForNamespace; + }, }, watch: { search: { @@ -207,7 +216,9 @@ export default { <template> <div> - <div class="gl-display-flex gl-align-items-center"> + <div + class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0" + > <runner-type-tabs ref="runner-type-tabs" v-model="search" @@ -219,15 +230,23 @@ export default { nav-class="gl-border-none!" /> - <registration-dropdown - v-if="registrationToken" - class="gl-ml-auto" - :registration-token="registrationToken" - :type="$options.GROUP_TYPE" - right - /> + <div class="gl-w-full gl-md-w-auto gl-display-flex"> + <gl-button + v-if="shouldShowCreateRunnerWorkflow && newRunnerPath" + :href="newRunnerPath" + variant="confirm" + > + {{ s__('Runners|New group runner') }} + </gl-button> + <registration-dropdown + v-if="registrationToken" + class="gl-ml-3" + :registration-token="registrationToken" + :type="$options.GROUP_TYPE" + right + /> + </div> </div> - <div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-gap-3" :class="$options.FILTER_CSS_CLASSES" @@ -250,8 +269,7 @@ export default { v-if="noRunnersFound" :registration-token="registrationToken" :is-search-filtered="isSearchFiltered" - :svg-path="emptyStateSvgPath" - :filtered-svg-path="emptyStateFilteredSvgPath" + :new-runner-path="newRunnerPath" /> <template v-else> <runner-list diff --git a/app/assets/javascripts/ci/runner/group_runners/index.js b/app/assets/javascripts/ci/runner/group_runners/index.js index 46514d5afe8..a5e2521ede5 100644 --- a/app/assets/javascripts/ci/runner/group_runners/index.js +++ b/app/assets/javascripts/ci/runner/group_runners/index.js @@ -18,12 +18,11 @@ export const initGroupRunners = (selector = '#js-group-runners') => { const { registrationToken, runnerInstallHelpPage, + newRunnerPath, groupId, groupFullPath, onlineContactTimeoutSecs, staleTimeoutSecs, - emptyStateSvgPath, - emptyStateFilteredSvgPath, } = el.dataset; const { cacheConfig, typeDefs, localMutations } = createLocalState(); @@ -41,14 +40,13 @@ export const initGroupRunners = (selector = '#js-group-runners') => { groupId, onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10), staleTimeoutSecs: parseInt(staleTimeoutSecs, 10), - emptyStateSvgPath, - emptyStateFilteredSvgPath, }, render(h) { return h(GroupRunnersApp, { props: { registrationToken, groupFullPath, + newRunnerPath, }, }); }, diff --git a/app/assets/javascripts/ci/runner/local_storage_alert/show_alert_from_local_storage.js b/app/assets/javascripts/ci/runner/local_storage_alert/show_alert_from_local_storage.js index d768a06494a..bad3ca6024e 100644 --- a/app/assets/javascripts/ci/runner/local_storage_alert/show_alert_from_local_storage.js +++ b/app/assets/javascripts/ci/runner/local_storage_alert/show_alert_from_local_storage.js @@ -7,7 +7,7 @@ export const showAlertFromLocalStorage = async () => { if (alertOptions) { try { - const { createAlert } = await import('~/flash'); + const { createAlert } = await import('~/alert'); createAlert(JSON.parse(alertOptions)); } catch { // ignore when the alert data cannot be parsed diff --git a/app/assets/javascripts/ci/runner/project_new_runner/index.js b/app/assets/javascripts/ci/runner/project_new_runner/index.js new file mode 100644 index 00000000000..44f1a0ffdab --- /dev/null +++ b/app/assets/javascripts/ci/runner/project_new_runner/index.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import ProjectNewRunnerApp from './project_new_runner_app.vue'; + +Vue.use(VueApollo); + +export const initProjectNewRunner = (selector = '#js-project-new-runner') => { + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { projectId } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(ProjectNewRunnerApp, { + props: { + projectId, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue new file mode 100644 index 00000000000..f0ae54c0232 --- /dev/null +++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue @@ -0,0 +1,83 @@ +<script> +import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { redirectTo, setUrlParams } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated +import { s__ } from '~/locale'; + +import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue'; +import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue'; +import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue'; +import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue'; +import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, PROJECT_TYPE } from '../constants'; +import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; + +export default { + name: 'ProjectNewRunnerApp', + components: { + RegistrationCompatibilityAlert, + RegistrationFeedbackBanner, + RunnerPlatformsRadioGroup, + RunnerCreateForm, + }, + props: { + projectId: { + type: String, + required: true, + }, + }, + data() { + return { + platform: DEFAULT_PLATFORM, + }; + }, + methods: { + onSaved(runner) { + const params = { [PARAM_KEY_PLATFORM]: this.platform }; + const ephemeralRegisterUrl = setUrlParams(params, runner.ephemeralRegisterUrl); + + saveAlertToLocalStorage({ + message: s__('Runners|Runner created.'), + variant: VARIANT_SUCCESS, + }); + redirectTo(ephemeralRegisterUrl); // eslint-disable-line import/no-deprecated + }, + onError(error) { + createAlert({ message: error.message }); + }, + }, + PROJECT_TYPE, +}; +</script> + +<template> + <div> + <registration-feedback-banner /> + + <h1 class="gl-font-size-h2">{{ s__('Runners|New project runner') }}</h1> + + <registration-compatibility-alert :alert-key="projectId" /> + + <p> + {{ + s__( + 'Runners|Create a project runner to generate a command that registers the runner with all its configurations.', + ) + }} + </p> + + <hr aria-hidden="true" /> + + <h2 class="gl-font-weight-normal gl-font-lg gl-my-5"> + {{ s__('Runners|Platform') }} + </h2> + <runner-platforms-radio-group v-model="platform" /> + + <hr aria-hidden="true" /> + + <runner-create-form + :runner-type="$options.PROJECT_TYPE" + :project-id="projectId" + @saved="onSaved" + @error="onError" + /> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/project_register_runner/index.js b/app/assets/javascripts/ci/runner/project_register_runner/index.js new file mode 100644 index 00000000000..63e88359ee7 --- /dev/null +++ b/app/assets/javascripts/ci/runner/project_register_runner/index.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage'; +import ProjectRegisterRunnerApp from './project_register_runner_app.vue'; + +Vue.use(VueApollo); + +export const initProjectRegisterRunner = (selector = '#js-project-register-runner') => { + showAlertFromLocalStorage(); + + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { runnerId, runnersPath } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(ProjectRegisterRunnerApp, { + props: { + runnerId, + runnersPath, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/ci/runner/project_register_runner/project_register_runner_app.vue b/app/assets/javascripts/ci/runner/project_register_runner/project_register_runner_app.vue new file mode 100644 index 00000000000..b3fad595c7e --- /dev/null +++ b/app/assets/javascripts/ci/runner/project_register_runner/project_register_runner_app.vue @@ -0,0 +1,69 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { getParameterByName, updateHistory, mergeUrlParams } from '~/lib/utils/url_utility'; +import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM } from '../constants'; +import RegistrationInstructions from '../components/registration/registration_instructions.vue'; +import PlatformsDrawer from '../components/registration/platforms_drawer.vue'; + +export default { + name: 'ProjectRegisterRunnerApp', + components: { + GlButton, + RegistrationInstructions, + PlatformsDrawer, + }, + props: { + runnerId: { + type: String, + required: true, + }, + runnersPath: { + type: String, + required: true, + }, + }, + data() { + return { + platform: getParameterByName(PARAM_KEY_PLATFORM) || DEFAULT_PLATFORM, + isDrawerOpen: false, + }; + }, + watch: { + platform(platform) { + updateHistory({ + url: mergeUrlParams({ [PARAM_KEY_PLATFORM]: platform }, window.location.href), + }); + }, + }, + methods: { + onSelectPlatform(platform) { + this.platform = platform; + }, + onToggleDrawer(val = !this.isDrawerOpen) { + this.isDrawerOpen = val; + }, + }, +}; +</script> +<template> + <div> + <registration-instructions + :runner-id="runnerId" + :platform="platform" + @toggleDrawer="onToggleDrawer" + > + <template #runner-list-name>{{ s__('Runners|Project › CI/CD Settings › Runners') }}</template> + </registration-instructions> + + <platforms-drawer + :platform="platform" + :open="isDrawerOpen" + @selectPlatform="onSelectPlatform" + @close="onToggleDrawer(false)" + /> + + <gl-button :href="runnersPath" variant="confirm">{{ + s__('Runners|Go to runners page') + }}</gl-button> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/project_runners/register/index.js b/app/assets/javascripts/ci/runner/project_runners/register/index.js new file mode 100644 index 00000000000..9986c93c918 --- /dev/null +++ b/app/assets/javascripts/ci/runner/project_runners/register/index.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue'; +import { PROJECT_TYPE } from '~/ci/runner/constants'; + +Vue.use(VueApollo); + +export const initProjectRunnersRegistrationDropdown = ( + selector = '#js-project-runner-registration-dropdown', +) => { + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { registrationToken, projectId } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + provide: { + projectId, + }, + render(h) { + return h(RegistrationDropdown, { + props: { + registrationToken, + type: PROJECT_TYPE, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/ci/runner/runner_edit/runner_edit_app.vue b/app/assets/javascripts/ci/runner/runner_edit/runner_edit_app.vue index 4593c9ae52b..843342b20df 100644 --- a/app/assets/javascripts/ci/runner/runner_edit/runner_edit_app.vue +++ b/app/assets/javascripts/ci/runner/runner_edit/runner_edit_app.vue @@ -1,5 +1,5 @@ <script> -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import RunnerHeader from '../components/runner_header.vue'; |