diff options
Diffstat (limited to 'app/assets/javascripts/runner/components')
10 files changed, 182 insertions, 57 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 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); |