diff options
Diffstat (limited to 'app/assets/javascripts/runner/components')
12 files changed, 336 insertions, 144 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> |