Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 12:55:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 12:55:51 +0300
commite8d2c2579383897a1dd7f9debd359abe8ae8373d (patch)
treec42be41678c2586d49a75cabce89322082698334 /app/assets/javascripts/runner
parentfc845b37ec3a90aaa719975f607740c22ba6a113 (diff)
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'app/assets/javascripts/runner')
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue21
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_type_cell.vue4
-rw-r--r--app/assets/javascripts/runner/components/helpers/masked_value.vue60
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue141
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue36
-rw-r--r--app/assets/javascripts/runner/components/runner_manual_setup_help.vue6
-rw-r--r--app/assets/javascripts/runner/components/runner_registration_token_reset.vue10
-rw-r--r--app/assets/javascripts/runner/components/runner_tag.vue27
-rw-r--r--app/assets/javascripts/runner/components/runner_tags.vue13
-rw-r--r--app/assets/javascripts/runner/components/runner_type_help.vue4
-rw-r--r--app/assets/javascripts/runner/components/runner_update_form.vue67
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token.vue91
-rw-r--r--app/assets/javascripts/runner/constants.js9
-rw-r--r--app/assets/javascripts/runner/graphql/get_runner.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/get_runners.query.graphql6
-rw-r--r--app/assets/javascripts/runner/graphql/runner_delete.mutation.graphql (renamed from app/assets/javascripts/runner/graphql/delete_runner.mutation.graphql)0
-rw-r--r--app/assets/javascripts/runner/graphql/runner_details.fragment.graphql13
-rw-r--r--app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql12
-rw-r--r--app/assets/javascripts/runner/graphql/runner_node.fragment.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/runner_update.mutation.graphql2
-rw-r--r--app/assets/javascripts/runner/runner_details/runner_details_app.vue34
-rw-r--r--app/assets/javascripts/runner/runner_details/runner_update_form_utils.js38
-rw-r--r--app/assets/javascripts/runner/runner_list/index.js3
-rw-r--r--app/assets/javascripts/runner/runner_list/runner_list_app.vue35
-rw-r--r--app/assets/javascripts/runner/runner_list/runner_search_utils.js17
-rw-r--r--app/assets/javascripts/runner/sentry_utils.js20
26 files changed, 481 insertions, 192 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
index 7f9f796bdee..863f0ab995f 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -1,9 +1,11 @@
<script>
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
+import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
-import deleteRunnerMutation from '~/runner/graphql/delete_runner.mutation.graphql';
+import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import { captureException } from '~/runner/sentry_utils';
const i18n = {
I18N_EDIT: __('Edit'),
@@ -14,6 +16,7 @@ const i18n = {
};
export default {
+ name: 'RunnerActionsCell',
components: {
GlButton,
GlButtonGroup,
@@ -86,7 +89,7 @@ export default {
});
if (errors && errors.length) {
- this.onError(new Error(errors[0]));
+ throw new Error(errors.join(' '));
}
} catch (e) {
this.onError(e);
@@ -109,7 +112,7 @@ export default {
runnerDelete: { errors },
},
} = await this.$apollo.mutate({
- mutation: deleteRunnerMutation,
+ mutation: runnerDeleteMutation,
variables: {
input: {
id: this.runner.id,
@@ -119,7 +122,7 @@ export default {
refetchQueries: ['getRunners'],
});
if (errors && errors.length) {
- this.onError(new Error(errors[0]));
+ throw new Error(errors.join(' '));
}
} catch (e) {
this.onError(e);
@@ -129,9 +132,13 @@ export default {
},
onError(error) {
- // TODO Render errors when "delete" action is done
- // `active` toggle would not fail due to user input.
- throw error;
+ const { message } = error;
+ createFlash({ message });
+
+ this.reportToSentry(error);
+ },
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
},
},
i18n,
diff --git a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue b/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
index b3ebdfd82e3..f186a8daf72 100644
--- a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
@@ -32,11 +32,11 @@ export default {
<runner-type-badge :type="runnerType" size="sm" />
<gl-badge v-if="locked" variant="warning" size="sm">
- {{ __('locked') }}
+ {{ s__('Runners|locked') }}
</gl-badge>
<gl-badge v-if="paused" variant="danger" size="sm">
- {{ __('paused') }}
+ {{ s__('Runners|paused') }}
</gl-badge>
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/helpers/masked_value.vue b/app/assets/javascripts/runner/components/helpers/masked_value.vue
new file mode 100644
index 00000000000..feccb37de81
--- /dev/null
+++ b/app/assets/javascripts/runner/components/helpers/masked_value.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isMasked: true,
+ };
+ },
+ computed: {
+ label() {
+ if (this.isMasked) {
+ return __('Click to reveal');
+ }
+ return __('Click to hide');
+ },
+ icon() {
+ if (this.isMasked) {
+ return 'eye';
+ }
+ return 'eye-slash';
+ },
+ displayedValue() {
+ if (this.isMasked && this.value?.length) {
+ return '*'.repeat(this.value.length);
+ }
+ return this.value;
+ },
+ },
+ methods: {
+ toggleMasked() {
+ this.isMasked = !this.isMasked;
+ },
+ },
+};
+</script>
+<template>
+ <span
+ >{{ displayedValue }}
+ <gl-button
+ :aria-label="label"
+ :icon="icon"
+ class="gl-text-body!"
+ data-testid="toggle-masked"
+ variant="link"
+ @click="toggleMasked"
+ />
+ </span>
+</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
index bec33ce2f44..e14b3b17fa8 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
@@ -1,9 +1,9 @@
<script>
-import { GlFilteredSearchToken } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
-import { __, s__ } from '~/locale';
+import { formatNumber, sprintf, __, 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 BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import {
STATUS_ACTIVE,
STATUS_PAUSED,
@@ -19,50 +19,9 @@ import {
CONTACTED_ASC,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
+ PARAM_KEY_TAG,
} 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
-];
+import TagToken from './search_tokens/tag_token.vue';
const sortOptions = [
{
@@ -95,6 +54,14 @@ export default {
return Array.isArray(val?.filters) && typeof val?.sort === 'string';
},
},
+ namespace: {
+ type: String,
+ required: true,
+ },
+ activeRunnersCount: {
+ type: Number,
+ required: true,
+ },
},
data() {
// filtered_search_bar_root.vue may mutate the inital
@@ -106,6 +73,62 @@ export default {
initialSortBy: sort,
};
},
+ computed: {
+ searchTokens() {
+ return [
+ {
+ icon: 'status',
+ title: __('Status'),
+ type: PARAM_KEY_STATUS,
+ token: BaseToken,
+ 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: BaseToken,
+ unique: true,
+ options: [
+ { value: INSTANCE_TYPE, title: s__('Runners|instance') },
+ { value: GROUP_TYPE, title: s__('Runners|group') },
+ { value: PROJECT_TYPE, title: s__('Runners|project') },
+ ],
+ // TODO We should support more complex search rules,
+ // search for multiple states (OR) or have NOT operators
+ operators: OPERATOR_IS_ONLY,
+ },
+
+ {
+ icon: 'tag',
+ title: s__('Runners|Tags'),
+ type: PARAM_KEY_TAG,
+ token: TagToken,
+ recentTokenValuesStorageKey: `${this.namespace}-recent-tags`,
+ operators: OPERATOR_IS_ONLY,
+ },
+ ];
+ },
+ activeRunnersMessage() {
+ return sprintf(__('Runners currently online: %{active_runners_count}'), {
+ active_runners_count: formatNumber(this.activeRunnersCount),
+ });
+ },
+ },
methods: {
onFilter(filters) {
const { sort } = this.value;
@@ -127,19 +150,23 @@ export default {
},
},
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"
- />
+ <div>
+ <filtered-search
+ v-bind="$attrs"
+ :namespace="namespace"
+ recent-searches-storage-key="runners-search"
+ :sort-options="$options.sortOptions"
+ :initial-filter-value="initialFilterValue"
+ :initial-sort-by="initialSortBy"
+ :tokens="searchTokens"
+ :search-input-placeholder="__('Search or filter results...')"
+ data-testid="runners-filtered-search"
+ @onFilter="onFilter"
+ @onSort="onSort"
+ />
+ <div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div>
+ </div>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index 41adbbb55f6..69a1f106ca8 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -1,8 +1,9 @@
<script>
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { formatNumber, sprintf, __, s__ } from '~/locale';
+import { formatNumber, __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { RUNNER_JOB_COUNT_LIMIT } from '../constants';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerNameCell from './cells/runner_name_cell.vue';
import RunnerTypeCell from './cells/runner_type_cell.vue';
@@ -51,19 +52,20 @@ export default {
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: {
+ formatProjectCount(projectCount) {
+ if (projectCount === null) {
+ return __('n/a');
+ }
+ return formatNumber(projectCount);
+ },
+ formatJobCount(jobCount) {
+ if (jobCount > RUNNER_JOB_COUNT_LIMIT) {
+ return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
+ }
+ return formatNumber(jobCount);
+ },
runnerTrAttr(runner) {
if (runner) {
return {
@@ -88,12 +90,12 @@ export default {
</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"
+ data-testid="runner-list"
stacked="md"
fixed
>
@@ -117,12 +119,12 @@ export default {
{{ ipAddress }}
</template>
- <template #cell(projectCount)>
- <!-- TODO add projects count -->
+ <template #cell(projectCount)="{ item: { projectCount } }">
+ {{ formatProjectCount(projectCount) }}
</template>
- <template #cell(jobCount)>
- <!-- TODO add jobs count -->
+ <template #cell(jobCount)="{ item: { jobCount } }">
+ {{ formatJobCount(jobCount) }}
</template>
<template #cell(tagList)="{ item: { tagList } }">
diff --git a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
index 426d377c92b..475d362bb52 100644
--- a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
+++ b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
@@ -1,6 +1,7 @@
<script>
import { GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
+import MaskedValue from '~/runner/components/helpers/masked_value.vue';
import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
@@ -11,6 +12,7 @@ export default {
GlLink,
GlSprintf,
ClipboardButton,
+ MaskedValue,
RunnerInstructions,
RunnerRegistrationTokenReset,
},
@@ -92,7 +94,9 @@ export default {
{{ __('And this registration token:') }}
<br />
- <code data-testid="registration-token">{{ currentRegistrationToken }}</code>
+ <code data-testid="registration-token"
+ ><masked-value :value="currentRegistrationToken"
+ /></code>
<clipboard-button :title="__('Copy token')" :text="currentRegistrationToken" />
</li>
</ol>
diff --git a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
index b03574264d9..2335faa4f85 100644
--- a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
+++ b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
@@ -3,9 +3,11 @@ import { GlButton } from '@gitlab/ui';
import createFlash, { FLASH_TYPES } from '~/flash';
import { __, s__ } from '~/locale';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
+import { captureException } from '~/runner/sentry_utils';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
export default {
+ name: 'RunnerRegistrationTokenReset',
components: {
GlButton,
},
@@ -52,8 +54,7 @@ export default {
},
});
if (errors && errors.length) {
- this.onError(new Error(errors[0]));
- return;
+ throw new Error(errors.join(' '));
}
this.onSuccess(token);
} catch (e) {
@@ -65,6 +66,8 @@ export default {
onError(error) {
const { message } = error;
createFlash({ message });
+
+ this.reportToSentry(error);
},
onSuccess(token) {
createFlash({
@@ -73,6 +76,9 @@ export default {
});
this.$emit('tokenReset', token);
},
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
},
};
</script>
diff --git a/app/assets/javascripts/runner/components/runner_tag.vue b/app/assets/javascripts/runner/components/runner_tag.vue
new file mode 100644
index 00000000000..06562e618a8
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_tag.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+import { RUNNER_TAG_BADGE_VARIANT } from '../constants';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ props: {
+ tag: {
+ type: String,
+ required: true,
+ },
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ },
+ RUNNER_TAG_BADGE_VARIANT,
+};
+</script>
+<template>
+ <gl-badge :size="size" :variant="$options.RUNNER_TAG_BADGE_VARIANT">
+ {{ tag }}
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/runner/components/runner_tags.vue
index 4ba07e00c96..aec0d8e2c66 100644
--- a/app/assets/javascripts/runner/components/runner_tags.vue
+++ b/app/assets/javascripts/runner/components/runner_tags.vue
@@ -1,9 +1,9 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import RunnerTag from './runner_tag.vue';
export default {
components: {
- GlBadge,
+ RunnerTag,
},
props: {
tagList: {
@@ -16,18 +16,11 @@ export default {
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>
+ <runner-tag v-for="tag in tagList" :key="tag" :tag="tag" :size="size" />
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_type_help.vue b/app/assets/javascripts/runner/components/runner_type_help.vue
index 927deb290a4..70456b3ab65 100644
--- a/app/assets/javascripts/runner/components/runner_type_help.vue
+++ b/app/assets/javascripts/runner/components/runner_type_help.vue
@@ -44,13 +44,13 @@ export default {
</li>
<li>
<gl-badge variant="warning" size="sm">
- {{ __('locked') }}
+ {{ s__('Runners|locked') }}
</gl-badge>
- {{ __('Cannot be assigned to other projects.') }}
</li>
<li>
<gl-badge variant="danger" size="sm">
- {{ __('paused') }}
+ {{ s__('Runners|paused') }}
</gl-badge>
- {{ __('Not available to run jobs.') }}
</li>
diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue
index 0c1b83b6830..85d14547efd 100644
--- a/app/assets/javascripts/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/runner/components/runner_update_form.vue
@@ -7,42 +7,26 @@ import {
GlFormInputGroup,
GlTooltipDirective,
} from '@gitlab/ui';
+import {
+ modelToUpdateMutationVariables,
+ runnerToModel,
+} from 'ee_else_ce/runner/runner_details/runner_update_form_utils';
import createFlash, { FLASH_TYPES } from '~/flash';
import { __ } from '~/locale';
+import { captureException } from '~/runner/sentry_utils';
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 {
+ name: 'RunnerUpdateForm',
components: {
GlButton,
GlForm,
GlFormCheckbox,
GlFormGroup,
GlFormInputGroup,
+ RunnerUpdateCostFactorFields: () =>
+ import('ee_component/runner/components/runner_update_cost_factor_fields.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -67,18 +51,6 @@ export default {
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) {
@@ -98,31 +70,32 @@ export default {
},
} = await this.$apollo.mutate({
mutation: runnerUpdateMutation,
- variables: {
- input: this.updateMutationInput,
- },
+ variables: modelToUpdateMutationVariables(this.model),
});
if (errors?.length) {
- this.onError(new Error(errors[0]));
+ // Validation errors need not be thrown
+ createFlash({ message: errors[0] });
return;
}
this.onSuccess();
- } catch (e) {
- this.onError(e);
+ } catch (error) {
+ const { message } = error;
+ createFlash({ message });
+
+ this.reportToSentry(error);
} 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);
},
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
},
ACCESS_LEVEL_NOT_PROTECTED,
ACCESS_LEVEL_REF_PROTECTED,
@@ -213,6 +186,8 @@ export default {
<gl-form-input-group v-model="model.tagList" />
</gl-form-group>
+ <runner-update-cost-factor-fields v-model="model" />
+
<div class="form-actions">
<gl-button
type="submit"
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
new file mode 100644
index 00000000000..0c69072f06a
--- /dev/null
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlFilteredSearchSuggestion, GlToken } from '@gitlab/ui';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
+
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { RUNNER_TAG_BG_CLASS } from '../../constants';
+
+export const TAG_SUGGESTIONS_PATH = '/admin/runners/tag_list.json';
+
+export default {
+ components: {
+ BaseToken,
+ GlFilteredSearchSuggestion,
+ GlToken,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ tags: [],
+ loading: false,
+ };
+ },
+ methods: {
+ fnCurrentTokenValue(data) {
+ // By default, values are transformed with `toLowerCase`
+ // however, runner tags are case sensitive.
+ return data;
+ },
+ getTagsOptions(search) {
+ // TODO This should be implemented via a GraphQL API
+ // The API should
+ // 1) scope to the rights of the user
+ // 2) stay up to date to the removal of old tags
+ // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
+ return axios
+ .get(TAG_SUGGESTIONS_PATH, {
+ params: {
+ search,
+ },
+ })
+ .then(({ data }) => {
+ return data.map(({ id, name }) => ({ id, value: name, text: name }));
+ });
+ },
+ async fetchTags(searchTerm) {
+ this.loading = true;
+ try {
+ this.tags = await this.getTagsOptions(searchTerm);
+ } catch {
+ createFlash({
+ message: s__('Runners|Something went wrong while fetching the tags suggestions'),
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ RUNNER_TAG_BG_CLASS,
+};
+</script>
+
+<template>
+ <base-token
+ v-bind="$attrs"
+ :config="config"
+ :suggestions-loading="loading"
+ :suggestions="tags"
+ :fn-current-token-value="fnCurrentTokenValue"
+ :recent-suggestions-storage-key="config.recentTokenValuesStorageKey"
+ @fetch-suggestions="fetchTags"
+ v-on="$listeners"
+ >
+ <template #view-token="{ viewTokenProps: { listeners, inputValue, activeTokenValue } }">
+ <gl-token variant="search-value" :class="$options.RUNNER_TAG_BG_CLASS" v-on="listeners">
+ {{ activeTokenValue ? activeTokenValue.text : inputValue }}
+ </gl-token>
+ </template>
+ <template #suggestions-list="{ suggestions }">
+ <gl-filtered-search-suggestion v-for="tag in suggestions" :key="tag.id" :value="tag.value">
+ {{ tag.text }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </base-token>
+</template>
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index a57d18ba745..2822882e0cc 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -1,18 +1,23 @@
import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
+export const RUNNER_JOB_COUNT_LIMIT = 1000;
+export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
-export const RUNNER_ENTITY_TYPE = 'Ci::Runner';
+export const RUNNER_TAG_BADGE_VARIANT = 'info';
+export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
// Filtered search parameter names
// - Used for URL params names
// - GlFilteredSearch tokens type
-export const PARAM_KEY_SEARCH = 'search';
export const PARAM_KEY_STATUS = 'status';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
+export const PARAM_KEY_TAG = 'tag';
+export const PARAM_KEY_SEARCH = 'search';
+
export const PARAM_KEY_SORT = 'sort';
export const PARAM_KEY_PAGE = 'page';
export const PARAM_KEY_AFTER = 'after';
diff --git a/app/assets/javascripts/runner/graphql/get_runner.query.graphql b/app/assets/javascripts/runner/graphql/get_runner.query.graphql
index 84e0d6cc95c..c294cb9bf22 100644
--- a/app/assets/javascripts/runner/graphql/get_runner.query.graphql
+++ b/app/assets/javascripts/runner/graphql/get_runner.query.graphql
@@ -1,4 +1,4 @@
-#import "~/runner/graphql/runner_details.fragment.graphql"
+#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql"
query getRunner($id: CiRunnerID!) {
runner(id: $id) {
diff --git a/app/assets/javascripts/runner/graphql/get_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_runners.query.graphql
index 45df9c625a6..9f837197558 100644
--- a/app/assets/javascripts/runner/graphql/get_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/get_runners.query.graphql
@@ -6,9 +6,10 @@ query getRunners(
$after: String
$first: Int
$last: Int
- $search: String
$status: CiRunnerStatus
$type: CiRunnerType
+ $tagList: [String!]
+ $search: String
$sort: CiRunnerSort
) {
runners(
@@ -16,9 +17,10 @@ query getRunners(
after: $after
first: $first
last: $last
- search: $search
status: $status
type: $type
+ tagList: $tagList
+ search: $search
sort: $sort
) {
nodes {
diff --git a/app/assets/javascripts/runner/graphql/delete_runner.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_delete.mutation.graphql
index d580ea2785e..d580ea2785e 100644
--- a/app/assets/javascripts/runner/graphql/delete_runner.mutation.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_delete.mutation.graphql
diff --git a/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql
index 6d7dc1e2798..2449ee0fc0f 100644
--- a/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql
@@ -1,12 +1,5 @@
+#import "./runner_details_shared.fragment.graphql"
+
fragment RunnerDetails on CiRunner {
- id
- runnerType
- active
- accessLevel
- runUntagged
- locked
- ipAddress
- description
- maximumTimeout
- tagList
+ ...RunnerDetailsShared
}
diff --git a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
new file mode 100644
index 00000000000..8c50cba7de3
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
@@ -0,0 +1,12 @@
+fragment RunnerDetailsShared on CiRunner {
+ id
+ runnerType
+ active
+ accessLevel
+ runUntagged
+ locked
+ ipAddress
+ description
+ maximumTimeout
+ tagList
+}
diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
index 0835e3c7c09..68d6f02f799 100644
--- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
@@ -10,4 +10,6 @@ fragment RunnerNode on CiRunner {
locked
tagList
contactedAt
+ jobCount
+ projectCount
}
diff --git a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
index d50c1880d77..dcc7fdf24f1 100644
--- a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/runner/graphql/runner_details.fragment.graphql"
+#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql"
mutation runnerUpdate($input: RunnerUpdateInput!) {
runnerUpdate(input: $input) {
diff --git a/app/assets/javascripts/runner/runner_details/runner_details_app.vue b/app/assets/javascripts/runner/runner_details/runner_details_app.vue
index 5d5fa81b851..6557a7834e7 100644
--- a/app/assets/javascripts/runner/runner_details/runner_details_app.vue
+++ b/app/assets/javascripts/runner/runner_details/runner_details_app.vue
@@ -1,20 +1,22 @@
<script>
+import createFlash from '~/flash';
+import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { sprintf } from '~/locale';
import RunnerTypeAlert from '../components/runner_type_alert.vue';
import RunnerTypeBadge from '../components/runner_type_badge.vue';
import RunnerUpdateForm from '../components/runner_update_form.vue';
-import { I18N_DETAILS_TITLE, RUNNER_ENTITY_TYPE } from '../constants';
+import { I18N_DETAILS_TITLE, I18N_FETCH_ERROR } from '../constants';
import getRunnerQuery from '../graphql/get_runner.query.graphql';
+import { captureException } from '../sentry_utils';
export default {
+ name: 'RunnerDetailsApp',
components: {
RunnerTypeAlert,
RunnerTypeBadge,
RunnerUpdateForm,
},
- i18n: {
- I18N_DETAILS_TITLE,
- },
props: {
runnerId: {
type: String,
@@ -31,9 +33,27 @@ export default {
query: getRunnerQuery,
variables() {
return {
- id: convertToGraphQLId(RUNNER_ENTITY_TYPE, this.runnerId),
+ id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId),
};
},
+ error(error) {
+ createFlash({ message: I18N_FETCH_ERROR });
+
+ this.reportToSentry(error);
+ },
+ },
+ },
+ computed: {
+ pageTitle() {
+ return sprintf(I18N_DETAILS_TITLE, { runner_id: this.runnerId });
+ },
+ },
+ errorCaptured(error) {
+ this.reportToSentry(error);
+ },
+ methods: {
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
},
},
};
@@ -41,9 +61,7 @@ export default {
<template>
<div>
<h2 class="page-title">
- {{ sprintf($options.i18n.I18N_DETAILS_TITLE, { runner_id: runnerId }) }}
-
- <runner-type-badge v-if="runner" :type="runner.runnerType" />
+ {{ pageTitle }} <runner-type-badge v-if="runner" :type="runner.runnerType" />
</h2>
<runner-type-alert v-if="runner" :type="runner.runnerType" />
diff --git a/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js b/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js
new file mode 100644
index 00000000000..3b519fa7d71
--- /dev/null
+++ b/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js
@@ -0,0 +1,38 @@
+export 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 const modelToUpdateMutationVariables = (model) => {
+ const { maximumTimeout, tagList } = model;
+
+ return {
+ input: {
+ ...model,
+ maximumTimeout: maximumTimeout !== '' ? maximumTimeout : null,
+ tagList: tagList
+ ?.split(',')
+ .map((tag) => tag.trim())
+ .filter((tag) => Boolean(tag)),
+ },
+ };
+};
diff --git a/app/assets/javascripts/runner/runner_list/index.js b/app/assets/javascripts/runner/runner_list/index.js
index 5eba14a7948..16616f00d1e 100644
--- a/app/assets/javascripts/runner/runner_list/index.js
+++ b/app/assets/javascripts/runner/runner_list/index.js
@@ -12,7 +12,8 @@ export const initRunnerList = (selector = '#js-runner-list') => {
return null;
}
- // TODO `activeRunnersCount` should be implemented using a GraphQL API.
+ // TODO `activeRunnersCount` should be implemented using a GraphQL API
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/333806
const { activeRunnersCount, registrationToken, runnerInstallHelpPage } = el.dataset;
const apolloProvider = new VueApollo({
diff --git a/app/assets/javascripts/runner/runner_list/runner_list_app.vue b/app/assets/javascripts/runner/runner_list/runner_list_app.vue
index 7f3a980ccca..8d39243d609 100644
--- a/app/assets/javascripts/runner/runner_list/runner_list_app.vue
+++ b/app/assets/javascripts/runner/runner_list/runner_list_app.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
@@ -7,8 +7,9 @@ import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
-import { INSTANCE_TYPE } from '../constants';
+import { INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
+import { captureException } from '../sentry_utils';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
@@ -16,6 +17,7 @@ import {
} from './runner_search_utils';
export default {
+ name: 'RunnerListApp',
components: {
RunnerFilteredSearchBar,
RunnerList,
@@ -59,8 +61,10 @@ export default {
pageInfo: runners?.pageInfo || {},
};
},
- error(err) {
- this.captureException(err);
+ error(error) {
+ createFlash({ message: I18N_FETCH_ERROR });
+
+ this.reportToSentry(error);
},
},
},
@@ -87,15 +91,12 @@ export default {
},
},
},
- errorCaptured(err) {
- this.captureException(err);
+ errorCaptured(error) {
+ this.reportToSentry(error);
},
methods: {
- captureException(err) {
- Sentry.withScope((scope) => {
- scope.setTag('component', 'runner_list_app');
- Sentry.captureException(err);
- });
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
},
},
INSTANCE_TYPE,
@@ -115,17 +116,17 @@ export default {
</div>
</div>
- <runner-filtered-search-bar v-model="search" namespace="admin_runners" />
+ <runner-filtered-search-bar
+ v-model="search"
+ namespace="admin_runners"
+ :active-runners-count="activeRunnersCount"
+ />
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
</div>
<template v-else>
- <runner-list
- :runners="runners.items"
- :loading="runnersLoading"
- :active-runners-count="activeRunnersCount"
- />
+ <runner-list :runners="runners.items" :loading="runnersLoading" />
<runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" />
</template>
</div>
diff --git a/app/assets/javascripts/runner/runner_list/runner_search_utils.js b/app/assets/javascripts/runner/runner_list/runner_search_utils.js
index e45972b81db..9a0dc9c3a32 100644
--- a/app/assets/javascripts/runner/runner_list/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_list/runner_search_utils.js
@@ -6,9 +6,10 @@ import {
prepareTokens,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import {
- PARAM_KEY_SEARCH,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
+ PARAM_KEY_TAG,
+ PARAM_KEY_SEARCH,
PARAM_KEY_SORT,
PARAM_KEY_PAGE,
PARAM_KEY_AFTER,
@@ -40,7 +41,7 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
return {
filters: prepareTokens(
urlQueryToFilter(query, {
- filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE],
+ filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG],
filteredSearchTermKey: PARAM_KEY_SEARCH,
legacySpacesDecode: false,
}),
@@ -56,15 +57,19 @@ export const fromSearchToUrl = (
) => {
const filterParams = {
// Defaults
- [PARAM_KEY_SEARCH]: null,
[PARAM_KEY_STATUS]: [],
[PARAM_KEY_RUNNER_TYPE]: [],
+ [PARAM_KEY_TAG]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
};
+ if (!filterParams[PARAM_KEY_SEARCH]) {
+ filterParams[PARAM_KEY_SEARCH] = null;
+ }
+
const isDefaultSort = sort !== DEFAULT_SORT;
const isFirstPage = pagination?.page === 1;
const otherParams = {
@@ -87,12 +92,12 @@ export const fromSearchToVariables = ({ filters = [], sort = null, pagination =
variables.search = queryObj[PARAM_KEY_SEARCH];
- // TODO Get more than one value when GraphQL API supports OR for "status"
+ // TODO Get more than one value when GraphQL API supports OR for "status" or "runner_type"
[variables.status] = queryObj[PARAM_KEY_STATUS] || [];
-
- // TODO Get more than one value when GraphQL API supports OR for "runner type"
[variables.type] = queryObj[PARAM_KEY_RUNNER_TYPE] || [];
+ variables.tagList = queryObj[PARAM_KEY_TAG];
+
if (sort) {
variables.sort = sort;
}
diff --git a/app/assets/javascripts/runner/sentry_utils.js b/app/assets/javascripts/runner/sentry_utils.js
new file mode 100644
index 00000000000..29de1f9adae
--- /dev/null
+++ b/app/assets/javascripts/runner/sentry_utils.js
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/browser';
+
+const COMPONENT_TAG = 'vue_component';
+
+/**
+ * Captures an error in a Vue component and sends it
+ * to Sentry
+ *
+ * @param {Object} options
+ * @param {Error} options.error - Exception or error
+ * @param {String} options.component - Component name in CamelCase format
+ */
+export const captureException = ({ error, component }) => {
+ Sentry.withScope((scope) => {
+ if (component) {
+ scope.setTag(COMPONENT_TAG, component);
+ }
+ Sentry.captureException(error);
+ });
+};