diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 09:40:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 09:40:42 +0000 |
commit | ee664acb356f8123f4f6b00b73c1e1cf0866c7fb (patch) | |
tree | f8479f94a28f66654c6a4f6fb99bad6b4e86a40e /app/assets/javascripts/runner | |
parent | 62f7d5c5b69180e82ae8196b7b429eeffc8e7b4f (diff) | |
download | gitlab-ce-ee664acb356f8123f4f6b00b73c1e1cf0866c7fb.tar.gz |
Add latest changes from gitlab-org/gitlab@15-5-stable-eev15.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/runner')
19 files changed, 297 insertions, 117 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 f5620876783..dbaabb35cde 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -17,8 +17,6 @@ import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_cou import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; -import RunnerBulkDelete from '../components/runner_bulk_delete.vue'; -import RunnerBulkDeleteCheckbox from '../components/runner_bulk_delete_checkbox.vue'; import RunnerList from '../components/runner_list.vue'; import RunnerListEmptyState from '../components/runner_list_empty_state.vue'; import RunnerName from '../components/runner_name.vue'; @@ -30,7 +28,12 @@ import RunnerActionsCell from '../components/cells/runner_actions_cell.vue'; import { pausedTokenConfig } from '../components/search_tokens/paused_token_config'; import { statusTokenConfig } from '../components/search_tokens/status_token_config'; import { tagTokenConfig } from '../components/search_tokens/tag_token_config'; -import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants'; +import { + ADMIN_FILTERED_SEARCH_NAMESPACE, + INSTANCE_TYPE, + I18N_FETCH_ERROR, + FILTER_CSS_CLASSES, +} from '../constants'; import { captureException } from '../sentry_utils'; export default { @@ -40,8 +43,6 @@ export default { RegistrationDropdown, RunnerStackedLayoutBanner, RunnerFilteredSearchBar, - RunnerBulkDelete, - RunnerBulkDeleteCheckbox, RunnerList, RunnerListEmptyState, RunnerName, @@ -51,7 +52,7 @@ export default { RunnerActionsCell, }, mixins: [glFeatureFlagMixin()], - inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'], + inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'], props: { registrationToken: { type: String, @@ -114,11 +115,6 @@ export default { upgradeStatusTokenConfig, ]; }, - isBulkDeleteEnabled() { - // Feature flag: admin_runners_bulk_delete - // Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981 - return this.glFeatures.adminRunnersBulkDelete; - }, isSearchFiltered() { return isSearchFiltered(this.search); }, @@ -155,18 +151,13 @@ export default { reportToSentry(error) { captureException({ error, component: this.$options.name }); }, - onChecked({ runner, isChecked }) { - this.localMutations.setRunnerChecked({ - runner, - isChecked, - }); - }, onPaginationInput(value) { this.search.pagination = value; }, }, filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, + FILTER_CSS_CLASSES, }; </script> <template> @@ -195,6 +186,7 @@ export default { <runner-filtered-search-bar v-model="search" + :class="$options.FILTER_CSS_CLASSES" :tokens="searchTokens" :namespace="$options.filteredSearchNamespace" /> @@ -209,20 +201,12 @@ export default { :filtered-svg-path="emptyStateFilteredSvgPath" /> <template v-else> - <runner-bulk-delete - v-if="isBulkDeleteEnabled" - :runners="runners.items" - @deleted="onDeleted" - /> <runner-list :runners="runners.items" :loading="runnersLoading" - :checkable="isBulkDeleteEnabled" - @checked="onChecked" + :checkable="true" + @deleted="onDeleted" > - <template v-if="isBulkDeleteEnabled" #head-checkbox> - <runner-bulk-delete-checkbox :runners="runners.items" /> - </template> <template #runner-name="{ runner }"> <gl-link :href="runner.adminUrl"> <runner-name :runner="runner" /> 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 7a4760f81ee..13f520c4edb 100644 --- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue @@ -52,11 +52,6 @@ export default { :compact="true" @toggledPaused="onToggledPaused" /> - <runner-delete-button - :disabled="!canDelete" - :runner="runner" - :compact="true" - @deleted="onDeleted" - /> + <runner-delete-button v-if="canDelete" :runner="runner" :compact="true" @deleted="onDeleted" /> </gl-button-group> </template> diff --git a/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue b/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue new file mode 100644 index 00000000000..cb43760b2d6 --- /dev/null +++ b/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue @@ -0,0 +1,63 @@ +<script> +import { GlLink, GlTooltipDirective } from '@gitlab/ui'; +import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, I18N_ADMIN } from '../../constants'; + +export default { + components: { + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + runner: { + type: Object, + required: true, + }, + }, + computed: { + cell() { + switch (this.runner?.runnerType) { + case INSTANCE_TYPE: + return { + text: I18N_ADMIN, + }; + case GROUP_TYPE: { + const { name, fullName, webUrl } = this.runner?.groups?.nodes[0] || {}; + + return { + text: name, + href: webUrl, + tooltip: fullName !== name ? fullName : '', + }; + } + case PROJECT_TYPE: { + const { name, nameWithNamespace, webUrl } = this.runner?.ownerProject || {}; + + return { + text: name, + href: webUrl, + tooltip: nameWithNamespace !== name ? nameWithNamespace : '', + }; + } + default: + return {}; + } + }, + }, +}; +</script> + +<template> + <div> + <gl-link + v-if="cell.href" + v-gl-tooltip="cell.tooltip" + :href="cell.href" + class="gl-text-body gl-text-decoration-underline" + > + {{ cell.text }} + </gl-link> + <span v-else>{{ cell.text }}</span> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue index dde5a5a4a05..75afb7a00bc 100644 --- a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue +++ b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue @@ -1,5 +1,6 @@ <script> import { GlFormCheckbox } from '@gitlab/ui'; +import { s__ } from '~/locale'; import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql'; export default { @@ -25,14 +26,20 @@ export default { }, }, computed: { + deletableRunners() { + return this.runners.filter((runner) => runner.userPermissions?.deleteRunner); + }, disabled() { - return !this.runners.length; + return !this.deletableRunners.length; }, checked() { - return Boolean(this.runners.length) && this.runners.every(this.isChecked); + return Boolean(this.deletableRunners.length) && this.deletableRunners.every(this.isChecked); }, indeterminate() { - return !this.checked && this.runners.some(this.isChecked); + return !this.checked && this.deletableRunners.some(this.isChecked); + }, + label() { + return this.checked ? s__('Runners|Unselect all') : s__('Runners|Select all'); }, }, methods: { @@ -41,7 +48,7 @@ export default { }, onChange($event) { this.localMutations.setRunnersChecked({ - runners: this.runners, + runners: this.deletableRunners, isChecked: $event, }); }, @@ -51,6 +58,7 @@ export default { <template> <gl-form-checkbox + :aria-label="label" :indeterminate="indeterminate" :checked="checked" :disabled="disabled" diff --git a/app/assets/javascripts/runner/components/runner_delete_button.vue b/app/assets/javascripts/runner/components/runner_delete_button.vue index 62382891df0..b4f022a7d14 100644 --- a/app/assets/javascripts/runner/components/runner_delete_button.vue +++ b/app/assets/javascripts/runner/components/runner_delete_button.vue @@ -5,12 +5,7 @@ import { createAlert } from '~/flash'; import { sprintf } from '~/locale'; import { captureException } from '~/runner/sentry_utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { - I18N_DELETE_DISABLED_MANY_PROJECTS, - I18N_DELETE_DISABLED_UNKNOWN_REASON, - I18N_DELETE_RUNNER, - I18N_DELETED_TOAST, -} from '../constants'; +import { I18N_DELETE_RUNNER, I18N_DELETED_TOAST } from '../constants'; import RunnerDeleteModal from './runner_delete_modal.vue'; export default { @@ -31,11 +26,6 @@ export default { return runner?.id && runner?.shortSha; }, }, - disabled: { - type: Boolean, - required: false, - default: false, - }, compact: { type: Boolean, required: false, @@ -85,29 +75,14 @@ export default { return null; }, tooltip() { - if (this.disabled && this.runner.projectCount > 1) { - return I18N_DELETE_DISABLED_MANY_PROJECTS; - } - if (this.disabled) { - return I18N_DELETE_DISABLED_UNKNOWN_REASON; - } - // Only show basic "delete" tooltip when compact. // Also prevent a "sticky" tooltip: If this button is - // disabled, mouseout listeners don't run leaving the tooltip stuck + // loading, mouseout listeners don't run leaving the tooltip stuck if (this.compact && !this.deleting) { return I18N_DELETE_RUNNER; } return ''; }, - wrapperTabindex() { - if (this.disabled) { - // Trigger tooltip on keyboard-focusable wrapper - // See https://bootstrap-vue.org/docs/directives/tooltip - return '0'; - } - return null; - }, }, methods: { async onDelete() { @@ -156,14 +131,13 @@ export default { </script> <template> - <div v-gl-tooltip="tooltip" class="btn-group" :tabindex="wrapperTabindex"> + <div v-gl-tooltip="tooltip" class="btn-group"> <gl-button v-gl-modal="runnerDeleteModalId" :aria-label="ariaLabel" :icon="icon" :class="buttonClass" :loading="deleting" - :disabled="disabled" variant="danger" category="secondary" v-bind="$attrs" diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue index 79f934764c6..3d72abcd393 100644 --- a/app/assets/javascripts/runner/components/runner_details.vue +++ b/app/assets/javascripts/runner/components/runner_details.vue @@ -4,7 +4,6 @@ import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants'; import RunnerDetail from './runner_detail.vue'; @@ -29,7 +28,6 @@ export default { RunnerTags, TimeAgo, }, - mixins: [glFeatureFlagMixin()], props: { runner: { type: Object, @@ -117,10 +115,7 @@ export default { </template> </runner-detail> <runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" /> - <runner-detail - v-if="glFeatures.enforceRunnerTokenExpiresAt" - :empty-value="s__('Runners|Never expires')" - > + <runner-detail :empty-value="s__('Runners|Never expires')"> <template #label> {{ s__('Runners|Token expiry') }} <help-popover :options="tokenExpirationHelpPopoverOptions"> 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 5a9ab21a457..da59de9a9eb 100644 --- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue +++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue @@ -85,7 +85,6 @@ export default { </script> <template> <filtered-search - 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" v-bind="$attrs" :namespace="namespace" recent-searches-storage-key="runners-search" diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index 26f1f3ce08c..e895537dcdc 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -2,15 +2,20 @@ import { GlFormCheckbox, GlTableLite, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { s__ } from '~/locale'; +import HelpPopover from '~/vue_shared/components/help_popover.vue'; import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql'; import { formatJobCount, tableField } from '../utils'; +import RunnerBulkDelete from './runner_bulk_delete.vue'; +import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue'; import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue'; import RunnerStatusPopover from './runner_status_popover.vue'; import RunnerStatusCell from './cells/runner_status_cell.vue'; +import RunnerOwnerCell from './cells/runner_owner_cell.vue'; const defaultFields = [ tableField({ key: 'status', label: s__('Runners|Status'), thClasses: ['gl-w-15p'] }), tableField({ key: 'summary', label: s__('Runners|Runner') }), + tableField({ key: 'owner', label: s__('Runners|Owner'), thClasses: ['gl-w-20p'] }), tableField({ key: 'actions', label: '', thClasses: ['gl-w-15p'] }), ]; @@ -19,9 +24,13 @@ export default { GlFormCheckbox, GlTableLite, GlSkeletonLoader, + HelpPopover, + RunnerBulkDelete, + RunnerBulkDeleteCheckbox, RunnerStatusPopover, RunnerStackedSummaryCell, RunnerStatusCell, + RunnerOwnerCell, }, directives: { GlTooltip: GlTooltipDirective, @@ -34,6 +43,7 @@ export default { }, }, }, + inject: ['localMutations'], props: { checkable: { type: Boolean, @@ -50,7 +60,7 @@ export default { required: true, }, }, - emits: ['checked'], + emits: ['deleted'], data() { return { checkedRunnerIds: [] }; }, @@ -79,6 +89,12 @@ export default { }, }, methods: { + canDelete(runner) { + return runner.userPermissions?.deleteRunner; + }, + onDeleted(event) { + this.$emit('deleted', event); + }, formatJobCount(jobCount) { return formatJobCount(jobCount); }, @@ -91,7 +107,7 @@ export default { return {}; }, onCheckboxChange(runner, isChecked) { - this.$emit('checked', { + this.localMutations.setRunnerChecked({ runner, isChecked, }); @@ -104,6 +120,7 @@ export default { </script> <template> <div> + <runner-bulk-delete v-if="checkable" :runners="runners" @deleted="onDeleted" /> <gl-table-lite :aria-busy="loading" :class="tableClass" @@ -116,11 +133,15 @@ export default { fixed > <template #head(checkbox)> - <slot name="head-checkbox"></slot> + <runner-bulk-delete-checkbox :runners="runners" /> </template> <template #cell(checkbox)="{ item }"> - <gl-form-checkbox :checked="isChecked(item)" @change="onCheckboxChange(item, $event)" /> + <gl-form-checkbox + v-if="canDelete(item)" + :checked="isChecked(item)" + @change="onCheckboxChange(item, $event)" + /> </template> <template #head(status)="{ label }"> @@ -140,6 +161,21 @@ export default { </runner-stacked-summary-cell> </template> + <template #head(owner)="{ label }"> + {{ label }} + <help-popover> + {{ + s__( + 'Runners|The project, group or instance where the runner was registered. Instance runners are always owned by Administrator.', + ) + }} + </help-popover> + </template> + + <template #cell(owner)="{ item }"> + <runner-owner-cell :runner="item" /> + </template> + <template #cell(actions)="{ item }"> <slot name="runner-actions-cell" :runner="item"></slot> </template> diff --git a/app/assets/javascripts/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/runner/components/runner_list_empty_state.vue index ab9cde6a401..e6576c83e69 100644 --- a/app/assets/javascripts/runner/components/runner_list_empty_state.vue +++ b/app/assets/javascripts/runner/components/runner_list_empty_state.vue @@ -53,7 +53,7 @@ export default { :svg-path="svgPath" :svg-height="$options.svgHeight" > - <template #description> + <template v-if="registrationToken" #description> <gl-sprintf :message=" s__( @@ -71,5 +71,12 @@ export default { :registration-token="registrationToken" /> </template> + <template v-else #description> + {{ + s__( + 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.', + ) + }} + </template> </gl-empty-state> </template> diff --git a/app/assets/javascripts/runner/components/runner_membership_toggle.vue b/app/assets/javascripts/runner/components/runner_membership_toggle.vue new file mode 100644 index 00000000000..2b37b1cc797 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_membership_toggle.vue @@ -0,0 +1,42 @@ +<script> +import { GlToggle } from '@gitlab/ui'; +import { + I18N_SHOW_ONLY_INHERITED, + MEMBERSHIP_DESCENDANTS, + MEMBERSHIP_ALL_AVAILABLE, +} from '../constants'; + +export default { + components: { + GlToggle, + }, + props: { + value: { + type: String, + default: MEMBERSHIP_DESCENDANTS, + required: false, + }, + }, + computed: { + toggle() { + return this.value === MEMBERSHIP_DESCENDANTS; + }, + }, + methods: { + onChange(value) { + this.$emit('input', value ? MEMBERSHIP_DESCENDANTS : MEMBERSHIP_ALL_AVAILABLE); + }, + }, + I18N_SHOW_ONLY_INHERITED, +}; +</script> + +<template> + <gl-toggle + data-testid="runner-membership-toggle" + :value="toggle" + :label="$options.I18N_SHOW_ONLY_INHERITED" + label-position="left" + @change="onChange" + /> +</template> diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue index 59230bb809e..6e7c41885f8 100644 --- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue +++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue @@ -7,6 +7,12 @@ import { s__ } from '~/locale'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { RUNNER_TAG_BG_CLASS } from '../../constants'; +// 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 +// 3) consider the scope of search, like searching within the tags of a group +// See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796 export const TAG_SUGGESTIONS_PATH = '/admin/runners/tag_list.json'; export default { @@ -29,12 +35,6 @@ export default { }, methods: { 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 - // 3) consider the scope of search, like searching within the tags of a group - // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796 return axios .get(TAG_SUGGESTIONS_PATH, { params: { @@ -46,6 +46,12 @@ export default { }); }, async fetchTags(searchTerm) { + // Note: Suggestions should only be enabled for admin users + if (this.config.suggestionsDisabled) { + this.tags = []; + return; + } + this.loading = true; try { this.tags = await this.getTagsOptions(searchTerm); diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index 3009577599f..dfc5f0c4152 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -11,6 +11,9 @@ export const RUNNER_DETAILS_JOBS_PAGE_SIZE = 30; 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 FILTER_CSS_CLASSES = + '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'; + // Type export const I18N_ALL_TYPES = s__('Runners|All'); @@ -76,12 +79,6 @@ export const I18N_RESUME = __('Resume'); export const I18N_RESUME_TOOLTIP = s__('Runners|Resume accepting jobs'); export const I18N_DELETE_RUNNER = s__('Runners|Delete runner'); -export const I18N_DELETE_DISABLED_MANY_PROJECTS = s__( - 'Runners|Multi-project runners cannot be deleted', -); -export const I18N_DELETE_DISABLED_UNKNOWN_REASON = s__( - 'Runners|Runner cannot be deleted, please contact your administrator', -); export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted'); // List @@ -91,6 +88,8 @@ export const I18N_LOCKED_RUNNER_DESCRIPTION = s__( export const I18N_VERSION_LABEL = s__('Runners|Version %{version}'); export const I18N_LAST_CONTACT_LABEL = s__('Runners|Last contact: %{timeAgo}'); export const I18N_CREATED_AT_LABEL = s__('Runners|Created %{timeAgo}'); +export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited'); +export const I18N_ADMIN = s__('Runners|Administrator'); // Runner details @@ -116,6 +115,7 @@ export const PARAM_KEY_PAUSED = 'paused'; export const PARAM_KEY_RUNNER_TYPE = 'runner_type'; export const PARAM_KEY_TAG = 'tag'; export const PARAM_KEY_SEARCH = 'search'; +export const PARAM_KEY_MEMBERSHIP = 'membership'; export const PARAM_KEY_SORT = 'sort'; export const PARAM_KEY_AFTER = 'after'; @@ -148,6 +148,13 @@ export const CONTACTED_ASC = 'CONTACTED_ASC'; export const DEFAULT_SORT = CREATED_DESC; +// CiRunnerMembershipFilter + +export const MEMBERSHIP_DESCENDANTS = 'DESCENDANTS'; +export const MEMBERSHIP_ALL_AVAILABLE = 'ALL_AVAILABLE'; + +export const DEFAULT_MEMBERSHIP = MEMBERSHIP_DESCENDANTS; + // Local storage namespaces export const ADMIN_FILTERED_SEARCH_NAMESPACE = 'admin_runners'; diff --git a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql index 4c519b9b867..95f9dd1beb9 100644 --- a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql +++ b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql @@ -2,6 +2,7 @@ query getGroupRunners( $groupFullPath: ID! + $membership: CiRunnerMembershipFilter $before: String $after: String $first: Int @@ -9,13 +10,14 @@ query getGroupRunners( $paused: Boolean $status: CiRunnerStatus $type: CiRunnerType + $tagList: [String!] $search: String $sort: CiRunnerSort ) { group(fullPath: $groupFullPath) { id # Apollo required runners( - membership: DESCENDANTS + membership: $membership before: $before after: $after first: $first @@ -23,6 +25,7 @@ query getGroupRunners( paused: $paused status: $status type: $type + tagList: $tagList search: $search sort: $sort ) { diff --git a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql index 958b4ea0dd3..e88a2c2e7e6 100644 --- a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql +++ b/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql @@ -1,5 +1,6 @@ query getGroupRunnersCount( $groupFullPath: ID! + $membership: CiRunnerMembershipFilter $paused: Boolean $status: CiRunnerStatus $type: CiRunnerType @@ -9,7 +10,7 @@ query getGroupRunnersCount( group(fullPath: $groupFullPath) { id # Apollo required runners( - membership: DESCENDANTS + membership: $membership paused: $paused status: $status type: $type diff --git a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql index a12ba7a751a..0dff011daaa 100644 --- a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql @@ -16,4 +16,18 @@ fragment ListItemShared on CiRunner { updateRunner deleteRunner } + groups(first: 1) { + nodes { + id + name + fullName + webUrl + } + } + ownerProject { + id + name + nameWithNamespace + webUrl + } } diff --git a/app/assets/javascripts/runner/graphql/list/local_state.js b/app/assets/javascripts/runner/graphql/list/local_state.js index 154af261bba..e0477c660b4 100644 --- a/app/assets/javascripts/runner/graphql/list/local_state.js +++ b/app/assets/javascripts/runner/graphql/list/local_state.js @@ -20,10 +20,6 @@ import typeDefs from './typedefs.graphql'; * localMutations.setRunnerChecked( ... ) * ``` * - * Note: Currently only in use behind a feature flag: - * admin_runners_bulk_delete for the admin list, rollout issue: - * https://gitlab.com/gitlab-org/gitlab/-/issues/353981 - * * @returns {Object} An object to configure an Apollo client: * contains cacheConfig, typeDefs, localMutations. */ @@ -52,16 +48,18 @@ export const createLocalState = () => { const localMutations = { setRunnerChecked({ runner, isChecked }) { - checkedRunnerIdsVar({ - ...checkedRunnerIdsVar(), - [runner.id]: isChecked, - }); + const { id, userPermissions } = runner; + if (userPermissions?.deleteRunner) { + checkedRunnerIdsVar({ + ...checkedRunnerIdsVar(), + [id]: isChecked, + }); + } }, setRunnersChecked({ runners, isChecked }) { - const newVal = runners.reduce( - (acc, { id }) => ({ ...acc, [id]: isChecked }), - checkedRunnerIdsVar(), - ); + const newVal = runners + .filter(({ userPermissions }) => userPermissions?.deleteRunner) + .reduce((acc, { id }) => ({ ...acc, [id]: isChecked }), checkedRunnerIdsVar()); checkedRunnerIdsVar(newVal); }, clearChecked() { 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 70826a6bfa1..7f56d895682 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -10,7 +10,9 @@ import { fromSearchToVariables, isSearchFiltered, } from 'ee_else_ce/runner/runner_search_utils'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql'; +import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue'; @@ -22,14 +24,17 @@ import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerPagination from '../components/runner_pagination.vue'; import RunnerTypeTabs from '../components/runner_type_tabs.vue'; import RunnerActionsCell from '../components/cells/runner_actions_cell.vue'; +import RunnerMembershipToggle from '../components/runner_membership_toggle.vue'; import { pausedTokenConfig } from '../components/search_tokens/paused_token_config'; import { statusTokenConfig } from '../components/search_tokens/status_token_config'; +import { tagTokenConfig } from '../components/search_tokens/tag_token_config'; import { GROUP_FILTERED_SEARCH_NAMESPACE, GROUP_TYPE, PROJECT_TYPE, I18N_FETCH_ERROR, + FILTER_CSS_CLASSES, } from '../constants'; import { captureException } from '../sentry_utils'; @@ -43,11 +48,13 @@ export default { RunnerList, RunnerListEmptyState, RunnerName, + RunnerMembershipToggle, RunnerStats, RunnerPagination, RunnerTypeTabs, RunnerActionsCell, }, + mixins: [glFeatureFlagMixin()], inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'], props: { registrationToken: { @@ -126,12 +133,20 @@ export default { noRunnersFound() { return !this.runnersLoading && !this.runners.items.length; }, - searchTokens() { - return [pausedTokenConfig, statusTokenConfig, upgradeStatusTokenConfig]; - }, filteredSearchNamespace() { return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`; }, + searchTokens() { + return [ + pausedTokenConfig, + statusTokenConfig, + { + ...tagTokenConfig, + suggestionsDisabled: true, + }, + upgradeStatusTokenConfig, + ]; + }, isSearchFiltered() { return isSearchFiltered(this.search); }, @@ -159,13 +174,17 @@ export default { editUrl(runner) { return this.runners.urlsById[runner.id]?.edit; }, + refetchCounts() { + this.$apollo.getClient().refetchQueries({ include: [groupRunnersCountQuery] }); + }, onToggledPaused() { // When a runner becomes Paused, the tab count can // become stale, refetch outdated counts. - this.$refs['runner-type-tabs'].refetch(); + this.refetchCounts(); }, onDeleted({ message }) { this.$root.$toast?.show(message); + this.refetchCounts(); }, reportToSentry(error) { captureException({ error, component: this.$options.name }); @@ -176,6 +195,7 @@ export default { }, TABS_RUNNER_TYPES: [GROUP_TYPE, PROJECT_TYPE], GROUP_TYPE, + FILTER_CSS_CLASSES, }; </script> @@ -204,11 +224,21 @@ export default { /> </div> - <runner-filtered-search-bar - v-model="search" - :tokens="searchTokens" - :namespace="filteredSearchNamespace" - /> + <div + class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-gap-3" + :class="$options.FILTER_CSS_CLASSES" + > + <runner-filtered-search-bar + v-model="search" + :tokens="searchTokens" + :namespace="filteredSearchNamespace" + class="gl-flex-grow-1 gl-align-self-stretch" + /> + <runner-membership-toggle + v-model="search.membership" + class="gl-align-self-end gl-md-align-self-center" + /> + </div> <runner-stats :scope="$options.GROUP_TYPE" :variables="countVariables" /> @@ -220,7 +250,7 @@ export default { :filtered-svg-path="emptyStateFilteredSvgPath" /> <template v-else> - <runner-list :runners="runners.items" :loading="runnersLoading"> + <runner-list :runners="runners.items" :loading="runnersLoading" @deleted="onDeleted"> <template #runner-name="{ runner }"> <gl-link :href="webUrl(runner)"> <runner-name :runner="runner" /> diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js index feed6b0ceb7..0e7efd2b8a1 100644 --- a/app/assets/javascripts/runner/group_runners/index.js +++ b/app/assets/javascripts/runner/group_runners/index.js @@ -2,6 +2,7 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; +import { createLocalState } from '../graphql/list/local_state'; import GroupRunnersApp from './group_runners_app.vue'; Vue.use(GlToast); @@ -26,8 +27,10 @@ export const initGroupRunners = (selector = '#js-group-runners') => { emptyStateFilteredSvgPath, } = el.dataset; + const { cacheConfig, typeDefs, localMutations } = createLocalState(); + const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }), }); return new Vue({ @@ -35,6 +38,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => { apolloProvider, provide: { runnerInstallHelpPage, + localMutations, groupId, onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10), staleTimeoutSecs: parseInt(staleTimeoutSecs, 10), diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js index dc582ccbac1..adc832b0600 100644 --- a/app/assets/javascripts/runner/runner_search_utils.js +++ b/app/assets/javascripts/runner/runner_search_utils.js @@ -13,10 +13,12 @@ import { PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG, PARAM_KEY_SEARCH, + PARAM_KEY_MEMBERSHIP, PARAM_KEY_SORT, PARAM_KEY_AFTER, PARAM_KEY_BEFORE, DEFAULT_SORT, + DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE, } from './constants'; import { getPaginationVariables } from './utils'; @@ -57,9 +59,10 @@ import { getPaginationVariables } from './utils'; * @param {Object} search * @returns {boolean} True if the value follows the search format. */ -export const searchValidator = ({ runnerType, filters, sort }) => { +export const searchValidator = ({ runnerType, membership, filters, sort }) => { return ( (runnerType === null || typeof runnerType === 'string') && + (membership === null || typeof membership === 'string') && Array.isArray(filters) && typeof sort === 'string' ); @@ -140,9 +143,11 @@ export const updateOutdatedUrl = (url = window.location.href) => { export const fromUrlQueryToSearch = (query = window.location.search) => { const params = queryToObject(query, { gatherArrays: true }); const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null; + const membership = params[PARAM_KEY_MEMBERSHIP]?.[0] || null; return { runnerType, + membership: membership || DEFAULT_MEMBERSHIP, filters: prepareTokens( urlQueryToFilter(query, { filterNamesAllowList: [PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG], @@ -162,13 +167,14 @@ export const fromUrlQueryToSearch = (query = window.location.search) => { * @returns {String} New URL for the page */ export const fromSearchToUrl = ( - { runnerType = null, filters = [], sort = null, pagination = {} }, + { runnerType = null, membership = null, filters = [], sort = null, pagination = {} }, url = window.location.href, ) => { const filterParams = { // Defaults [PARAM_KEY_STATUS]: [], [PARAM_KEY_RUNNER_TYPE]: [], + [PARAM_KEY_MEMBERSHIP]: [], [PARAM_KEY_TAG]: [], // Current filters ...filterToQueryObject(processFilters(filters), { @@ -180,6 +186,10 @@ export const fromSearchToUrl = ( filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType]; } + if (membership && membership !== DEFAULT_MEMBERSHIP) { + filterParams[PARAM_KEY_MEMBERSHIP] = [membership]; + } + if (!filterParams[PARAM_KEY_SEARCH]) { filterParams[PARAM_KEY_SEARCH] = null; } @@ -203,6 +213,7 @@ export const fromSearchToUrl = ( */ export const fromSearchToVariables = ({ runnerType = null, + membership = null, filters = [], sort = null, pagination = {}, @@ -226,6 +237,9 @@ export const fromSearchToVariables = ({ if (runnerType) { filterVariables.type = runnerType; } + if (membership) { + filterVariables.membership = membership; + } if (sort) { filterVariables.sort = sort; } |