diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-18 13:16:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-18 13:16:36 +0000 |
commit | 311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch) | |
tree | 07e7870bca8aed6d61fdcc810731c50d2c40af47 /app/assets/javascripts/runner | |
parent | 27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff) | |
download | gitlab-ce-311b0269b4eb9839fa63f80c8d7a58f32b8138a0.tar.gz |
Add latest changes from gitlab-org/gitlab@14-5-stable-eev14.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/runner')
31 files changed, 720 insertions, 389 deletions
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue index c8513a0b803..3edb658eaf5 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -1,18 +1,26 @@ <script> -import { GlLink } from '@gitlab/ui'; +import { GlBadge, GlLink } from '@gitlab/ui'; import createFlash from '~/flash'; import { fetchPolicies } from '~/lib/graphql'; import { updateHistory } from '~/lib/utils/url_utility'; -import { formatNumber, sprintf, __ } from '~/locale'; +import { sprintf, __ } from '~/locale'; + +import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerList from '../components/runner_list.vue'; -import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerPagination from '../components/runner_pagination.vue'; +import RunnerTypeTabs from '../components/runner_type_tabs.vue'; + import { statusTokenConfig } from '../components/search_tokens/status_token_config'; import { tagTokenConfig } from '../components/search_tokens/tag_token_config'; -import { typeTokenConfig } from '../components/search_tokens/type_token_config'; -import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants'; +import { + ADMIN_FILTERED_SEARCH_NAMESPACE, + INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, + I18N_FETCH_ERROR, +} from '../constants'; import getRunnersQuery from '../graphql/get_runners.query.graphql'; import { fromUrlQueryToSearch, @@ -24,19 +32,37 @@ import { captureException } from '../sentry_utils'; export default { name: 'AdminRunnersApp', components: { + GlBadge, GlLink, + RegistrationDropdown, RunnerFilteredSearchBar, RunnerList, - RunnerManualSetupHelp, RunnerName, RunnerPagination, + RunnerTypeTabs, }, props: { + registrationToken: { + type: String, + required: true, + }, activeRunnersCount: { - type: Number, + type: String, required: true, }, - registrationToken: { + allRunnersCount: { + type: String, + required: true, + }, + instanceRunnersCount: { + type: String, + required: true, + }, + groupRunnersCount: { + type: String, + required: true, + }, + projectRunnersCount: { type: String, required: true, }, @@ -86,13 +112,12 @@ export default { }, activeRunnersMessage() { return sprintf(__('Runners currently online: %{active_runners_count}'), { - active_runners_count: formatNumber(this.activeRunnersCount), + active_runners_count: this.activeRunnersCount, }); }, searchTokens() { return [ statusTokenConfig, - typeTokenConfig, { ...tagTokenConfig, recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`, @@ -116,6 +141,20 @@ export default { this.reportToSentry(error); }, methods: { + tabCount({ runnerType }) { + switch (runnerType) { + case null: + return this.allRunnersCount; + case INSTANCE_TYPE: + return this.instanceRunnersCount; + case GROUP_TYPE: + return this.groupRunnersCount; + case PROJECT_TYPE: + return this.projectRunnersCount; + default: + return null; + } + }, reportToSentry(error) { captureException({ error, component: this.$options.name }); }, @@ -126,10 +165,30 @@ export default { </script> <template> <div> - <runner-manual-setup-help - :registration-token="registrationToken" - :type="$options.INSTANCE_TYPE" - /> + <div + class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0" + > + <runner-type-tabs + v-model="search" + class="gl-w-full" + content-class="gl-display-none" + nav-class="gl-border-none!" + > + <template #title="{ tab }"> + {{ tab.title }} + <gl-badge v-if="tabCount(tab)" class="gl-ml-1" size="sm"> + {{ tabCount(tab) }} + </gl-badge> + </template> + </runner-type-tabs> + + <registration-dropdown + class="gl-w-full gl-sm-w-auto gl-mr-auto" + :registration-token="registrationToken" + :type="$options.INSTANCE_TYPE" + right + /> + </div> <runner-filtered-search-bar v-model="search" diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js index 1eec1019b73..62da6cbfa2b 100644 --- a/app/assets/javascripts/runner/admin_runners/index.js +++ b/app/assets/javascripts/runner/admin_runners/index.js @@ -1,8 +1,10 @@ +import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import AdminRunnersApp from './admin_runners_app.vue'; +Vue.use(GlToast); Vue.use(VueApollo); export const initAdminRunners = (selector = '#js-admin-runners') => { @@ -14,15 +16,19 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { // TODO `activeRunnersCount` should be implemented using a GraphQL API // https://gitlab.com/gitlab-org/gitlab/-/issues/333806 - const { activeRunnersCount, registrationToken, runnerInstallHelpPage } = el.dataset; + const { + runnerInstallHelpPage, + registrationToken, + + activeRunnersCount, + allRunnersCount, + instanceRunnersCount, + groupRunnersCount, + projectRunnersCount, + } = el.dataset; const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient( - {}, - { - assumeImmutableResults: true, - }, - ), + defaultClient: createDefaultClient(), }); return new Vue({ @@ -34,8 +40,15 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { render(h) { return h(AdminRunnersApp, { props: { - activeRunnersCount: parseInt(activeRunnersCount, 10), registrationToken, + + // All runner counts are returned as formatted + // strings, we do not use `parseInt`. + activeRunnersCount, + allRunnersCount, + instanceRunnersCount, + groupRunnersCount, + projectRunnersCount, }, }); }, 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 e26bdbf1aea..c4bddb7b398 100644 --- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue @@ -3,7 +3,7 @@ import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui'; import createFlash from '~/flash'; import { __, s__ } from '~/locale'; import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql'; -import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql'; +import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql'; import { captureException } from '~/runner/sentry_utils'; const i18n = { @@ -71,7 +71,7 @@ export default { runnerUpdate: { errors }, }, } = await this.$apollo.mutate({ - mutation: runnerUpdateMutation, + mutation: runnerActionsUpdateMutation, variables: { input: { id: this.runner.id, diff --git a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue new file mode 100644 index 00000000000..9ba1192bc8c --- /dev/null +++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue @@ -0,0 +1,40 @@ +<script> +import { GlTooltipDirective } from '@gitlab/ui'; + +import RunnerContactedStateBadge from '../runner_contacted_state_badge.vue'; +import RunnerPausedBadge from '../runner_paused_badge.vue'; + +import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants'; + +export default { + components: { + RunnerContactedStateBadge, + RunnerPausedBadge, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + runner: { + type: Object, + required: true, + }, + }, + computed: { + paused() { + return !this.runner.active; + }, + }, + i18n: { + I18N_LOCKED_RUNNER_DESCRIPTION, + I18N_PAUSED_RUNNER_DESCRIPTION, + }, +}; +</script> + +<template> + <div> + <runner-contacted-state-badge :runner="runner" size="sm" /> + <runner-paused-badge v-if="paused" size="sm" /> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue index 886b5cb29fc..3b476997915 100644 --- a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue @@ -1,11 +1,21 @@ <script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; + import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import RunnerName from '../runner_name.vue'; +import RunnerTypeBadge from '../runner_type_badge.vue'; + +import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../../constants'; export default { components: { + GlIcon, TooltipOnTruncate, RunnerName, + RunnerTypeBadge, + }, + directives: { + GlTooltip: GlTooltipDirective, }, props: { runner: { @@ -14,10 +24,19 @@ export default { }, }, computed: { + runnerType() { + return this.runner.runnerType; + }, + locked() { + return this.runner.locked; + }, description() { return this.runner.description; }, }, + i18n: { + I18N_LOCKED_RUNNER_DESCRIPTION, + }, }; </script> @@ -26,6 +45,14 @@ export default { <slot :runner="runner" name="runner-name"> <runner-name :runner="runner" /> </slot> + + <runner-type-badge :type="runnerType" size="sm" /> + <gl-icon + v-if="locked" + v-gl-tooltip + :title="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION" + name="lock" + /> <tooltip-on-truncate class="gl-display-block" :title="description" truncate-target="child"> <div class="gl-text-truncate"> {{ description }} diff --git a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue b/app/assets/javascripts/runner/components/cells/runner_type_cell.vue deleted file mode 100644 index c8cb0bf6088..00000000000 --- a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue +++ /dev/null @@ -1,47 +0,0 @@ -<script> -import { GlTooltipDirective } from '@gitlab/ui'; -import RunnerTypeBadge from '../runner_type_badge.vue'; -import RunnerStateLockedBadge from '../runner_state_locked_badge.vue'; -import RunnerStatePausedBadge from '../runner_state_paused_badge.vue'; -import { I18N_LOCKED_RUNNER_DESCRIPTION, I18N_PAUSED_RUNNER_DESCRIPTION } from '../../constants'; - -export default { - components: { - RunnerTypeBadge, - RunnerStateLockedBadge, - RunnerStatePausedBadge, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - runner: { - type: Object, - required: true, - }, - }, - computed: { - runnerType() { - return this.runner.runnerType; - }, - locked() { - return this.runner.locked; - }, - paused() { - return !this.runner.active; - }, - }, - i18n: { - I18N_LOCKED_RUNNER_DESCRIPTION, - I18N_PAUSED_RUNNER_DESCRIPTION, - }, -}; -</script> - -<template> - <div> - <runner-type-badge :type="runnerType" size="sm" /> - <runner-state-locked-badge v-if="locked" size="sm" /> - <runner-state-paused-badge v-if="paused" size="sm" /> - </div> -</template> diff --git a/app/assets/javascripts/runner/components/helpers/masked_value.vue b/app/assets/javascripts/runner/components/helpers/masked_value.vue deleted file mode 100644 index feccb37de81..00000000000 --- a/app/assets/javascripts/runner/components/helpers/masked_value.vue +++ /dev/null @@ -1,60 +0,0 @@ -<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/registration/registration_dropdown.vue b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue new file mode 100644 index 00000000000..3fbe3c1be74 --- /dev/null +++ b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue @@ -0,0 +1,112 @@ +<script> +import { + GlFormGroup, + GlDropdown, + GlDropdownForm, + GlDropdownItem, + GlDropdownDivider, +} from '@gitlab/ui'; +import { s__ } from '~/locale'; +import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants'; +import RegistrationToken from './registration_token.vue'; +import RegistrationTokenResetDropdownItem from './registration_token_reset_dropdown_item.vue'; + +export default { + i18n: { + showInstallationInstructions: s__( + 'Runners|Show runner installation and registration instructions', + ), + registrationToken: s__('Runners|Registration token'), + }, + components: { + GlFormGroup, + GlDropdown, + GlDropdownForm, + GlDropdownItem, + GlDropdownDivider, + RegistrationToken, + RunnerInstructionsModal, + RegistrationTokenResetDropdownItem, + }, + props: { + registrationToken: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + validator(type) { + return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type); + }, + }, + }, + data() { + return { + currentRegistrationToken: this.registrationToken, + instructionsModalOpened: false, + }; + }, + computed: { + dropdownText() { + switch (this.type) { + case INSTANCE_TYPE: + return s__('Runners|Register an instance runner'); + case GROUP_TYPE: + return s__('Runners|Register a group runner'); + case PROJECT_TYPE: + return s__('Runners|Register a project runner'); + default: + return s__('Runners|Register a runner'); + } + }, + }, + methods: { + onShowInstructionsClick() { + // Rendering the modal on demand, to avoid + // loading instructions prematurely from API. + this.instructionsModalOpened = true; + + this.$nextTick(() => { + // $refs.runnerInstructionsModal is defined in + // the tick after the modal is rendered + this.$refs.runnerInstructionsModal.show(); + }); + }, + onTokenReset(token) { + this.currentRegistrationToken = token; + + this.$refs.runnerRegistrationDropdown.hide(true); + }, + }, +}; +</script> + +<template> + <gl-dropdown + ref="runnerRegistrationDropdown" + menu-class="gl-w-auto!" + :text="dropdownText" + variant="confirm" + v-bind="$attrs" + > + <gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick"> + {{ $options.i18n.showInstallationInstructions }} + <runner-instructions-modal + v-if="instructionsModalOpened" + ref="runnerInstructionsModal" + :registration-token="registrationToken" + data-testid="runner-instructions-modal" + /> + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-form class="gl-p-4!"> + <gl-form-group class="gl-mb-0" :label="$options.i18n.registrationToken"> + <registration-token :value="currentRegistrationToken" /> + </gl-form-group> + </gl-dropdown-form> + <gl-dropdown-divider /> + <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" /> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/runner/components/registration/registration_token.vue b/app/assets/javascripts/runner/components/registration/registration_token.vue new file mode 100644 index 00000000000..d54a66ff0e4 --- /dev/null +++ b/app/assets/javascripts/runner/components/registration/registration_token.vue @@ -0,0 +1,83 @@ +<script> +import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { s__, __ } from '~/locale'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; + +export default { + components: { + GlButtonGroup, + GlButton, + ModalCopyButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + value: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + isMasked: true, + }; + }, + computed: { + maskLabel() { + if (this.isMasked) { + return __('Click to reveal'); + } + return __('Click to hide'); + }, + maskIcon() { + if (this.isMasked) { + return 'eye'; + } + return 'eye-slash'; + }, + displayedValue() { + if (this.isMasked && this.value?.length) { + return '*'.repeat(this.value.length); + } + return this.value; + }, + }, + methods: { + onToggleMasked() { + this.isMasked = !this.isMasked; + }, + onCopied() { + // value already in the clipboard, simply notify the user + this.$toast?.show(s__('Runners|Registration token copied!')); + }, + }, + i18n: { + copyLabel: s__('Runners|Copy registration token'), + }, +}; +</script> +<template> + <gl-button-group> + <gl-button class="gl-font-monospace" data-testid="token-value" label> + {{ displayedValue }} + </gl-button> + <gl-button + v-gl-tooltip + :aria-label="maskLabel" + :title="maskLabel" + :icon="maskIcon" + class="gl-w-auto! gl-flex-shrink-0!" + data-testid="toggle-masked" + @click.stop="onToggleMasked" + /> + <modal-copy-button + class="gl-w-auto! gl-flex-shrink-0!" + :aria-label="$options.i18n.copyLabel" + :title="$options.i18n.copyLabel" + :text="value" + @success="onCopied" + /> + </gl-button-group> +</template> diff --git a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue index cdf14abd4f9..3bb15bff8d8 100644 --- a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue +++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue @@ -1,17 +1,18 @@ <script> -import { GlButton } from '@gitlab/ui'; -import createFlash, { FLASH_TYPES } from '~/flash'; +import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; +import createFlash from '~/flash'; import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; 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'; +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants'; export default { name: 'RunnerRegistrationTokenReset', components: { - GlButton, + GlDropdownItem, + GlLoadingIcon, }, inject: { groupId: { @@ -95,10 +96,7 @@ export default { this.reportToSentry(error); }, onSuccess(token) { - createFlash({ - message: s__('Runners|New registration token generated!'), - type: FLASH_TYPES.SUCCESS, - }); + this.$toast?.show(s__('Runners|New registration token generated!')); this.$emit('tokenReset', token); }, reportToSentry(error) { @@ -108,7 +106,8 @@ export default { }; </script> <template> - <gl-button :loading="loading" @click="resetToken"> + <gl-dropdown-item @click.capture.native.stop="resetToken"> {{ __('Reset registration token') }} - </gl-button> + <gl-loading-icon v-if="loading" inline /> + </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue b/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue new file mode 100644 index 00000000000..b4727f832f8 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_contacted_state_badge.vue @@ -0,0 +1,69 @@ +<script> +import { GlBadge, GlTooltipDirective } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import { + I18N_ONLINE_RUNNER_DESCRIPTION, + I18N_OFFLINE_RUNNER_DESCRIPTION, + I18N_NOT_CONNECTED_RUNNER_DESCRIPTION, + STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_NOT_CONNECTED, +} from '../constants'; + +export default { + components: { + GlBadge, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + runner: { + required: true, + type: Object, + }, + }, + computed: { + contactedAtTimeAgo() { + if (this.runner.contactedAt) { + return getTimeago().format(this.runner.contactedAt); + } + return null; + }, + badge() { + switch (this.runner.status) { + case STATUS_ONLINE: + return { + variant: 'success', + label: s__('Runners|online'), + tooltip: sprintf(I18N_ONLINE_RUNNER_DESCRIPTION, { + timeAgo: this.contactedAtTimeAgo, + }), + }; + case STATUS_OFFLINE: + return { + variant: 'muted', + label: s__('Runners|offline'), + tooltip: sprintf(I18N_OFFLINE_RUNNER_DESCRIPTION, { + timeAgo: this.contactedAtTimeAgo, + }), + }; + case STATUS_NOT_CONNECTED: + return { + variant: 'muted', + label: s__('Runners|not connected'), + tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION, + }; + default: + return null; + } + }, + }, +}; +</script> +<template> + <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" :variant="badge.variant" v-bind="$attrs"> + {{ badge.label }} + </gl-badge> +</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 e04ca8ddca0..a9dfec35479 100644 --- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue +++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue @@ -2,6 +2,7 @@ import { cloneDeep } from 'lodash'; import { __ } from '~/locale'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import { searchValidator } from '~/runner/runner_search_utils'; import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants'; const sortOptions = [ @@ -31,9 +32,12 @@ export default { value: { type: Object, required: true, - validator(val) { - return Array.isArray(val?.filters) && typeof val?.sort === 'string'; - }, + validator: searchValidator, + }, + tokens: { + type: Array, + required: false, + default: () => [], }, namespace: { type: String, @@ -43,7 +47,7 @@ export default { data() { // filtered_search_bar_root.vue may mutate the inital // filters. Use `cloneDeep` to prevent those mutations - // from affecting this component + // from affecting this component const { filters, sort } = cloneDeep(this.value); return { initialFilterValue: filters, @@ -52,19 +56,17 @@ export default { }, methods: { onFilter(filters) { - const { sort } = this.value; - + // Apply new filters, from page 1 this.$emit('input', { + ...this.value, filters, - sort, pagination: { page: 1 }, }); }, onSort(sort) { - const { filters } = this.value; - + // Apply new sort, from page 1 this.$emit('input', { - filters, + ...this.value, sort, pagination: { page: 1 }, }); @@ -74,13 +76,16 @@ export default { }; </script> <template> - <div> + <div + class="gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1" + > <filtered-search v-bind="$attrs" :namespace="namespace" recent-searches-storage-key="runners-search" :sort-options="$options.sortOptions" :initial-filter-value="initialFilterValue" + :tokens="tokens" :initial-sort-by="initialSortBy" :search-input-placeholder="__('Search or filter results...')" data-testid="runners-filtered-search" diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index 3f6ea389288..f8dbc469c22 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -1,12 +1,11 @@ <script> import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { formatNumber, __, s__ } from '~/locale'; +import { __, 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 RunnerSummaryCell from './cells/runner_summary_cell.vue'; -import RunnerTypeCell from './cells/runner_type_cell.vue'; +import RunnerStatusCell from './cells/runner_status_cell.vue'; import RunnerTags from './runner_tags.vue'; const tableField = ({ key, label = '', width = 10 }) => { @@ -37,7 +36,7 @@ export default { RunnerActionsCell, RunnerSummaryCell, RunnerTags, - RunnerTypeCell, + RunnerStatusCell, }, directives: { GlTooltip: GlTooltipDirective, @@ -54,18 +53,6 @@ export default { }, }, 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 { @@ -76,13 +63,11 @@ export default { }, }, fields: [ - tableField({ key: 'type', label: __('Type/State') }), - tableField({ key: 'summary', label: s__('Runners|Runner'), width: 30 }), + tableField({ key: 'status', label: s__('Runners|Status') }), + tableField({ key: 'summary', label: s__('Runners|Runner ID'), 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: 'tagList', label: __('Tags'), width: 20 }), tableField({ key: 'contactedAt', label: __('Last contact') }), tableField({ key: 'actions', label: '' }), ], @@ -103,8 +88,8 @@ export default { <gl-skeleton-loader v-for="i in 4" :key="i" /> </template> - <template #cell(type)="{ item }"> - <runner-type-cell :runner="item" /> + <template #cell(status)="{ item }"> + <runner-status-cell :runner="item" /> </template> <template #cell(summary)="{ item, index }"> @@ -123,14 +108,6 @@ export default { {{ ipAddress }} </template> - <template #cell(projectCount)="{ item: { projectCount } }"> - {{ formatProjectCount(projectCount) }} - </template> - - <template #cell(jobCount)="{ item: { jobCount } }"> - {{ formatJobCount(jobCount) }} - </template> - <template #cell(tagList)="{ item: { tagList } }"> <runner-tags :tag-list="tagList" size="sm" /> </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 deleted file mode 100644 index 475d362bb52..00000000000 --- a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue +++ /dev/null @@ -1,108 +0,0 @@ -<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'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants'; - -export default { - components: { - GlLink, - GlSprintf, - ClipboardButton, - MaskedValue, - RunnerInstructions, - RunnerRegistrationTokenReset, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - inject: { - runnerInstallHelpPage: { - default: null, - }, - }, - props: { - registrationToken: { - type: String, - required: true, - }, - type: { - type: String, - required: true, - validator(type) { - return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type); - }, - }, - }, - data() { - return { - currentRegistrationToken: this.registrationToken, - }; - }, - computed: { - rootUrl() { - return gon.gitlab_url || ''; - }, - typeName() { - switch (this.type) { - case INSTANCE_TYPE: - return s__('Runners|shared'); - case GROUP_TYPE: - return s__('Runners|group'); - case PROJECT_TYPE: - return s__('Runners|specific'); - default: - return ''; - } - }, - }, - methods: { - onTokenReset(token) { - this.currentRegistrationToken = token; - }, - }, -}; -</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" - ><masked-value :value="currentRegistrationToken" - /></code> - <clipboard-button :title="__('Copy token')" :text="currentRegistrationToken" /> - </li> - </ol> - - <runner-registration-token-reset :type="type" @tokenReset="onTokenReset" /> - - <runner-instructions /> - </div> -</template> diff --git a/app/assets/javascripts/runner/components/runner_state_paused_badge.vue b/app/assets/javascripts/runner/components/runner_paused_badge.vue index d1e6fa05e4d..d1e6fa05e4d 100644 --- a/app/assets/javascripts/runner/components/runner_state_paused_badge.vue +++ b/app/assets/javascripts/runner/components/runner_paused_badge.vue diff --git a/app/assets/javascripts/runner/components/runner_state_locked_badge.vue b/app/assets/javascripts/runner/components/runner_state_locked_badge.vue deleted file mode 100644 index 458526010bc..00000000000 --- a/app/assets/javascripts/runner/components/runner_state_locked_badge.vue +++ /dev/null @@ -1,25 +0,0 @@ -<script> -import { GlBadge, GlTooltipDirective } from '@gitlab/ui'; -import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants'; - -export default { - components: { - GlBadge, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - i18n: { - I18N_LOCKED_RUNNER_DESCRIPTION, - }, -}; -</script> -<template> - <gl-badge - v-gl-tooltip="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION" - variant="warning" - v-bind="$attrs" - > - {{ s__('Runners|locked') }} - </gl-badge> -</template> diff --git a/app/assets/javascripts/runner/components/runner_tag.vue b/app/assets/javascripts/runner/components/runner_tag.vue index 06562e618a8..6ad2023a866 100644 --- a/app/assets/javascripts/runner/components/runner_tag.vue +++ b/app/assets/javascripts/runner/components/runner_tag.vue @@ -1,11 +1,15 @@ <script> -import { GlBadge } from '@gitlab/ui'; +import { GlBadge, GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui'; import { RUNNER_TAG_BADGE_VARIANT } from '../constants'; export default { components: { GlBadge, }, + directives: { + GlTooltip: GlTooltipDirective, + GlResizeObserver: GlResizeObserverDirective, + }, props: { tag: { type: String, @@ -14,14 +18,39 @@ export default { size: { type: String, required: false, - default: 'md', + default: 'sm', + }, + }, + data() { + return { + overflowing: false, + }; + }, + computed: { + tooltip() { + if (this.overflowing) { + return this.tag; + } + return ''; + }, + }, + methods: { + onResize() { + const { scrollWidth, offsetWidth } = this.$el; + this.overflowing = scrollWidth > offsetWidth; }, }, RUNNER_TAG_BADGE_VARIANT, }; </script> <template> - <gl-badge :size="size" :variant="$options.RUNNER_TAG_BADGE_VARIANT"> + <gl-badge + v-gl-tooltip="tooltip" + v-gl-resize-observer="onResize" + class="gl-display-inline-block gl-max-w-full gl-text-truncate" + :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 aec0d8e2c66..8da5e33076f 100644 --- a/app/assets/javascripts/runner/components/runner_tags.vue +++ b/app/assets/javascripts/runner/components/runner_tags.vue @@ -14,13 +14,19 @@ export default { size: { type: String, required: false, - default: 'md', + default: 'sm', }, }, }; </script> <template> <div> - <runner-tag v-for="tag in tagList" :key="tag" :tag="tag" :size="size" /> + <runner-tag + v-for="tag in tagList" + :key="tag" + class="gl-display-inline gl-mr-1" + :tag="tag" + :size="size" + /> </div> </template> diff --git a/app/assets/javascripts/runner/components/runner_type_alert.vue b/app/assets/javascripts/runner/components/runner_type_alert.vue index aa435aaa823..1400875a1d6 100644 --- a/app/assets/javascripts/runner/components/runner_type_alert.vue +++ b/app/assets/javascripts/runner/components/runner_type_alert.vue @@ -9,17 +9,14 @@ const ALERT_DATA = { message: s__( 'Runners|This runner is available to all groups and projects in your GitLab instance.', ), - variant: 'success', anchor: 'shared-runners', }, [GROUP_TYPE]: { message: s__('Runners|This runner is available to all projects and subgroups in a group.'), - variant: 'success', anchor: 'group-runners', }, [PROJECT_TYPE]: { message: s__('Runners|This runner is associated with one or more projects.'), - variant: 'info', anchor: 'specific-runners', }, }; @@ -50,7 +47,7 @@ export default { }; </script> <template> - <gl-alert v-if="alert" :variant="alert.variant" :dismissible="false"> + <gl-alert v-if="alert" variant="info" :dismissible="false"> {{ alert.message }} <gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link> </gl-alert> diff --git a/app/assets/javascripts/runner/components/runner_type_badge.vue b/app/assets/javascripts/runner/components/runner_type_badge.vue index 1a61b80184b..b885dcefdcb 100644 --- a/app/assets/javascripts/runner/components/runner_type_badge.vue +++ b/app/assets/javascripts/runner/components/runner_type_badge.vue @@ -12,17 +12,14 @@ import { const BADGE_DATA = { [INSTANCE_TYPE]: { - variant: 'success', text: s__('Runners|shared'), tooltip: I18N_INSTANCE_RUNNER_DESCRIPTION, }, [GROUP_TYPE]: { - variant: 'success', text: s__('Runners|group'), tooltip: I18N_GROUP_RUNNER_DESCRIPTION, }, [PROJECT_TYPE]: { - variant: 'info', text: s__('Runners|specific'), tooltip: I18N_PROJECT_RUNNER_DESCRIPTION, }, @@ -53,7 +50,7 @@ export default { }; </script> <template> - <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" :variant="badge.variant" v-bind="$attrs"> + <gl-badge v-if="badge" v-gl-tooltip="badge.tooltip" variant="info" v-bind="$attrs"> {{ badge.text }} </gl-badge> </template> diff --git a/app/assets/javascripts/runner/components/runner_type_tabs.vue b/app/assets/javascripts/runner/components/runner_type_tabs.vue new file mode 100644 index 00000000000..b767dafaccf --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_type_tabs.vue @@ -0,0 +1,66 @@ +<script> +import { GlTabs, GlTab } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { searchValidator } from '~/runner/runner_search_utils'; +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants'; + +const tabs = [ + { + title: s__('Runners|All'), + runnerType: null, + }, + { + title: s__('Runners|Instance'), + runnerType: INSTANCE_TYPE, + }, + { + title: s__('Runners|Group'), + runnerType: GROUP_TYPE, + }, + { + title: s__('Runners|Project'), + runnerType: PROJECT_TYPE, + }, +]; + +export default { + components: { + GlTabs, + GlTab, + }, + props: { + value: { + type: Object, + required: true, + validator: searchValidator, + }, + }, + methods: { + onTabSelected({ runnerType }) { + this.$emit('input', { + ...this.value, + runnerType, + pagination: { page: 1 }, + }); + }, + isTabActive({ runnerType }) { + return runnerType === this.value.runnerType; + }, + }, + tabs, +}; +</script> +<template> + <gl-tabs v-bind="$attrs" data-testid="runner-type-tabs"> + <gl-tab + v-for="tab in $options.tabs" + :key="`${tab.runnerType}`" + :active="isTabActive(tab)" + @click="onTabSelected(tab)" + > + <template #title> + <slot name="title" :tab="tab">{{ tab.title }}</slot> + </template> + </gl-tab> + </gl-tabs> +</template> diff --git a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js index 03dff5e61a5..9963048ae1d 100644 --- a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js +++ b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js @@ -10,23 +10,29 @@ import { PARAM_KEY_STATUS, } from '../../constants'; +const 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') }, + { value: STATUS_NOT_CONNECTED, title: s__('Runners|Not connected') }, +]; + export const statusTokenConfig = { 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: + options: options.map(({ value, title }) => ({ + value, + // Replace whitespace with a special character to avoid + // splitting this value. + // Replacing in each option, as translations may also + // contain spaces! + // see: https://gitlab.com/gitlab-org/gitlab/-/issues/344142 // 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. + title: title.replace(' ', '\u00a0'), + })), operators: OPERATOR_IS_ONLY, }; diff --git a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js b/app/assets/javascripts/runner/components/search_tokens/type_token_config.js deleted file mode 100644 index 1da61c53386..00000000000 --- a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js +++ /dev/null @@ -1,20 +0,0 @@ -import { __, s__ } from '~/locale'; -import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; -import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, PARAM_KEY_RUNNER_TYPE } from '../../constants'; - -export const typeTokenConfig = { - 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, -}; diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index a2fb9d9efd8..3952e2398e0 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -1,21 +1,33 @@ import { s__ } from '~/locale'; export const RUNNER_PAGE_SIZE = 20; -export const RUNNER_JOB_COUNT_LIMIT = 1000; export const GROUP_RUNNER_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}'); +// Type export const I18N_INSTANCE_RUNNER_DESCRIPTION = s__('Runners|Available to all projects'); export const I18N_GROUP_RUNNER_DESCRIPTION = s__( 'Runners|Available to all projects and subgroups in the group', ); export const I18N_PROJECT_RUNNER_DESCRIPTION = s__('Runners|Associated with one or more projects'); + +// Status +export const I18N_ONLINE_RUNNER_DESCRIPTION = s__( + 'Runners|Runner is online; last contact was %{timeAgo}', +); +export const I18N_OFFLINE_RUNNER_DESCRIPTION = s__( + 'Runners|No recent contact from this runner; last contact was %{timeAgo}', +); +export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__( + 'Runners|This runner has never connected to this instance', +); + export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects'); export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs'); -export const RUNNER_TAG_BADGE_VARIANT = 'info'; +export const RUNNER_TAG_BADGE_VARIANT = 'neutral'; export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100'; // Filtered search parameter names diff --git a/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql new file mode 100644 index 00000000000..547cc43907c --- /dev/null +++ b/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql @@ -0,0 +1,14 @@ +#import "~/runner/graphql/runner_node.fragment.graphql" + +# Mutation for updates within the runners list via action +# buttons (play, pause, ...), loads attributes shown in the +# runner list. + +mutation runnerActionsUpdate($input: RunnerUpdateInput!) { + runnerUpdate(input: $input) { + runner { + ...RunnerNode + } + errors + } +} diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql index 68d6f02f799..98f2dab26ca 100644 --- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql @@ -10,6 +10,5 @@ fragment RunnerNode on CiRunner { locked tagList contactedAt - jobCount - projectCount + status } diff --git a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql index dcc7fdf24f1..ea622fd4958 100644 --- a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql +++ b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql @@ -1,5 +1,8 @@ #import "ee_else_ce/runner/graphql/runner_details.fragment.graphql" +# Mutation for updates from the runner form, loads +# attributes shown in the runner details. + mutation runnerUpdate($input: RunnerUpdateInput!) { runnerUpdate(input: $input) { runner { diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue index 4bb28796dfa..c3dfa885f27 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -5,14 +5,14 @@ import { fetchPolicies } from '~/lib/graphql'; import { updateHistory } from '~/lib/utils/url_utility'; import { formatNumber, sprintf, s__ } from '~/locale'; +import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerList from '../components/runner_list.vue'; -import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerPagination from '../components/runner_pagination.vue'; +import RunnerTypeTabs from '../components/runner_type_tabs.vue'; import { statusTokenConfig } from '../components/search_tokens/status_token_config'; -import { typeTokenConfig } from '../components/search_tokens/type_token_config'; import { I18N_FETCH_ERROR, GROUP_FILTERED_SEARCH_NAMESPACE, @@ -31,11 +31,12 @@ export default { name: 'GroupRunnersApp', components: { GlLink, + RegistrationDropdown, RunnerFilteredSearchBar, RunnerList, - RunnerManualSetupHelp, RunnerName, RunnerPagination, + RunnerTypeTabs, }, props: { registrationToken: { @@ -112,7 +113,7 @@ export default { }); }, searchTokens() { - return [statusTokenConfig, typeTokenConfig]; + return [statusTokenConfig]; }, filteredSearchNamespace() { return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`; @@ -144,7 +145,20 @@ export default { <template> <div> - <runner-manual-setup-help :registration-token="registrationToken" :type="$options.GROUP_TYPE" /> + <div class="gl-display-flex gl-align-items-center"> + <runner-type-tabs + v-model="search" + content-class="gl-display-none" + nav-class="gl-border-none!" + /> + + <registration-dropdown + class="gl-ml-auto" + :registration-token="registrationToken" + :type="$options.GROUP_TYPE" + right + /> + </div> <runner-filtered-search-bar v-model="search" diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js index 9545764c68d..60b7a7ab541 100644 --- a/app/assets/javascripts/runner/group_runners/index.js +++ b/app/assets/javascripts/runner/group_runners/index.js @@ -1,8 +1,10 @@ +import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import GroupRunnersApp from './group_runners_app.vue'; +Vue.use(GlToast); Vue.use(VueApollo); export const initGroupRunners = (selector = '#js-group-runners') => { @@ -21,12 +23,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => { } = el.dataset; const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient( - {}, - { - assumeImmutableResults: true, - }, - ), + defaultClient: createDefaultClient(), }); return new Vue({ diff --git a/app/assets/javascripts/runner/runner_details/index.js b/app/assets/javascripts/runner/runner_details/index.js index 05e6f86869d..db8f239a3c3 100644 --- a/app/assets/javascripts/runner/runner_details/index.js +++ b/app/assets/javascripts/runner/runner_details/index.js @@ -15,12 +15,7 @@ export const initRunnerDetail = (selector = '#js-runner-details') => { const { runnerId } = el.dataset; const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient( - {}, - { - assumeImmutableResults: true, - }, - ), + defaultClient: createDefaultClient(), }); return new Vue({ diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js index 0a817ea0acf..b88023720e8 100644 --- a/app/assets/javascripts/runner/runner_search_utils.js +++ b/app/assets/javascripts/runner/runner_search_utils.js @@ -18,6 +18,50 @@ import { RUNNER_PAGE_SIZE, } from './constants'; +/** + * The filters and sorting of the runners are built around + * an object called "search" that contains the current state + * of search in the UI. For example: + * + * ``` + * const search = { + * // The current tab + * runnerType: 'INSTANCE_TYPE', + * + * // Filters in the search bar + * filters: [ + * { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, + * { type: 'filtered-search-term', value: { data: '' } }, + * ], + * + * // Current sorting value + * sort: 'CREATED_DESC', + * + * // Pagination information + * pagination: { page: 1 }, + * }; + * ``` + * + * An object in this format can be used to generate URLs + * with the search parameters or by runner components + * a input using a v-model. + * + * @module runner_search_utils + */ + +/** + * Validates a search value + * @param {Object} search + * @returns {boolean} True if the value follows the search format. + */ +export const searchValidator = ({ runnerType, filters, sort }) => { + return ( + (runnerType === null || typeof runnerType === 'string') && + Array.isArray(filters) && + typeof sort === 'string' + ); +}; + const getPaginationFromParams = (params) => { const page = parseInt(params[PARAM_KEY_PAGE], 10); const after = params[PARAM_KEY_AFTER]; @@ -35,13 +79,20 @@ const getPaginationFromParams = (params) => { }; }; +/** + * Takes a URL query and transforms it into a "search" object + * @param {String?} query + * @returns {Object} A search object + */ export const fromUrlQueryToSearch = (query = window.location.search) => { const params = queryToObject(query, { gatherArrays: true }); + const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null; return { + runnerType, filters: prepareTokens( urlQueryToFilter(query, { - filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG], + filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_TAG], filteredSearchTermKey: PARAM_KEY_SEARCH, }), ), @@ -50,8 +101,15 @@ export const fromUrlQueryToSearch = (query = window.location.search) => { }; }; +/** + * Takes a "search" object and transforms it into a URL. + * + * @param {Object} search + * @param {String} url + * @returns {String} New URL for the page + */ export const fromSearchToUrl = ( - { filters = [], sort = null, pagination = {} }, + { runnerType = null, filters = [], sort = null, pagination = {} }, url = window.location.href, ) => { const filterParams = { @@ -65,6 +123,10 @@ export const fromSearchToUrl = ( }), }; + if (runnerType) { + filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType]; + } + if (!filterParams[PARAM_KEY_SEARCH]) { filterParams[PARAM_KEY_SEARCH] = null; } @@ -82,21 +144,31 @@ export const fromSearchToUrl = ( return setUrlParams({ ...filterParams, ...otherParams }, url, false, true, true); }; -export const fromSearchToVariables = ({ filters = [], sort = null, pagination = {} } = {}) => { +/** + * Takes a "search" object and transforms it into variables for runner a GraphQL query. + * + * @param {Object} search + * @returns {Object} Hash of filter values + */ +export const fromSearchToVariables = ({ + runnerType = null, + filters = [], + sort = null, + pagination = {}, +} = {}) => { const variables = {}; const queryObj = filterToQueryObject(processFilters(filters), { filteredSearchTermKey: PARAM_KEY_SEARCH, }); - variables.search = queryObj[PARAM_KEY_SEARCH]; - - // TODO Get more than one value when GraphQL API supports OR for "status" or "runner_type" [variables.status] = queryObj[PARAM_KEY_STATUS] || []; - [variables.type] = queryObj[PARAM_KEY_RUNNER_TYPE] || []; - + variables.search = queryObj[PARAM_KEY_SEARCH]; variables.tagList = queryObj[PARAM_KEY_TAG]; + if (runnerType) { + variables.type = runnerType; + } if (sort) { variables.sort = sort; } |