diff options
Diffstat (limited to 'app/assets/javascripts/runner')
26 files changed, 465 insertions, 183 deletions
diff --git a/app/assets/javascripts/runner/runner_details/runner_details_app.vue b/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue index 6557a7834e7..4d2ca9b0c58 100644 --- a/app/assets/javascripts/runner/runner_details/runner_details_app.vue +++ b/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue @@ -1,20 +1,17 @@ <script> -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; -import { sprintf } from '~/locale'; -import RunnerTypeAlert from '../components/runner_type_alert.vue'; -import RunnerTypeBadge from '../components/runner_type_badge.vue'; +import RunnerHeader from '../components/runner_header.vue'; import RunnerUpdateForm from '../components/runner_update_form.vue'; -import { I18N_DETAILS_TITLE, I18N_FETCH_ERROR } from '../constants'; +import { I18N_FETCH_ERROR } from '../constants'; import getRunnerQuery from '../graphql/get_runner.query.graphql'; import { captureException } from '../sentry_utils'; export default { - name: 'RunnerDetailsApp', + name: 'AdminRunnerEditApp', components: { - RunnerTypeAlert, - RunnerTypeBadge, + RunnerHeader, RunnerUpdateForm, }, props: { @@ -37,17 +34,12 @@ export default { }; }, error(error) { - createFlash({ message: I18N_FETCH_ERROR }); + createAlert({ message: I18N_FETCH_ERROR }); this.reportToSentry(error); }, }, }, - computed: { - pageTitle() { - return sprintf(I18N_DETAILS_TITLE, { runner_id: this.runnerId }); - }, - }, errorCaptured(error) { this.reportToSentry(error); }, @@ -60,12 +52,7 @@ export default { </script> <template> <div> - <h2 class="page-title"> - {{ pageTitle }} <runner-type-badge v-if="runner" :type="runner.runnerType" /> - </h2> - - <runner-type-alert v-if="runner" :type="runner.runnerType" /> - + <runner-header v-if="runner" :runner="runner" /> <runner-update-form :runner="runner" class="gl-my-5" /> </div> </template> diff --git a/app/assets/javascripts/runner/runner_details/index.js b/app/assets/javascripts/runner/admin_runner_edit/index.js index db8f239a3c3..adb420f9963 100644 --- a/app/assets/javascripts/runner/runner_details/index.js +++ b/app/assets/javascripts/runner/admin_runner_edit/index.js @@ -1,11 +1,11 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import RunnerDetailsApp from './runner_details_app.vue'; +import AdminRunnerEditApp from './admin_runner_edit_app.vue'; Vue.use(VueApollo); -export const initRunnerDetail = (selector = '#js-runner-details') => { +export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => { const el = document.querySelector(selector); if (!el) { @@ -22,7 +22,7 @@ export const initRunnerDetail = (selector = '#js-runner-details') => { el, apolloProvider, render(h) { - return h(RunnerDetailsApp, { + return h(AdminRunnerEditApp, { props: { runnerId, }, 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 f8220553db6..bb2bac531a7 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -1,14 +1,15 @@ <script> import { GlBadge, GlLink } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { fetchPolicies } from '~/lib/graphql'; import { updateHistory } from '~/lib/utils/url_utility'; +import { formatNumber } 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 RunnerName from '../components/runner_name.vue'; -import RunnerOnlineStat from '../components/stat/runner_online_stat.vue'; +import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerPagination from '../components/runner_pagination.vue'; import RunnerTypeTabs from '../components/runner_type_tabs.vue'; @@ -19,9 +20,13 @@ import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, + STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, I18N_FETCH_ERROR, } from '../constants'; import getRunnersQuery from '../graphql/get_runners.query.graphql'; +import getRunnersCountQuery from '../graphql/get_runners_count.query.graphql'; import { fromUrlQueryToSearch, fromSearchToUrl, @@ -29,6 +34,17 @@ import { } from '../runner_search_utils'; import { captureException } from '../sentry_utils'; +const runnersCountSmartQuery = { + query: getRunnersCountQuery, + fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, + update(data) { + return data?.runners?.count; + }, + error(error) { + this.reportToSentry(error); + }, +}; + export default { name: 'AdminRunnersApp', components: { @@ -38,7 +54,7 @@ export default { RunnerFilteredSearchBar, RunnerList, RunnerName, - RunnerOnlineStat, + RunnerStats, RunnerPagination, RunnerTypeTabs, }, @@ -47,26 +63,6 @@ export default { type: String, required: true, }, - activeRunnersCount: { - type: String, - required: true, - }, - allRunnersCount: { - type: String, - required: true, - }, - instanceRunnersCount: { - type: String, - required: true, - }, - groupRunnersCount: { - type: String, - required: true, - }, - projectRunnersCount: { - type: String, - required: true, - }, }, data() { return { @@ -95,16 +91,78 @@ export default { }; }, error(error) { - createFlash({ message: I18N_FETCH_ERROR }); + createAlert({ message: I18N_FETCH_ERROR }); this.reportToSentry(error); }, }, + allRunnersCount: { + ...runnersCountSmartQuery, + variables() { + return this.countVariables; + }, + }, + instanceRunnersCount: { + ...runnersCountSmartQuery, + variables() { + return { + ...this.countVariables, + type: INSTANCE_TYPE, + }; + }, + }, + groupRunnersCount: { + ...runnersCountSmartQuery, + variables() { + return { + ...this.countVariables, + type: GROUP_TYPE, + }; + }, + }, + projectRunnersCount: { + ...runnersCountSmartQuery, + variables() { + return { + ...this.countVariables, + type: PROJECT_TYPE, + }; + }, + }, + onlineRunnersTotal: { + ...runnersCountSmartQuery, + variables() { + return { + status: STATUS_ONLINE, + }; + }, + }, + offlineRunnersTotal: { + ...runnersCountSmartQuery, + variables() { + return { + status: STATUS_OFFLINE, + }; + }, + }, + staleRunnersTotal: { + ...runnersCountSmartQuery, + variables() { + return { + status: STATUS_STALE, + }; + }, + }, }, computed: { variables() { return fromSearchToVariables(this.search); }, + countVariables() { + // Exclude pagination variables, leave only filters variables + const { sort, before, last, after, first, ...countVariables } = this.variables; + return countVariables; + }, runnersLoading() { return this.$apollo.queries.runners.loading; }, @@ -125,7 +183,7 @@ export default { search: { deep: true, handler() { - // TODO Implement back button reponse using onpopstate + // TODO Implement back button response using onpopstate updateHistory({ url: fromSearchToUrl(this.search), title: document.title, @@ -138,18 +196,27 @@ export default { }, methods: { tabCount({ runnerType }) { + let count; switch (runnerType) { case null: - return this.allRunnersCount; + count = this.allRunnersCount; + break; case INSTANCE_TYPE: - return this.instanceRunnersCount; + count = this.instanceRunnersCount; + break; case GROUP_TYPE: - return this.groupRunnersCount; + count = this.groupRunnersCount; + break; case PROJECT_TYPE: - return this.projectRunnersCount; + count = this.projectRunnersCount; + break; default: return null; } + if (typeof count === 'number') { + return formatNumber(count); + } + return ''; }, reportToSentry(error) { captureException({ error, component: this.$options.name }); @@ -161,7 +228,11 @@ export default { </script> <template> <div> - <runner-online-stat class="gl-py-6 gl-px-5" :value="activeRunnersCount" /> + <runner-stats + :online-runners-count="onlineRunnersTotal" + :offline-runners-count="offlineRunnersTotal" + :stale-runners-count="staleRunnersTotal" + /> <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" diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js index 62da6cbfa2b..3b8a8fe9cd1 100644 --- a/app/assets/javascripts/runner/admin_runners/index.js +++ b/app/assets/javascripts/runner/admin_runners/index.js @@ -2,6 +2,8 @@ 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 AdminRunnersApp from './admin_runners_app.vue'; Vue.use(GlToast); @@ -14,18 +16,16 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { return null; } - // TODO `activeRunnersCount` should be implemented using a GraphQL API - // https://gitlab.com/gitlab-org/gitlab/-/issues/333806 - const { - runnerInstallHelpPage, - registrationToken, + // Redirect outdated URLs + const updatedUrlQuery = updateOutdatedUrl(); + if (updatedUrlQuery) { + visitUrl(updatedUrlQuery); - activeRunnersCount, - allRunnersCount, - instanceRunnersCount, - groupRunnersCount, - projectRunnersCount, - } = el.dataset; + // Prevent mounting the rest of the app, redirecting now. + return null; + } + + const { runnerInstallHelpPage, registrationToken } = el.dataset; const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), @@ -41,14 +41,6 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { return h(AdminRunnersApp, { props: { 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 33f7a67aba4..0934508c87f 100644 --- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue @@ -1,6 +1,6 @@ <script> import { GlButton, GlButtonGroup, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __, s__, sprintf } from '~/locale'; import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql'; import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql'; @@ -69,6 +69,12 @@ export default { runnerDeleteModalId() { return `delete-runner-modal-${this.runnerId}`; }, + canUpdate() { + return this.runner.userPermissions?.updateRunner; + }, + canDelete() { + return this.runner.userPermissions?.deleteRunner; + }, }, methods: { async onToggleActive() { @@ -133,7 +139,7 @@ export default { onError(error) { const { message } = error; - createFlash({ message }); + createAlert({ message }); this.reportToSentry(error); }, @@ -156,14 +162,15 @@ export default { See https://gitlab.com/gitlab-org/gitlab/-/issues/334802 --> <gl-button - v-if="runner.adminUrl" + v-if="canUpdate && runner.editAdminUrl" v-gl-tooltip.hover.viewport="$options.I18N_EDIT" - :href="runner.adminUrl" + :href="runner.editAdminUrl" :aria-label="$options.I18N_EDIT" icon="pencil" data-testid="edit-runner" /> <gl-button + v-if="canUpdate" v-gl-tooltip.hover.viewport="toggleActiveTitle" :aria-label="toggleActiveTitle" :icon="toggleActiveIcon" @@ -172,6 +179,7 @@ export default { @click="onToggleActive" /> <gl-button + v-if="canDelete" v-gl-tooltip.hover.viewport="deleteTitle" v-gl-modal="runnerDeleteModalId" :aria-label="deleteTitle" @@ -182,6 +190,7 @@ export default { /> <runner-delete-modal + v-if="canDelete" :ref="runnerDeleteModalId" :modal-id="runnerDeleteModalId" :runner-name="runnerName" diff --git a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue index 473cd7e9794..93f86ae2a2c 100644 --- a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue @@ -28,7 +28,15 @@ export default { <template> <div> - <runner-status-badge :runner="runner" size="sm" /> - <runner-paused-badge v-if="paused" size="sm" /> + <runner-status-badge + :runner="runner" + size="sm" + class="gl-display-inline-block gl-max-w-full gl-text-truncate" + /> + <runner-paused-badge + v-if="paused" + size="sm" + class="gl-display-inline-block gl-max-w-full gl-text-truncate" + /> </div> </template> diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue index 3bb15bff8d8..0e259807f98 100644 --- a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue +++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue @@ -1,6 +1,6 @@ <script> -import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { GlDropdownItem, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui'; +import { createAlert } from '~/flash'; import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { __, s__ } from '~/locale'; @@ -10,9 +10,17 @@ import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants'; export default { name: 'RunnerRegistrationTokenReset', + i18n: { + modalTitle: __('Reset registration token'), + modalCopy: __('Are you sure you want to reset the registration token?'), + }, components: { GlDropdownItem, GlLoadingIcon, + GlModal, + }, + directives: { + GlModal: GlModalDirective, }, inject: { groupId: { @@ -22,6 +30,7 @@ export default { default: null, }, }, + modalID: 'token-reset-modal', props: { type: { type: String, @@ -59,14 +68,10 @@ export default { }, }, methods: { + handleModalPrimary() { + this.resetToken(); + }, async resetToken() { - // TODO Replace confirmation with gl-modal - // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810 - // eslint-disable-next-line no-alert - if (!window.confirm(__('Are you sure you want to reset the registration token?'))) { - return; - } - this.loading = true; try { const { @@ -91,7 +96,7 @@ export default { }, onError(error) { const { message } = error; - createFlash({ message }); + createAlert({ message }); this.reportToSentry(error); }, @@ -106,8 +111,15 @@ export default { }; </script> <template> - <gl-dropdown-item @click.capture.native.stop="resetToken"> + <gl-dropdown-item v-gl-modal="$options.modalID"> {{ __('Reset registration token') }} + <gl-modal + :modal-id="$options.modalID" + :title="$options.i18n.modalTitle" + @primary="handleModalPrimary" + > + <p>{{ $options.i18n.modalCopy }}</p> + </gl-modal> <gl-loading-icon v-if="loading" inline /> </gl-dropdown-item> </template> diff --git a/app/assets/javascripts/runner/components/runner_header.vue b/app/assets/javascripts/runner/components/runner_header.vue new file mode 100644 index 00000000000..09f58df7bd0 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_header.vue @@ -0,0 +1,52 @@ +<script> +import { GlSprintf } from '@gitlab/ui'; +import { sprintf } from '~/locale'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { I18N_DETAILS_TITLE } from '../constants'; +import RunnerTypeBadge from './runner_type_badge.vue'; +import RunnerStatusBadge from './runner_status_badge.vue'; + +export default { + components: { + GlSprintf, + TimeAgo, + RunnerTypeBadge, + RunnerStatusBadge, + }, + props: { + runner: { + type: Object, + required: true, + }, + }, + computed: { + paused() { + return !this.runner.active; + }, + heading() { + const id = getIdFromGraphQLId(this.runner.id); + return sprintf(I18N_DETAILS_TITLE, { runner_id: id }); + }, + }, +}; +</script> +<template> + <div class="gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"> + <runner-status-badge :runner="runner" /> + <runner-type-badge v-if="runner" :type="runner.runnerType" /> + <template v-if="runner.createdAt"> + <gl-sprintf :message="__('%{runner} created %{timeago}')"> + <template #runner> + <strong>{{ heading }}</strong> + </template> + <template #timeago> + <time-ago :time="runner.createdAt" /> + </template> + </gl-sprintf> + </template> + <template v-else> + <strong>{{ heading }}</strong> + </template> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/runner_status_badge.vue b/app/assets/javascripts/runner/components/runner_status_badge.vue index 0823876a187..6d0445ecb7a 100644 --- a/app/assets/javascripts/runner/components/runner_status_badge.vue +++ b/app/assets/javascripts/runner/components/runner_status_badge.vue @@ -4,11 +4,10 @@ import { __, s__, sprintf } from '~/locale'; import { getTimeago } from '~/lib/utils/datetime_utility'; import { I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION, - I18N_NOT_CONNECTED_RUNNER_DESCRIPTION, + I18N_NEVER_CONTACTED_RUNNER_DESCRIPTION, I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION, I18N_STALE_RUNNER_DESCRIPTION, STATUS_ONLINE, - STATUS_NOT_CONNECTED, STATUS_NEVER_CONTACTED, STATUS_OFFLINE, STATUS_STALE, @@ -45,12 +44,11 @@ export default { timeAgo: this.contactedAtTimeAgo, }), }; - case STATUS_NOT_CONNECTED: case STATUS_NEVER_CONTACTED: return { variant: 'muted', - label: s__('Runners|not connected'), - tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION, + label: s__('Runners|never contacted'), + tooltip: I18N_NEVER_CONTACTED_RUNNER_DESCRIPTION, }; case STATUS_OFFLINE: return { diff --git a/app/assets/javascripts/runner/components/runner_type_alert.vue b/app/assets/javascripts/runner/components/runner_type_alert.vue deleted file mode 100644 index 1400875a1d6..00000000000 --- a/app/assets/javascripts/runner/components/runner_type_alert.vue +++ /dev/null @@ -1,54 +0,0 @@ -<script> -import { GlAlert, GlLink } from '@gitlab/ui'; -import { helpPagePath } from '~/helpers/help_page_helper'; -import { s__ } from '~/locale'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants'; - -const ALERT_DATA = { - [INSTANCE_TYPE]: { - message: s__( - 'Runners|This runner is available to all groups and projects in your GitLab instance.', - ), - anchor: 'shared-runners', - }, - [GROUP_TYPE]: { - message: s__('Runners|This runner is available to all projects and subgroups in a group.'), - anchor: 'group-runners', - }, - [PROJECT_TYPE]: { - message: s__('Runners|This runner is associated with one or more projects.'), - anchor: 'specific-runners', - }, -}; - -export default { - components: { - GlAlert, - GlLink, - }, - props: { - type: { - type: String, - required: false, - default: null, - validator(type) { - return Boolean(ALERT_DATA[type]); - }, - }, - }, - computed: { - alert() { - return ALERT_DATA[this.type]; - }, - helpHref() { - return helpPagePath('ci/runners/runners_scope', { anchor: this.alert.anchor }); - }, - }, -}; -</script> -<template> - <gl-alert v-if="alert" variant="info" :dismissible="false"> - {{ alert.message }} - <gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link> - </gl-alert> -</template> diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue index 9a6fc07f6dd..e3deb94236e 100644 --- a/app/assets/javascripts/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/runner/components/runner_update_form.vue @@ -10,8 +10,8 @@ import { import { modelToUpdateMutationVariables, runnerToModel, -} from 'ee_else_ce/runner/runner_details/runner_update_form_utils'; -import createFlash, { FLASH_TYPES } from '~/flash'; +} from 'ee_else_ce/runner/runner_update_form_utils'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; import { __ } from '~/locale'; import { captureException } from '~/runner/sentry_utils'; import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; @@ -75,14 +75,14 @@ export default { if (errors?.length) { // Validation errors need not be thrown - createFlash({ message: errors[0] }); + createAlert({ message: errors[0] }); return; } this.onSuccess(); } catch (error) { const { message } = error; - createFlash({ message }); + createAlert({ message }); this.reportToSentry(error); } finally { @@ -90,7 +90,7 @@ export default { } }, onSuccess() { - createFlash({ message: __('Changes saved.'), type: FLASH_TYPES.SUCCESS }); + createAlert({ message: __('Changes saved.'), variant: VARIANT_SUCCESS }); this.model = runnerToModel(this.runner); }, reportToSentry(error) { 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 4b356fa47ed..79038eb8228 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 @@ -6,7 +6,7 @@ import { STATUS_PAUSED, STATUS_ONLINE, STATUS_OFFLINE, - STATUS_NOT_CONNECTED, + STATUS_NEVER_CONTACTED, STATUS_STALE, PARAM_KEY_STATUS, } from '../../constants'; @@ -16,7 +16,7 @@ const options = [ { 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') }, + { value: STATUS_NEVER_CONTACTED, title: s__('Runners|Never contacted') }, { value: STATUS_STALE, title: s__('Runners|Stale') }, ]; 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 7461308ab91..59230bb809e 100644 --- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue +++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue @@ -1,6 +1,6 @@ <script> import { GlFilteredSearchSuggestion, GlToken } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { s__ } from '~/locale'; @@ -50,7 +50,7 @@ export default { try { this.tags = await this.getTagsOptions(searchTerm); } catch { - createFlash({ + createAlert({ message: s__('Runners|Something went wrong while fetching the tags suggestions'), }); } finally { diff --git a/app/assets/javascripts/runner/components/stat/runner_online_stat.vue b/app/assets/javascripts/runner/components/stat/runner_online_stat.vue deleted file mode 100644 index b92b9badef0..00000000000 --- a/app/assets/javascripts/runner/components/stat/runner_online_stat.vue +++ /dev/null @@ -1,17 +0,0 @@ -<script> -import { GlSingleStat } from '@gitlab/ui/dist/charts'; - -export default { - components: { - GlSingleStat, - }, -}; -</script> -<template> - <gl-single-stat - v-bind="$attrs" - variant="success" - :title="s__('Runners|Online Runners')" - :meta-text="s__('Runners|online')" - /> -</template> diff --git a/app/assets/javascripts/runner/components/stat/runner_stats.vue b/app/assets/javascripts/runner/components/stat/runner_stats.vue new file mode 100644 index 00000000000..d3693ee593e --- /dev/null +++ b/app/assets/javascripts/runner/components/stat/runner_stats.vue @@ -0,0 +1,49 @@ +<script> +import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants'; +import RunnerStatusStat from './runner_status_stat.vue'; + +export default { + components: { + RunnerStatusStat, + }, + props: { + onlineRunnersCount: { + type: Number, + required: false, + default: null, + }, + offlineRunnersCount: { + type: Number, + required: false, + default: null, + }, + staleRunnersCount: { + type: Number, + required: false, + default: null, + }, + }, + STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, +}; +</script> +<template> + <div class="gl-display-flex gl-py-6"> + <runner-status-stat + class="gl-px-5" + :status="$options.STATUS_ONLINE" + :value="onlineRunnersCount" + /> + <runner-status-stat + class="gl-px-5" + :status="$options.STATUS_OFFLINE" + :value="offlineRunnersCount" + /> + <runner-status-stat + class="gl-px-5" + :status="$options.STATUS_STALE" + :value="staleRunnersCount" + /> + </div> +</template> diff --git a/app/assets/javascripts/runner/components/stat/runner_status_stat.vue b/app/assets/javascripts/runner/components/stat/runner_status_stat.vue new file mode 100644 index 00000000000..b77bbe15541 --- /dev/null +++ b/app/assets/javascripts/runner/components/stat/runner_status_stat.vue @@ -0,0 +1,65 @@ +<script> +import { GlSingleStat } from '@gitlab/ui/dist/charts'; +import { s__, formatNumber } from '~/locale'; +import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants'; + +export default { + components: { + GlSingleStat, + }, + props: { + value: { + type: Number, + required: false, + default: null, + }, + status: { + type: String, + required: true, + }, + }, + computed: { + formattedValue() { + if (typeof this.value === 'number') { + return formatNumber(this.value); + } + return '-'; + }, + stat() { + switch (this.status) { + case STATUS_ONLINE: + return { + variant: 'success', + title: s__('Runners|Online runners'), + metaText: s__('Runners|online'), + }; + case STATUS_OFFLINE: + return { + variant: 'muted', + title: s__('Runners|Offline runners'), + metaText: s__('Runners|offline'), + }; + case STATUS_STALE: + return { + variant: 'warning', + title: s__('Runners|Stale runners'), + metaText: s__('Runners|stale'), + }; + default: + return { + title: s__('Runners|Runners'), + }; + } + }, + }, +}; +</script> +<template> + <gl-single-stat + v-if="stat" + :value="formattedValue" + :variant="stat.variant" + :title="stat.title" + :meta-text="stat.metaText" + /> +</template> diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index 355f3054917..ce8019ffaa0 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -18,8 +18,8 @@ export const I18N_PROJECT_RUNNER_DESCRIPTION = s__('Runners|Associated with one export const I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION = s__( 'Runners|Runner is online; last contact was %{timeAgo}', ); -export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__( - 'Runners|This runner has never connected to this instance', +export const I18N_NEVER_CONTACTED_RUNNER_DESCRIPTION = s__( + 'Runners|This runner has never contacted this instance', ); export const I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION = s__( 'Runners|No recent contact from this runner; last contact was %{timeAgo}', @@ -60,7 +60,6 @@ export const STATUS_ACTIVE = 'ACTIVE'; export const STATUS_PAUSED = 'PAUSED'; export const STATUS_ONLINE = 'ONLINE'; -export const STATUS_NOT_CONNECTED = 'NOT_CONNECTED'; export const STATUS_NEVER_CONTACTED = 'NEVER_CONTACTED'; export const STATUS_OFFLINE = 'OFFLINE'; export const STATUS_STALE = 'STALE'; diff --git a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql index 6da9e276f74..f7bcd683718 100644 --- a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql +++ b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql @@ -13,7 +13,7 @@ query getGroupRunners( $sort: CiRunnerSort ) { group(fullPath: $groupFullPath) { - id + id # Apollo required runners( membership: DESCENDANTS before: $before diff --git a/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql new file mode 100644 index 00000000000..554eb09e372 --- /dev/null +++ b/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql @@ -0,0 +1,20 @@ +query getGroupRunnersCount( + $groupFullPath: ID! + $status: CiRunnerStatus + $type: CiRunnerType + $tagList: [String!] + $search: String +) { + group(fullPath: $groupFullPath) { + id # Apollo required + runners( + membership: DESCENDANTS + status: $status + type: $type + tagList: $tagList + search: $search + ) { + count + } + } +} diff --git a/app/assets/javascripts/runner/graphql/get_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_runners.query.graphql index 51a91b9eb96..05df399fa6a 100644 --- a/app/assets/javascripts/runner/graphql/get_runners.query.graphql +++ b/app/assets/javascripts/runner/graphql/get_runners.query.graphql @@ -26,6 +26,7 @@ query getRunners( nodes { ...RunnerNode adminUrl + editAdminUrl } pageInfo { ...PageInfo diff --git a/app/assets/javascripts/runner/graphql/get_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/get_runners_count.query.graphql new file mode 100644 index 00000000000..181a4495cae --- /dev/null +++ b/app/assets/javascripts/runner/graphql/get_runners_count.query.graphql @@ -0,0 +1,10 @@ +query getRunnersCount( + $status: CiRunnerStatus + $type: CiRunnerType + $tagList: [String!] + $search: String +) { + runners(status: $status, type: $type, tagList: $tagList, search: $search) { + count + } +} diff --git a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql index 8c50cba7de3..8e968343b9b 100644 --- a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql @@ -9,4 +9,6 @@ fragment RunnerDetailsShared on CiRunner { description maximumTimeout tagList + createdAt + status(legacyMode: null) } diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql index 169f6ffd2ea..4a771d779dc 100644 --- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql @@ -12,4 +12,8 @@ fragment RunnerNode on CiRunner { tagList contactedAt status(legacyMode: null) + userPermissions { + updateRunner + deleteRunner + } } 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 a58a53a6a0d..3a7b58e3dc9 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -1,6 +1,6 @@ <script> import { GlLink } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { fetchPolicies } from '~/lib/graphql'; import { updateHistory } from '~/lib/utils/url_utility'; import { formatNumber, sprintf, s__ } from '~/locale'; @@ -9,7 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerList from '../components/runner_list.vue'; import RunnerName from '../components/runner_name.vue'; -import RunnerOnlineStat from '../components/stat/runner_online_stat.vue'; +import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerPagination from '../components/runner_pagination.vue'; import RunnerTypeTabs from '../components/runner_type_tabs.vue'; @@ -19,8 +19,12 @@ import { GROUP_FILTERED_SEARCH_NAMESPACE, GROUP_TYPE, GROUP_RUNNER_COUNT_LIMIT, + STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, } from '../constants'; import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql'; +import getGroupRunnersCountQuery from '../graphql/get_group_runners_count.query.graphql'; import { fromUrlQueryToSearch, fromSearchToUrl, @@ -28,6 +32,17 @@ import { } from '../runner_search_utils'; import { captureException } from '../sentry_utils'; +const runnersCountSmartQuery = { + query: getGroupRunnersCountQuery, + fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, + update(data) { + return data?.group?.runners?.count; + }, + error(error) { + this.reportToSentry(error); + }, +}; + export default { name: 'GroupRunnersApp', components: { @@ -36,7 +51,7 @@ export default { RunnerFilteredSearchBar, RunnerList, RunnerName, - RunnerOnlineStat, + RunnerStats, RunnerPagination, RunnerTypeTabs, }, @@ -84,11 +99,38 @@ export default { }; }, error(error) { - createFlash({ message: I18N_FETCH_ERROR }); + createAlert({ message: I18N_FETCH_ERROR }); this.reportToSentry(error); }, }, + onlineRunnersTotal: { + ...runnersCountSmartQuery, + variables() { + return { + groupFullPath: this.groupFullPath, + status: STATUS_ONLINE, + }; + }, + }, + offlineRunnersTotal: { + ...runnersCountSmartQuery, + variables() { + return { + groupFullPath: this.groupFullPath, + status: STATUS_OFFLINE, + }; + }, + }, + staleRunnersTotal: { + ...runnersCountSmartQuery, + variables() { + return { + groupFullPath: this.groupFullPath, + status: STATUS_STALE, + }; + }, + }, }, computed: { variables() { @@ -147,7 +189,11 @@ export default { <template> <div> - <runner-online-stat class="gl-py-6 gl-px-5" :value="groupRunnersCount" /> + <runner-stats + :online-runners-count="onlineRunnersTotal" + :offline-runners-count="offlineRunnersTotal" + :stale-runners-count="staleRunnersTotal" + /> <div class="gl-display-flex gl-align-items-center"> <runner-type-tabs diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js index b88023720e8..c80a73948b8 100644 --- a/app/assets/javascripts/runner/runner_search_utils.js +++ b/app/assets/javascripts/runner/runner_search_utils.js @@ -16,6 +16,7 @@ import { PARAM_KEY_BEFORE, DEFAULT_SORT, RUNNER_PAGE_SIZE, + STATUS_NEVER_CONTACTED, } from './constants'; /** @@ -79,6 +80,33 @@ const getPaginationFromParams = (params) => { }; }; +// Outdated URL parameters +const STATUS_NOT_CONNECTED = 'NOT_CONNECTED'; + +/** + * Returns an updated URL for old (or deprecated) admin runner URLs. + * + * Use for redirecting users to currently used URLs. + * + * @param {String?} URL + * @returns Updated URL if outdated, `null` otherwise + */ +export const updateOutdatedUrl = (url = window.location.href) => { + const urlObj = new URL(url); + const query = urlObj.search; + + const params = queryToObject(query, { gatherArrays: true }); + + const runnerType = params[PARAM_KEY_STATUS]?.[0] || null; + if (runnerType === STATUS_NOT_CONNECTED) { + const updatedParams = { + [PARAM_KEY_STATUS]: [STATUS_NEVER_CONTACTED], + }; + return setUrlParams(updatedParams, url, false, true, true); + } + return null; +}; + /** * Takes a URL query and transforms it into a "search" object * @param {String?} query diff --git a/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js b/app/assets/javascripts/runner/runner_update_form_utils.js index 3b519fa7d71..3b519fa7d71 100644 --- a/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js +++ b/app/assets/javascripts/runner/runner_update_form_utils.js |