diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-31 00:09:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-31 00:09:29 +0300 |
commit | 89245154567c6ea01821606f3127bef766462d5e (patch) | |
tree | 2fa9adfe5c3bad9deadcbf80b07c0a1da821d060 /app/assets/javascripts/runner | |
parent | f06ebebadece98495408c29112abf1b65a43ecb6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/runner')
8 files changed, 277 insertions, 14 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 9abd45424e7..3853a7d8666 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -4,9 +4,11 @@ import { createAlert } from '~/flash'; import { updateHistory } from '~/lib/utils/url_utility'; import { formatNumber } from '~/locale'; import { fetchPolicies } from '~/lib/graphql'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; +import RunnerBulkDelete from '../components/runner_bulk_delete.vue'; import RunnerList from '../components/runner_list.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerStats from '../components/stat/runner_stats.vue'; @@ -53,6 +55,7 @@ export default { GlLink, RegistrationDropdown, RunnerFilteredSearchBar, + RunnerBulkDelete, RunnerList, RunnerName, RunnerStats, @@ -60,6 +63,8 @@ export default { RunnerTypeTabs, RunnerActionsCell, }, + mixins: [glFeatureFlagMixin()], + inject: ['localMutations'], props: { registrationToken: { type: String, @@ -180,6 +185,11 @@ export default { }, ]; }, + isBulkDeleteEnabled() { + // Feature flag: admin_runners_bulk_delete + // Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981 + return this.glFeatures.adminRunnersBulkDelete; + }, }, watch: { search: { @@ -238,6 +248,12 @@ export default { reportToSentry(error) { captureException({ error, component: this.$options.name }); }, + onChecked({ runner, isChecked }) { + this.localMutations.setRunnerChecked({ + runner, + isChecked, + }); + }, }, filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, @@ -286,7 +302,13 @@ export default { {{ __('No runners found') }} </div> <template v-else> - <runner-list :runners="runners.items" :loading="runnersLoading"> + <runner-bulk-delete v-if="isBulkDeleteEnabled" /> + <runner-list + :runners="runners.items" + :loading="runnersLoading" + :checkable="isBulkDeleteEnabled" + @checked="onChecked" + > <template #runner-name="{ runner }"> <gl-link :href="runner.adminUrl"> <runner-name :runner="runner" /> diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js index 3b8a8fe9cd1..2405bab7957 100644 --- a/app/assets/javascripts/runner/admin_runners/index.js +++ b/app/assets/javascripts/runner/admin_runners/index.js @@ -1,9 +1,10 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import createDefaultClient from '~/lib/graphql'; import { visitUrl } from '~/lib/utils/url_utility'; import { updateOutdatedUrl } from '~/runner/runner_search_utils'; +import createDefaultClient from '~/lib/graphql'; +import { createLocalState } from '../graphql/list/local_state'; import AdminRunnersApp from './admin_runners_app.vue'; Vue.use(GlToast); @@ -27,8 +28,10 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { const { runnerInstallHelpPage, registrationToken } = el.dataset; + const { cacheConfig, typeDefs, localMutations } = createLocalState(); + const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }), }); return new Vue({ @@ -36,6 +39,7 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { apolloProvider, provide: { runnerInstallHelpPage, + localMutations, }, render(h) { return h(AdminRunnersApp, { diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete.vue b/app/assets/javascripts/runner/components/runner_bulk_delete.vue new file mode 100644 index 00000000000..50791de0bda --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_bulk_delete.vue @@ -0,0 +1,111 @@ +<script> +import { GlButton, GlModalDirective, GlSprintf } from '@gitlab/ui'; +import { n__, sprintf } from '~/locale'; +import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; +import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql'; + +export default { + components: { + GlButton, + GlSprintf, + }, + directives: { + GlModal: GlModalDirective, + }, + inject: ['localMutations'], + data() { + return { + checkedRunnerIds: [], + }; + }, + apollo: { + checkedRunnerIds: { + query: checkedRunnerIdsQuery, + }, + }, + computed: { + checkedCount() { + return this.checkedRunnerIds.length || 0; + }, + bannerMessage() { + return sprintf( + n__( + 'Runners|%{strongStart}%{count}%{strongEnd} runner selected', + 'Runners|%{strongStart}%{count}%{strongEnd} runners selected', + this.checkedCount, + ), + { + count: this.checkedCount, + }, + ); + }, + modalTitle() { + return n__('Runners|Delete %d runner', 'Runners|Delete %d runners', this.checkedCount); + }, + modalHtmlMessage() { + return sprintf( + n__( + 'Runners|%{strongStart}%{count}%{strongEnd} runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?', + 'Runners|%{strongStart}%{count}%{strongEnd} runners will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?', + this.checkedCount, + ), + { + strongStart: '<strong>', + strongEnd: '</strong>', + count: this.checkedCount, + }, + false, + ); + }, + primaryBtnText() { + return n__( + 'Runners|Permanently delete %d runner', + 'Runners|Permanently delete %d runners', + this.checkedCount, + ); + }, + }, + methods: { + onClearChecked() { + this.localMutations.clearChecked(); + }, + onClickDelete: ignoreWhilePending(async function onClickDelete() { + const confirmed = await confirmAction(null, { + title: this.modalTitle, + modalHtmlMessage: this.modalHtmlMessage, + primaryBtnVariant: 'danger', + primaryBtnText: this.primaryBtnText, + }); + + if (confirmed) { + // TODO Call $apollo.mutate with list of runner + // ids in `this.checkedRunnerIds`. + // See https://gitlab.com/gitlab-org/gitlab/-/issues/339525/ + } + }), + }, +}; +</script> + +<template> + <div v-if="checkedCount" class="gl-my-4 gl-p-4 gl-border-1 gl-border-solid gl-border-gray-100"> + <div class="gl-display-flex gl-align-items-center"> + <div> + <gl-sprintf :message="bannerMessage"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </div> + <div class="gl-ml-auto"> + <gl-button data-testid="clear-btn" variant="default" @click="onClearChecked">{{ + s__('Runners|Clear selection') + }}</gl-button> + <gl-button data-testid="delete-btn" variant="danger" @click="onClickDelete">{{ + s__('Runners|Delete selected') + }}</gl-button> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index 819b6951e13..05cd1879d9d 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -4,11 +4,22 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/toolt import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __, s__ } from '~/locale'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql'; import { formatJobCount, tableField } from '../utils'; import RunnerSummaryCell from './cells/runner_summary_cell.vue'; import RunnerStatusCell from './cells/runner_status_cell.vue'; import RunnerTags from './runner_tags.vue'; +const defaultFields = [ + tableField({ key: 'status', label: s__('Runners|Status') }), + tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }), + tableField({ key: 'version', label: __('Version') }), + tableField({ key: 'jobCount', label: __('Jobs') }), + tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }), + tableField({ key: 'contactedAt', label: __('Last contact') }), + tableField({ key: 'actions', label: '' }), +]; + export default { components: { GlTableLite, @@ -22,7 +33,20 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + apollo: { + checkedRunnerIds: { + query: checkedRunnerIdsQuery, + skip() { + return !this.checkable; + }, + }, + }, props: { + checkable: { + type: Boolean, + required: false, + default: false, + }, loading: { type: Boolean, required: false, @@ -33,6 +57,10 @@ export default { required: true, }, }, + emits: ['checked'], + data() { + return { checkedRunnerIds: [] }; + }, computed: { tableClass() { // <gl-table-lite> does not provide a busy state, add @@ -42,6 +70,18 @@ export default { 'gl-opacity-6': this.loading, }; }, + fields() { + if (this.checkable) { + const checkboxField = tableField({ + key: 'checkbox', + label: s__('Runners|Checkbox'), + thClasses: ['gl-w-9'], + tdClass: ['gl-text-center'], + }); + return [checkboxField, ...defaultFields]; + } + return defaultFields; + }, }, methods: { formatJobCount(jobCount) { @@ -55,16 +95,16 @@ export default { } return {}; }, + onCheckboxChange(runner, isChecked) { + this.$emit('checked', { + runner, + isChecked, + }); + }, + isChecked(runner) { + return this.checkedRunnerIds.includes(runner.id); + }, }, - fields: [ - tableField({ key: 'status', label: s__('Runners|Status') }), - tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }), - tableField({ key: 'version', label: __('Version') }), - tableField({ key: 'jobCount', label: __('Jobs') }), - tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }), - tableField({ key: 'contactedAt', label: __('Last contact') }), - tableField({ key: 'actions', label: '' }), - ], }; </script> <template> @@ -73,13 +113,29 @@ export default { :aria-busy="loading" :class="tableClass" :items="runners" - :fields="$options.fields" + :fields="fields" :tbody-tr-attr="runnerTrAttr" data-testid="runner-list" stacked="md" primary-key="id" fixed > + <template #head(checkbox)> + <!-- + Checkbox to select all to be added here + See https://gitlab.com/gitlab-org/gitlab/-/issues/339525/ + --> + <span></span> + </template> + + <template #cell(checkbox)="{ item }"> + <input + type="checkbox" + :checked="isChecked(item)" + @change="onCheckboxChange(item, $event.target.checked)" + /> + </template> + <template #cell(status)="{ item }"> <runner-status-cell :runner="item" /> </template> diff --git a/app/assets/javascripts/runner/graphql/list/checked_runner_ids.query.graphql b/app/assets/javascripts/runner/graphql/list/checked_runner_ids.query.graphql new file mode 100644 index 00000000000..c01f1edb451 --- /dev/null +++ b/app/assets/javascripts/runner/graphql/list/checked_runner_ids.query.graphql @@ -0,0 +1,3 @@ +query getCheckedRunnerIds { + checkedRunnerIds @client +} diff --git a/app/assets/javascripts/runner/graphql/list/local_state.js b/app/assets/javascripts/runner/graphql/list/local_state.js new file mode 100644 index 00000000000..e87bc72c86a --- /dev/null +++ b/app/assets/javascripts/runner/graphql/list/local_state.js @@ -0,0 +1,63 @@ +import { makeVar } from '@apollo/client/core'; +import typeDefs from './typedefs.graphql'; + +/** + * Local state for checkable runner items. + * + * Usage: + * + * ``` + * import { createLocalState } from '~/runner/graphql/list/local_state'; + * + * // initialize local state + * const { cacheConfig, typeDefs, localMutations } = createLocalState(); + * + * // configure the client + * apolloClient = createApolloClient({}, { cacheConfig, typeDefs }); + * + * // modify local state + * 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. + */ +export const createLocalState = () => { + const checkedRunnerIdsVar = makeVar({}); + + const cacheConfig = { + typePolicies: { + Query: { + fields: { + checkedRunnerIds() { + return Object.entries(checkedRunnerIdsVar()) + .filter(([, isChecked]) => isChecked) + .map(([key]) => key); + }, + }, + }, + }, + }; + + const localMutations = { + setRunnerChecked({ runner, isChecked }) { + checkedRunnerIdsVar({ + ...checkedRunnerIdsVar(), + [runner.id]: isChecked, + }); + }, + clearChecked() { + checkedRunnerIdsVar({}); + }, + }; + + return { + cacheConfig, + typeDefs, + localMutations, + }; +}; diff --git a/app/assets/javascripts/runner/graphql/list/typedefs.graphql b/app/assets/javascripts/runner/graphql/list/typedefs.graphql new file mode 100644 index 00000000000..24e9e20cc8c --- /dev/null +++ b/app/assets/javascripts/runner/graphql/list/typedefs.graphql @@ -0,0 +1,3 @@ +extend type Query { + checkedRunnerIds: [ID!]! +} diff --git a/app/assets/javascripts/runner/utils.js b/app/assets/javascripts/runner/utils.js index 6e4c8c45e7b..1f7794720de 100644 --- a/app/assets/javascripts/runner/utils.js +++ b/app/assets/javascripts/runner/utils.js @@ -24,7 +24,7 @@ export const formatJobCount = (jobCount) => { * @param {Object} options * @returns Field object to add to GlTable fields */ -export const tableField = ({ key, label = '', thClasses = [] }) => { +export const tableField = ({ key, label = '', thClasses = [], ...options }) => { return { key, label, @@ -32,6 +32,7 @@ export const tableField = ({ key, label = '', thClasses = [] }) => { tdAttr: { 'data-testid': `td-${key}`, }, + ...options, }; }; |