summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/runner/components
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 18:25:58 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-16 18:25:58 +0000
commita5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch)
treefb69158581673816a8cd895f9d352dcb3c678b1e /app/assets/javascripts/runner/components
parentd16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue171
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_name_cell.vue44
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_type_cell.vue42
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue145
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue142
-rw-r--r--app/assets/javascripts/runner/components/runner_manual_setup_help.vue76
-rw-r--r--app/assets/javascripts/runner/components/runner_pagination.vue57
-rw-r--r--app/assets/javascripts/runner/components/runner_tags.vue33
-rw-r--r--app/assets/javascripts/runner/components/runner_type_alert.vue66
-rw-r--r--app/assets/javascripts/runner/components/runner_type_badge.vue19
-rw-r--r--app/assets/javascripts/runner/components/runner_type_help.vue60
-rw-r--r--app/assets/javascripts/runner/components/runner_update_form.vue227
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>