diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
commit | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch) | |
tree | fb69158581673816a8cd895f9d352dcb3c678b1e /app/assets/javascripts/runner/components | |
parent | d16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff) | |
download | gitlab-ce-a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4.tar.gz |
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/runner/components')
12 files changed, 1073 insertions, 9 deletions
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue new file mode 100644 index 00000000000..7f9f796bdee --- /dev/null +++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue @@ -0,0 +1,171 @@ +<script> +import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { __, s__ } from '~/locale'; +import deleteRunnerMutation from '~/runner/graphql/delete_runner.mutation.graphql'; +import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql'; + +const i18n = { + I18N_EDIT: __('Edit'), + I18N_PAUSE: __('Pause'), + I18N_RESUME: __('Resume'), + I18N_REMOVE: __('Remove'), + I18N_REMOVE_CONFIRMATION: s__('Runners|Are you sure you want to delete this runner?'), +}; + +export default { + components: { + GlButton, + GlButtonGroup, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + runner: { + type: Object, + required: true, + }, + }, + data() { + return { + updating: false, + deleting: false, + }; + }, + computed: { + runnerNumericalId() { + return getIdFromGraphQLId(this.runner.id); + }, + runnerUrl() { + // TODO implement using webUrl from the API + return `${gon.gitlab_url || ''}/admin/runners/${this.runnerNumericalId}`; + }, + isActive() { + return this.runner.active; + }, + toggleActiveIcon() { + return this.isActive ? 'pause' : 'play'; + }, + toggleActiveTitle() { + if (this.updating) { + // Prevent a "sticky" tooltip: If this button is disabled, + // mouseout listeners don't run leaving the tooltip stuck + return ''; + } + return this.isActive ? i18n.I18N_PAUSE : i18n.I18N_RESUME; + }, + deleteTitle() { + // Prevent a "sticky" tooltip: If element gets removed, + // mouseout listeners don't run and leaving the tooltip stuck + return this.deleting ? '' : i18n.I18N_REMOVE; + }, + }, + methods: { + async onToggleActive() { + this.updating = true; + // TODO In HAML iteration we had a confirmation modal via: + // data-confirm="_('Are you sure?')" + // this may not have to ported, this is an easily reversible operation + + try { + const toggledActive = !this.runner.active; + + const { + data: { + runnerUpdate: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: runnerUpdateMutation, + variables: { + input: { + id: this.runner.id, + active: toggledActive, + }, + }, + }); + + if (errors && errors.length) { + this.onError(new Error(errors[0])); + } + } catch (e) { + this.onError(e); + } finally { + this.updating = false; + } + }, + + async onDelete() { + // TODO Replace confirmation with gl-modal + // eslint-disable-next-line no-alert + if (!window.confirm(i18n.I18N_REMOVE_CONFIRMATION)) { + return; + } + + this.deleting = true; + try { + const { + data: { + runnerDelete: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: deleteRunnerMutation, + variables: { + input: { + id: this.runner.id, + }, + }, + awaitRefetchQueries: true, + refetchQueries: ['getRunners'], + }); + if (errors && errors.length) { + this.onError(new Error(errors[0])); + } + } catch (e) { + this.onError(e); + } finally { + this.deleting = false; + } + }, + + onError(error) { + // TODO Render errors when "delete" action is done + // `active` toggle would not fail due to user input. + throw error; + }, + }, + i18n, +}; +</script> + +<template> + <gl-button-group> + <gl-button + v-gl-tooltip.hover.viewport + :title="$options.i18n.I18N_EDIT" + :aria-label="$options.i18n.I18N_EDIT" + icon="pencil" + :href="runnerUrl" + data-testid="edit-runner" + /> + <gl-button + v-gl-tooltip.hover.viewport + :title="toggleActiveTitle" + :aria-label="toggleActiveTitle" + :icon="toggleActiveIcon" + :loading="updating" + data-testid="toggle-active-runner" + @click="onToggleActive" + /> + <gl-button + v-gl-tooltip.hover.viewport + :title="deleteTitle" + :aria-label="deleteTitle" + icon="close" + :loading="deleting" + variant="danger" + data-testid="delete-runner" + @click="onDelete" + /> + </gl-button-group> +</template> diff --git a/app/assets/javascripts/runner/components/cells/runner_name_cell.vue b/app/assets/javascripts/runner/components/cells/runner_name_cell.vue new file mode 100644 index 00000000000..797a3359147 --- /dev/null +++ b/app/assets/javascripts/runner/components/cells/runner_name_cell.vue @@ -0,0 +1,44 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; + +export default { + components: { + GlLink, + TooltipOnTruncate, + }, + props: { + runner: { + type: Object, + required: true, + }, + }, + computed: { + runnerNumericalId() { + return getIdFromGraphQLId(this.runner.id); + }, + runnerUrl() { + // TODO implement using webUrl from the API + return `${gon.gitlab_url || ''}/admin/runners/${this.runnerNumericalId}`; + }, + description() { + return this.runner.description; + }, + shortSha() { + return this.runner.shortSha; + }, + }, +}; +</script> + +<template> + <div> + <gl-link :href="runnerUrl"> #{{ runnerNumericalId }} ({{ shortSha }})</gl-link> + <tooltip-on-truncate class="gl-display-block" :title="description" truncate-target="child"> + <div class="gl-text-truncate"> + {{ description }} + </div> + </tooltip-on-truncate> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue b/app/assets/javascripts/runner/components/cells/runner_type_cell.vue new file mode 100644 index 00000000000..b3ebdfd82e3 --- /dev/null +++ b/app/assets/javascripts/runner/components/cells/runner_type_cell.vue @@ -0,0 +1,42 @@ +<script> +import { GlBadge } from '@gitlab/ui'; +import RunnerTypeBadge from '../runner_type_badge.vue'; + +export default { + components: { + GlBadge, + RunnerTypeBadge, + }, + props: { + runner: { + type: Object, + required: true, + }, + }, + computed: { + runnerType() { + return this.runner.runnerType; + }, + locked() { + return this.runner.locked; + }, + paused() { + return !this.runner.active; + }, + }, +}; +</script> + +<template> + <div> + <runner-type-badge :type="runnerType" size="sm" /> + + <gl-badge v-if="locked" variant="warning" size="sm"> + {{ __('locked') }} + </gl-badge> + + <gl-badge v-if="paused" variant="danger" size="sm"> + {{ __('paused') }} + </gl-badge> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue new file mode 100644 index 00000000000..bec33ce2f44 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue @@ -0,0 +1,145 @@ +<script> +import { GlFilteredSearchToken } from '@gitlab/ui'; +import { cloneDeep } from 'lodash'; +import { __, s__ } from '~/locale'; +import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; +import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import { + STATUS_ACTIVE, + STATUS_PAUSED, + STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_NOT_CONNECTED, + INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, + CREATED_DESC, + CREATED_ASC, + CONTACTED_DESC, + CONTACTED_ASC, + PARAM_KEY_STATUS, + PARAM_KEY_RUNNER_TYPE, +} from '../constants'; + +const searchTokens = [ + { + icon: 'status', + title: __('Status'), + type: PARAM_KEY_STATUS, + token: GlFilteredSearchToken, + // TODO Get more than one value when GraphQL API supports OR for "status" + unique: true, + options: [ + { value: STATUS_ACTIVE, title: s__('Runners|Active') }, + { value: STATUS_PAUSED, title: s__('Runners|Paused') }, + { value: STATUS_ONLINE, title: s__('Runners|Online') }, + { value: STATUS_OFFLINE, title: s__('Runners|Offline') }, + + // Added extra quotes in this title to avoid splitting this value: + // see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438 + { value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` }, + ], + // TODO In principle we could support more complex search rules, + // this can be added to a separate issue. + operators: OPERATOR_IS_ONLY, + }, + + { + icon: 'file-tree', + title: __('Type'), + type: PARAM_KEY_RUNNER_TYPE, + token: GlFilteredSearchToken, + // TODO Get more than one value when GraphQL API supports OR for "status" + unique: true, + options: [ + { value: INSTANCE_TYPE, title: s__('Runners|shared') }, + { value: GROUP_TYPE, title: s__('Runners|group') }, + { value: PROJECT_TYPE, title: s__('Runners|specific') }, + ], + // TODO We should support more complex search rules, + // search for multiple states (OR) or have NOT operators + operators: OPERATOR_IS_ONLY, + }, + + // TODO Support tags +]; + +const sortOptions = [ + { + id: 1, + title: __('Created date'), + sortDirection: { + descending: CREATED_DESC, + ascending: CREATED_ASC, + }, + }, + { + id: 2, + title: __('Last contact'), + sortDirection: { + descending: CONTACTED_DESC, + ascending: CONTACTED_ASC, + }, + }, +]; + +export default { + components: { + FilteredSearch, + }, + props: { + value: { + type: Object, + required: true, + validator(val) { + return Array.isArray(val?.filters) && typeof val?.sort === 'string'; + }, + }, + }, + data() { + // filtered_search_bar_root.vue may mutate the inital + // filters. Use `cloneDeep` to prevent those mutations + // from affecting this component + const { filters, sort } = cloneDeep(this.value); + return { + initialFilterValue: filters, + initialSortBy: sort, + }; + }, + methods: { + onFilter(filters) { + const { sort } = this.value; + + this.$emit('input', { + filters, + sort, + pagination: { page: 1 }, + }); + }, + onSort(sort) { + const { filters } = this.value; + + this.$emit('input', { + filters, + sort, + pagination: { page: 1 }, + }); + }, + }, + sortOptions, + searchTokens, +}; +</script> +<template> + <filtered-search + v-bind="$attrs" + recent-searches-storage-key="runners-search" + :sort-options="$options.sortOptions" + :initial-filter-value="initialFilterValue" + :initial-sort-by="initialSortBy" + :tokens="$options.searchTokens" + :search-input-placeholder="__('Search or filter results...')" + @onFilter="onFilter" + @onSort="onSort" + /> +</template> diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue new file mode 100644 index 00000000000..41adbbb55f6 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -0,0 +1,142 @@ +<script> +import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { formatNumber, sprintf, __, s__ } from '~/locale'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import RunnerActionsCell from './cells/runner_actions_cell.vue'; +import RunnerNameCell from './cells/runner_name_cell.vue'; +import RunnerTypeCell from './cells/runner_type_cell.vue'; +import RunnerTags from './runner_tags.vue'; + +const tableField = ({ key, label = '', width = 10 }) => { + return { + key, + label, + thClass: [ + `gl-w-${width}p`, + 'gl-bg-transparent!', + 'gl-border-b-solid!', + 'gl-border-b-gray-100!', + 'gl-py-5!', + 'gl-px-0!', + 'gl-border-b-1!', + ], + tdClass: ['gl-py-5!', 'gl-px-1!'], + tdAttr: { + 'data-testid': `td-${key}`, + }, + }; +}; + +export default { + components: { + GlTable, + GlSkeletonLoader, + TimeAgo, + RunnerActionsCell, + RunnerNameCell, + RunnerTags, + RunnerTypeCell, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + runners: { + type: Array, + required: true, + }, + activeRunnersCount: { + type: Number, + required: true, + }, + }, + computed: { + activeRunnersMessage() { + return sprintf(__('Runners currently online: %{active_runners_count}'), { + active_runners_count: formatNumber(this.activeRunnersCount), + }); + }, + }, + methods: { + runnerTrAttr(runner) { + if (runner) { + return { + 'data-testid': `runner-row-${getIdFromGraphQLId(runner.id)}`, + }; + } + return {}; + }, + }, + fields: [ + tableField({ key: 'type', label: __('Type/State') }), + tableField({ key: 'name', label: s__('Runners|Runner'), width: 30 }), + tableField({ key: 'version', label: __('Version') }), + tableField({ key: 'ipAddress', label: __('IP Address') }), + tableField({ key: 'projectCount', label: __('Projects'), width: 5 }), + tableField({ key: 'jobCount', label: __('Jobs'), width: 5 }), + tableField({ key: 'tagList', label: __('Tags') }), + tableField({ key: 'contactedAt', label: __('Last contact') }), + tableField({ key: 'actions', label: '' }), + ], +}; +</script> +<template> + <div> + <div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div> + <gl-table + :busy="loading" + :items="runners" + :fields="$options.fields" + :tbody-tr-attr="runnerTrAttr" + stacked="md" + fixed + > + <template v-if="!runners.length" #table-busy> + <gl-skeleton-loader v-for="i in 4" :key="i" /> + </template> + + <template #cell(type)="{ item }"> + <runner-type-cell :runner="item" /> + </template> + + <template #cell(name)="{ item }"> + <runner-name-cell :runner="item" /> + </template> + + <template #cell(version)="{ item: { version } }"> + {{ version }} + </template> + + <template #cell(ipAddress)="{ item: { ipAddress } }"> + {{ ipAddress }} + </template> + + <template #cell(projectCount)> + <!-- TODO add projects count --> + </template> + + <template #cell(jobCount)> + <!-- TODO add jobs count --> + </template> + + <template #cell(tagList)="{ item: { tagList } }"> + <runner-tags :tag-list="tagList" size="sm" /> + </template> + + <template #cell(contactedAt)="{ item: { contactedAt } }"> + <time-ago v-if="contactedAt" :time="contactedAt" /> + <template v-else>{{ __('Never') }}</template> + </template> + + <template #cell(actions)="{ item }"> + <runner-actions-cell :runner="item" /> + </template> + </gl-table> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue new file mode 100644 index 00000000000..4755977b051 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue @@ -0,0 +1,76 @@ +<script> +import { GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue'; + +export default { + components: { + GlLink, + GlSprintf, + ClipboardButton, + RunnerInstructions, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: { + runnerInstallHelpPage: { + default: null, + }, + }, + props: { + registrationToken: { + type: String, + required: true, + }, + typeName: { + type: String, + required: false, + default: __('shared'), + }, + }, + computed: { + rootUrl() { + return gon.gitlab_url || ''; + }, + }, +}; +</script> + +<template> + <div class="bs-callout"> + <h5 data-testid="runner-help-title"> + <gl-sprintf :message="__('Set up a %{type} runner manually')"> + <template #type> + {{ typeName }} + </template> + </gl-sprintf> + </h5> + + <ol> + <li> + <gl-link :href="runnerInstallHelpPage" data-testid="runner-help-link" target="_blank"> + {{ __("Install GitLab Runner and ensure it's running.") }} + </gl-link> + </li> + <li> + {{ __('Register the runner with this URL:') }} + <br /> + + <code data-testid="coordinator-url">{{ rootUrl }}</code> + <clipboard-button :title="__('Copy URL')" :text="rootUrl" /> + </li> + <li> + {{ __('And this registration token:') }} + <br /> + + <code data-testid="registration-token">{{ registrationToken }}</code> + <clipboard-button :title="__('Copy token')" :text="registrationToken" /> + </li> + </ol> + + <!-- TODO Implement reset token functionality --> + <runner-instructions /> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_pagination.vue b/app/assets/javascripts/runner/components/runner_pagination.vue new file mode 100644 index 00000000000..8645b90f5cd --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_pagination.vue @@ -0,0 +1,57 @@ +<script> +import { GlPagination } from '@gitlab/ui'; + +export default { + components: { + GlPagination, + }, + props: { + value: { + required: false, + type: Object, + default: () => ({ + page: 1, + }), + }, + pageInfo: { + required: false, + type: Object, + default: () => ({}), + }, + }, + computed: { + prevPage() { + return this.pageInfo?.hasPreviousPage ? this.value?.page - 1 : null; + }, + nextPage() { + return this.pageInfo?.hasNextPage ? this.value?.page + 1 : null; + }, + }, + methods: { + handlePageChange(page) { + if (page > this.value.page) { + this.$emit('input', { + page, + after: this.pageInfo.endCursor, + }); + } else { + this.$emit('input', { + page, + before: this.pageInfo.startCursor, + }); + } + }, + }, +}; +</script> + +<template> + <gl-pagination + :value="value.page" + :prev-page="prevPage" + :next-page="nextPage" + align="center" + class="gl-pagination gl-mt-3" + @input="handlePageChange" + /> +</template> diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/runner/components/runner_tags.vue new file mode 100644 index 00000000000..4ba07e00c96 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_tags.vue @@ -0,0 +1,33 @@ +<script> +import { GlBadge } from '@gitlab/ui'; + +export default { + components: { + GlBadge, + }, + props: { + tagList: { + type: Array, + required: false, + default: () => [], + }, + size: { + type: String, + required: false, + default: 'md', + }, + variant: { + type: String, + required: false, + default: 'info', + }, + }, +}; +</script> +<template> + <div> + <gl-badge v-for="tag in tagList" :key="tag" :size="size" :variant="variant"> + {{ tag }} + </gl-badge> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_type_alert.vue b/app/assets/javascripts/runner/components/runner_type_alert.vue new file mode 100644 index 00000000000..72ce582e02c --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_type_alert.vue @@ -0,0 +1,66 @@ +<script> +import { GlAlert, GlLink } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { s__ } from '~/locale'; +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants'; + +const ALERT_DATA = { + [INSTANCE_TYPE]: { + title: s__( + 'Runners|This runner is available to all groups and projects in your GitLab instance.', + ), + message: s__( + 'Runners|Shared runners are available to every project in a GitLab instance. If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner.', + ), + variant: 'success', + anchor: 'shared-runners', + }, + [GROUP_TYPE]: { + title: s__('Runners|This runner is available to all projects and subgroups in a group.'), + message: s__( + 'Runners|Use Group runners when you want all projects in a group to have access to a set of runners.', + ), + variant: 'success', + anchor: 'group-runners', + }, + [PROJECT_TYPE]: { + title: s__('Runners|This runner is associated with specific projects.'), + message: s__( + 'Runners|You can set up a specific runner to be used by multiple projects but you cannot make this a shared runner.', + ), + variant: 'info', + anchor: 'specific-runners', + }, +}; + +export default { + components: { + GlAlert, + GlLink, + }, + props: { + type: { + type: String, + required: false, + default: null, + validator(type) { + return Boolean(ALERT_DATA[type]); + }, + }, + }, + computed: { + alert() { + return ALERT_DATA[this.type]; + }, + helpHref() { + return helpPagePath('ci/runners/runners_scope', { anchor: this.alert.anchor }); + }, + }, +}; +</script> +<template> + <gl-alert v-if="alert" :variant="alert.variant" :title="alert.title" :dismissible="false"> + {{ alert.message }} + <gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link> + </gl-alert> +</template> diff --git a/app/assets/javascripts/runner/components/runner_type_badge.vue b/app/assets/javascripts/runner/components/runner_type_badge.vue index dd4fff3a77a..c2f43daa899 100644 --- a/app/assets/javascripts/runner/components/runner_type_badge.vue +++ b/app/assets/javascripts/runner/components/runner_type_badge.vue @@ -3,7 +3,7 @@ import { GlBadge } from '@gitlab/ui'; import { s__ } from '~/locale'; import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants'; -const badge = { +const BADGE_DATA = { [INSTANCE_TYPE]: { variant: 'success', text: s__('Runners|shared'), @@ -25,21 +25,22 @@ export default { props: { type: { type: String, - required: true, + required: false, + default: null, + validator(type) { + return Boolean(BADGE_DATA[type]); + }, }, }, computed: { - variant() { - return badge[this.type]?.variant; - }, - text() { - return badge[this.type]?.text; + badge() { + return BADGE_DATA[this.type]; }, }, }; </script> <template> - <gl-badge v-if="text" :variant="variant" v-bind="$attrs"> - {{ text }} + <gl-badge v-if="badge" :variant="badge.variant" v-bind="$attrs"> + {{ badge.text }} </gl-badge> </template> diff --git a/app/assets/javascripts/runner/components/runner_type_help.vue b/app/assets/javascripts/runner/components/runner_type_help.vue new file mode 100644 index 00000000000..927deb290a4 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_type_help.vue @@ -0,0 +1,60 @@ +<script> +import { GlBadge } from '@gitlab/ui'; +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants'; +import RunnerTypeBadge from './runner_type_badge.vue'; + +export default { + components: { + GlBadge, + RunnerTypeBadge, + }, + runnerTypes: { + INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, + }, +}; +</script> + +<template> + <div class="bs-callout"> + <p>{{ __('Runners are processes that pick up and execute CI/CD jobs for GitLab.') }}</p> + <p> + {{ + __( + 'You can register runners as separate users, on separate servers, and on your local machine. Register as many runners as you want.', + ) + }} + </p> + + <div> + <span> {{ __('Runners can be:') }}</span> + <ul> + <li> + <runner-type-badge :type="$options.runnerTypes.INSTANCE_TYPE" size="sm" /> + - {{ __('Runs jobs from all unassigned projects.') }} + </li> + <li> + <runner-type-badge :type="$options.runnerTypes.GROUP_TYPE" size="sm" /> + - {{ __('Runs jobs from all unassigned projects in its group.') }} + </li> + <li> + <runner-type-badge :type="$options.runnerTypes.PROJECT_TYPE" size="sm" /> + - {{ __('Runs jobs from assigned projects.') }} + </li> + <li> + <gl-badge variant="warning" size="sm"> + {{ __('locked') }} + </gl-badge> + - {{ __('Cannot be assigned to other projects.') }} + </li> + <li> + <gl-badge variant="danger" size="sm"> + {{ __('paused') }} + </gl-badge> + - {{ __('Not available to run jobs.') }} + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue new file mode 100644 index 00000000000..0c1b83b6830 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_update_form.vue @@ -0,0 +1,227 @@ +<script> +import { + GlButton, + GlForm, + GlFormCheckbox, + GlFormGroup, + GlFormInputGroup, + GlTooltipDirective, +} from '@gitlab/ui'; +import createFlash, { FLASH_TYPES } from '~/flash'; +import { __ } from '~/locale'; +import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; +import runnerUpdateMutation from '../graphql/runner_update.mutation.graphql'; + +const runnerToModel = (runner) => { + const { + id, + description, + maximumTimeout, + accessLevel, + active, + locked, + runUntagged, + tagList = [], + } = runner || {}; + + return { + id, + description, + maximumTimeout, + accessLevel, + active, + locked, + runUntagged, + tagList: tagList.join(', '), + }; +}; + +export default { + components: { + GlButton, + GlForm, + GlFormCheckbox, + GlFormGroup, + GlFormInputGroup, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + runner: { + type: Object, + required: false, + default: null, + }, + }, + data() { + return { + saving: false, + model: runnerToModel(this.runner), + }; + }, + computed: { + canBeLockedToProject() { + return this.runner?.runnerType === PROJECT_TYPE; + }, + readonlyIpAddress() { + return this.runner?.ipAddress; + }, + updateMutationInput() { + const { maximumTimeout, tagList } = this.model; + + return { + ...this.model, + maximumTimeout: maximumTimeout !== '' ? maximumTimeout : null, + tagList: tagList + .split(',') + .map((tag) => tag.trim()) + .filter((tag) => Boolean(tag)), + }; + }, + }, + watch: { + runner(newVal, oldVal) { + if (oldVal === null) { + this.model = runnerToModel(newVal); + } + }, + }, + methods: { + async onSubmit() { + this.saving = true; + + try { + const { + data: { + runnerUpdate: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: runnerUpdateMutation, + variables: { + input: this.updateMutationInput, + }, + }); + + if (errors?.length) { + this.onError(new Error(errors[0])); + return; + } + + this.onSuccess(); + } catch (e) { + this.onError(e); + } finally { + this.saving = false; + } + }, + onError(error) { + const { message } = error; + createFlash({ message }); + }, + onSuccess() { + createFlash({ message: __('Changes saved.'), type: FLASH_TYPES.SUCCESS }); + this.model = runnerToModel(this.runner); + }, + }, + ACCESS_LEVEL_NOT_PROTECTED, + ACCESS_LEVEL_REF_PROTECTED, +}; +</script> +<template> + <gl-form @submit.prevent="onSubmit"> + <gl-form-checkbox + v-model="model.active" + data-testid="runner-field-paused" + :value="false" + :unchecked-value="true" + > + {{ __('Paused') }} + <template #help> + {{ __("Paused runners don't accept new jobs") }} + </template> + </gl-form-checkbox> + + <gl-form-checkbox + v-model="model.accessLevel" + data-testid="runner-field-protected" + :value="$options.ACCESS_LEVEL_REF_PROTECTED" + :unchecked-value="$options.ACCESS_LEVEL_NOT_PROTECTED" + > + {{ __('Protected') }} + <template #help> + {{ __('This runner will only run on pipelines triggered on protected branches') }} + </template> + </gl-form-checkbox> + + <gl-form-checkbox v-model="model.runUntagged" data-testid="runner-field-run-untagged"> + {{ __('Run untagged jobs') }} + <template #help> + {{ __('Indicates whether this runner can pick jobs without tags') }} + </template> + </gl-form-checkbox> + + <gl-form-checkbox + v-model="model.locked" + data-testid="runner-field-locked" + :disabled="!canBeLockedToProject" + > + {{ __('Lock to current projects') }} + <template #help> + {{ __('When a runner is locked, it cannot be assigned to other projects') }} + </template> + </gl-form-checkbox> + + <gl-form-group :label="__('IP Address')" data-testid="runner-field-ip-address"> + <gl-form-input-group :value="readonlyIpAddress" readonly select-on-click> + <template #append> + <gl-button + v-gl-tooltip.hover + :title="__('Copy IP Address')" + :aria-label="__('Copy IP Address')" + :data-clipboard-text="readonlyIpAddress" + icon="copy-to-clipboard" + class="d-inline-flex" + /> + </template> + </gl-form-input-group> + </gl-form-group> + + <gl-form-group :label="__('Description')" data-testid="runner-field-description"> + <gl-form-input-group v-model="model.description" /> + </gl-form-group> + + <gl-form-group + data-testid="runner-field-max-timeout" + :label="__('Maximum job timeout')" + :description=" + s__( + 'Runners|Enter the number of seconds. This timeout takes precedence over lower timeouts set for the project.', + ) + " + > + <gl-form-input-group v-model.number="model.maximumTimeout" type="number" /> + </gl-form-group> + + <gl-form-group + data-testid="runner-field-tags" + :label="__('Tags')" + :description=" + __('You can set up jobs to only use runners with specific tags. Separate tags with commas.') + " + > + <gl-form-input-group v-model="model.tagList" /> + </gl-form-group> + + <div class="form-actions"> + <gl-button + type="submit" + variant="confirm" + class="js-no-auto-disable" + :loading="saving || !runner" + > + {{ __('Save changes') }} + </gl-button> + </div> + </gl-form> +</template> |