diff options
Diffstat (limited to 'app/assets/javascripts/runner')
24 files changed, 431 insertions, 118 deletions
diff --git a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue index c3f317b40b0..06a8eb790fc 100644 --- a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue +++ b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue @@ -1,14 +1,16 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlBadge, GlTab, GlTooltipDirective } from '@gitlab/ui'; import { createAlert, VARIANT_SUCCESS } from '~/flash'; import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { redirectTo } from '~/lib/utils/url_utility'; +import { formatJobCount } from '../utils'; import RunnerDeleteButton from '../components/runner_delete_button.vue'; import RunnerEditButton from '../components/runner_edit_button.vue'; import RunnerPauseButton from '../components/runner_pause_button.vue'; import RunnerHeader from '../components/runner_header.vue'; import RunnerDetails from '../components/runner_details.vue'; +import RunnerJobs from '../components/runner_jobs.vue'; import { I18N_FETCH_ERROR } from '../constants'; import runnerQuery from '../graphql/show/runner.query.graphql'; import { captureException } from '../sentry_utils'; @@ -17,11 +19,14 @@ import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_lo export default { name: 'AdminRunnerShowApp', components: { + GlBadge, + GlTab, RunnerDeleteButton, RunnerEditButton, RunnerPauseButton, RunnerHeader, RunnerDetails, + RunnerJobs, }, directives: { GlTooltip: GlTooltipDirective, @@ -63,6 +68,9 @@ export default { canDelete() { return this.runner.userPermissions?.deleteRunner; }, + jobCount() { + return formatJobCount(this.runner?.jobCount); + }, }, errorCaptured(error) { this.reportToSentry(error); @@ -88,6 +96,24 @@ export default { </template> </runner-header> - <runner-details :runner="runner" /> + <runner-details :runner="runner"> + <template #jobs-tab> + <gl-tab> + <template #title> + {{ s__('Runners|Jobs') }} + <gl-badge + v-if="jobCount" + data-testid="job-count-badge" + class="gl-tab-counter-badge" + size="sm" + > + {{ jobCount }} + </gl-badge> + </template> + + <runner-jobs v-if="runner" :runner="runner" /> + </gl-tab> + </template> + </runner-details> </div> </template> 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 c2bb635e056..a90ef2d3530 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -10,6 +10,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo 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 RunnerListEmptyState from '../components/runner_list_empty_state.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerPagination from '../components/runner_pagination.vue'; @@ -35,6 +36,7 @@ import { fromUrlQueryToSearch, fromSearchToUrl, fromSearchToVariables, + isSearchFiltered, } from '../runner_search_utils'; import { captureException } from '../sentry_utils'; @@ -91,6 +93,7 @@ export default { RunnerFilteredSearchBar, RunnerBulkDelete, RunnerList, + RunnerListEmptyState, RunnerName, RunnerStats, RunnerPagination, @@ -98,7 +101,7 @@ export default { RunnerActionsCell, }, mixins: [glFeatureFlagMixin()], - inject: ['localMutations'], + inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'], props: { registrationToken: { type: String, @@ -190,6 +193,9 @@ export default { // Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981 return this.glFeatures.adminRunnersBulkDelete; }, + isSearchFiltered() { + return isSearchFiltered(this.search); + }, }, watch: { search: { @@ -298,9 +304,13 @@ export default { :stale-runners-count="staleRunnersTotal" /> - <div v-if="noRunnersFound" class="gl-text-center gl-p-5"> - {{ __('No runners found') }} - </div> + <runner-list-empty-state + v-if="noRunnersFound" + :registration-token="registrationToken" + :is-search-filtered="isSearchFiltered" + :svg-path="emptyStateSvgPath" + :filtered-svg-path="emptyStateFilteredSvgPath" + /> <template v-else> <runner-bulk-delete v-if="isBulkDeleteEnabled" /> <runner-list diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js index b1d8442bb32..7bb6cd5689e 100644 --- a/app/assets/javascripts/runner/admin_runners/index.js +++ b/app/assets/javascripts/runner/admin_runners/index.js @@ -34,6 +34,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { registrationToken, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyStateSvgPath, + emptyStateFilteredSvgPath, } = el.dataset; const { cacheConfig, typeDefs, localMutations } = createLocalState(); @@ -50,6 +52,8 @@ export const initAdminRunners = (selector = '#js-admin-runners') => { localMutations, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyStateSvgPath, + emptyStateFilteredSvgPath, }, render(h) { return h(AdminRunnersApp, { 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 93f86ae2a2c..a48db9f8ac8 100644 --- a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue +++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue @@ -7,6 +7,8 @@ import RunnerPausedBadge from '../runner_paused_badge.vue'; export default { components: { RunnerStatusBadge, + RunnerUpgradeStatusBadge: () => + import('ee_component/runner/components/runner_upgrade_status_badge.vue'), RunnerPausedBadge, }, directives: { @@ -33,6 +35,11 @@ export default { size="sm" class="gl-display-inline-block gl-max-w-full gl-text-truncate" /> + <runner-upgrade-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" diff --git a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue index bb2a8ddf151..212ad5fa5a0 100644 --- a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue +++ b/app/assets/javascripts/runner/components/registration/registration_dropdown.vue @@ -1,11 +1,5 @@ <script> -import { - GlFormGroup, - GlDropdown, - GlDropdownForm, - GlDropdownItem, - GlDropdownDivider, -} from '@gitlab/ui'; +import { GlDropdown, GlDropdownForm, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; import { s__ } from '~/locale'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants'; @@ -17,10 +11,8 @@ export default { showInstallationInstructions: s__( 'Runners|Show runner installation and registration instructions', ), - registrationToken: s__('Runners|Registration token'), }, components: { - GlFormGroup, GlDropdown, GlDropdownForm, GlDropdownItem, @@ -45,7 +37,6 @@ export default { data() { return { currentRegistrationToken: this.registrationToken, - instructionsModalOpened: false, }; }, computed: { @@ -64,15 +55,7 @@ export default { }, methods: { onShowInstructionsClick() { - // Rendering the modal on demand, to avoid - // loading instructions prematurely from API. - this.instructionsModalOpened = true; - - this.$nextTick(() => { - // $refs.runnerInstructionsModal is defined in - // the tick after the modal is rendered - this.$refs.runnerInstructionsModal.show(); - }); + this.$refs.runnerInstructionsModal.show(); }, onTokenReset(token) { this.currentRegistrationToken = token; @@ -94,7 +77,6 @@ export default { <gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick"> {{ $options.i18n.showInstallationInstructions }} <runner-instructions-modal - v-if="instructionsModalOpened" ref="runnerInstructionsModal" :registration-token="currentRegistrationToken" data-testid="runner-instructions-modal" @@ -102,9 +84,7 @@ export default { </gl-dropdown-item> <gl-dropdown-divider /> <gl-dropdown-form class="gl-p-4!"> - <gl-form-group class="gl-mb-0" :label="$options.i18n.registrationToken"> - <registration-token :value="currentRegistrationToken" /> - </gl-form-group> + <registration-token input-id="token-value" :value="currentRegistrationToken" /> </gl-dropdown-form> <gl-dropdown-divider /> <registration-token-reset-dropdown-item :type="type" @tokenReset="onTokenReset" /> diff --git a/app/assets/javascripts/runner/components/registration/registration_token.vue b/app/assets/javascripts/runner/components/registration/registration_token.vue index 68c6429a056..6b4e6a929b7 100644 --- a/app/assets/javascripts/runner/components/registration/registration_token.vue +++ b/app/assets/javascripts/runner/components/registration/registration_token.vue @@ -6,13 +6,27 @@ export default { components: { InputCopyToggleVisibility, }, + i18n: { + registrationToken: s__('Runners|Registration token'), + }, props: { + inputId: { + type: String, + required: true, + }, value: { type: String, required: false, default: '', }, }, + computed: { + formInputGroupProps() { + return { + id: this.inputId, + }; + }, + }, methods: { onCopy() { // value already in the clipboard, simply notify the user @@ -26,8 +40,10 @@ export default { <input-copy-toggle-visibility class="gl-m-0" :value="value" - data-testid="token-value" + :label="$options.i18n.registrationToken" + :label-for="inputId" :copy-button-title="$options.I18N_COPY_BUTTON_TITLE" + :form-input-group-props="formInputGroupProps" @copy="onCopy" /> </template> diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue index 3734f436034..75ddec6c716 100644 --- a/app/assets/javascripts/runner/components/runner_details.vue +++ b/app/assets/javascripts/runner/components/runner_details.vue @@ -1,26 +1,24 @@ <script> -import { GlBadge, GlTabs, GlTab, GlIntersperse } from '@gitlab/ui'; +import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui'; import { s__ } from '~/locale'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants'; -import { formatJobCount } from '../utils'; import RunnerDetail from './runner_detail.vue'; import RunnerGroups from './runner_groups.vue'; import RunnerProjects from './runner_projects.vue'; -import RunnerJobs from './runner_jobs.vue'; import RunnerTags from './runner_tags.vue'; export default { components: { - GlBadge, GlTabs, GlTab, GlIntersperse, RunnerDetail, + RunnerMaintenanceNoteDetail: () => + import('ee_component/runner/components/runner_maintenance_note_detail.vue'), RunnerGroups, RunnerProjects, - RunnerJobs, RunnerTags, TimeAgo, }, @@ -57,9 +55,6 @@ export default { isProjectRunner() { return this.runner?.runnerType === PROJECT_TYPE; }, - jobCount() { - return formatJobCount(this.runner?.jobCount); - }, }, ACCESS_LEVEL_REF_PROTECTED, }; @@ -106,6 +101,11 @@ export default { /> </template> </runner-detail> + + <runner-maintenance-note-detail + class="gl-pt-4 gl-border-t-gray-100 gl-border-t-1 gl-border-t-solid" + :value="runner.maintenanceNoteHtml" + /> </dl> </div> @@ -113,15 +113,6 @@ export default { <runner-projects v-if="isProjectRunner" :runner="runner" /> </template> </gl-tab> - <gl-tab> - <template #title> - {{ s__('Runners|Jobs') }} - <gl-badge v-if="jobCount" data-testid="job-count-badge" class="gl-ml-1" size="sm"> - {{ jobCount }} - </gl-badge> - </template> - - <runner-jobs v-if="runner" :runner="runner" /> - </gl-tab> + <slot name="jobs-tab"></slot> </gl-tabs> </template> diff --git a/app/assets/javascripts/runner/components/runner_jobs.vue b/app/assets/javascripts/runner/components/runner_jobs.vue index 4eb1312b204..57afdc4b9be 100644 --- a/app/assets/javascripts/runner/components/runner_jobs.vue +++ b/app/assets/javascripts/runner/components/runner_jobs.vue @@ -1,5 +1,5 @@ <script> -import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; +import { GlSkeletonLoader } from '@gitlab/ui'; import { createAlert } from '~/flash'; import runnerJobsQuery from '../graphql/show/runner_jobs.query.graphql'; import { I18N_FETCH_ERROR, I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '../constants'; @@ -11,7 +11,7 @@ import RunnerPagination from './runner_pagination.vue'; export default { name: 'RunnerJobs', components: { - GlSkeletonLoading, + GlSkeletonLoader, RunnerJobsTable, RunnerPagination, }, @@ -68,7 +68,9 @@ export default { <template> <div class="gl-pt-3"> - <gl-skeleton-loading v-if="loading" class="gl-py-5" /> + <div v-if="loading" class="gl-py-5"> + <gl-skeleton-loader /> + </div> <runner-jobs-table v-else-if="jobs.items.length" :jobs="jobs.items" /> <p v-else>{{ $options.I18N_NO_JOBS_FOUND }}</p> diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index dcfd4b84dd2..f1f99c728c5 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -12,7 +12,7 @@ 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: 'status', label: s__('Runners|Status'), thClasses: ['gl-w-15p'] }), tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }), tableField({ key: 'version', label: __('Version') }), tableField({ key: 'jobCount', label: __('Jobs') }), diff --git a/app/assets/javascripts/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/runner/components/runner_list_empty_state.vue new file mode 100644 index 00000000000..ab9cde6a401 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_list_empty_state.vue @@ -0,0 +1,75 @@ +<script> +import { GlEmptyState, GlLink, GlSprintf, GlModalDirective } from '@gitlab/ui'; +import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; + +export default { + components: { + GlEmptyState, + GlLink, + GlSprintf, + RunnerInstructionsModal, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + isSearchFiltered: { + type: Boolean, + required: false, + default: false, + }, + svgPath: { + type: String, + required: false, + default: '', + }, + filteredSvgPath: { + type: String, + required: false, + default: '', + }, + registrationToken: { + type: String, + required: false, + default: null, + }, + }, + modalId: 'runners-empty-state-instructions-modal', + svgHeight: 145, +}; +</script> + +<template> + <gl-empty-state + v-if="isSearchFiltered" + :title="s__('Runners|No results found')" + :svg-path="filteredSvgPath" + :svg-height="$options.svgHeight" + :description="s__('Runners|Edit your search and try again')" + /> + <gl-empty-state + v-else + :title="s__('Runners|Get started with runners')" + :svg-path="svgPath" + :svg-height="$options.svgHeight" + > + <template #description> + <gl-sprintf + :message=" + s__( + 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.', + ) + " + > + <template #link="{ content }"> + <gl-link v-gl-modal="$options.modalId">{{ content }}</gl-link> + </template> + </gl-sprintf> + + <runner-instructions-modal + :modal-id="$options.modalId" + :registration-token="registrationToken" + /> + </template> + </gl-empty-state> +</template> diff --git a/app/assets/javascripts/runner/components/runner_projects.vue b/app/assets/javascripts/runner/components/runner_projects.vue index daca718e2b5..c0c0c14e91e 100644 --- a/app/assets/javascripts/runner/components/runner_projects.vue +++ b/app/assets/javascripts/runner/components/runner_projects.vue @@ -1,5 +1,5 @@ <script> -import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; +import { GlSkeletonLoader } from '@gitlab/ui'; import { sprintf, formatNumber } from '~/locale'; import { createAlert } from '~/flash'; import runnerProjectsQuery from '../graphql/show/runner_projects.query.graphql'; @@ -17,7 +17,7 @@ import RunnerPagination from './runner_pagination.vue'; export default { name: 'RunnerProjects', components: { - GlSkeletonLoading, + GlSkeletonLoader, RunnerAssignedItem, RunnerPagination, }, @@ -86,7 +86,9 @@ export default { {{ heading }} </h3> - <gl-skeleton-loading v-if="loading" class="gl-py-5" /> + <div v-if="loading" class="gl-py-5"> + <gl-skeleton-loader /> + </div> <template v-else-if="projects.items.length"> <runner-assigned-item v-for="(project, i) in projects.items" diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue index 56c9007a781..c613e2d2467 100644 --- a/app/assets/javascripts/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/runner/components/runner_update_form.vue @@ -31,6 +31,8 @@ export default { GlFormGroup, GlFormInputGroup, GlSkeletonLoader, + RunnerMaintenanceNoteField: () => + import('ee_component/runner/components/runner_maintenance_note_field.vue'), RunnerUpdateCostFactorFields: () => import('ee_component/runner/components/runner_update_cost_factor_fields.vue'), }, @@ -115,9 +117,13 @@ export default { <h4 class="gl-font-lg gl-my-5">{{ s__('Runners|Details') }}</h4> <gl-skeleton-loader v-if="loading" /> - <gl-form-group v-else :label="__('Description')" data-testid="runner-field-description"> - <gl-form-input-group v-model="model.description" /> - </gl-form-group> + + <template v-else> + <gl-form-group :label="__('Description')" data-testid="runner-field-description"> + <gl-form-input-group v-model="model.description" /> + </gl-form-group> + <runner-maintenance-note-field v-model="model.maintenanceNote" /> + </template> <hr /> diff --git a/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql index 5d0450e7418..61bfe03bf6e 100644 --- a/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql +++ b/app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql @@ -1,4 +1,4 @@ -#import "~/runner/graphql/list/list_item.fragment.graphql" +#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql" #import "~/graphql_shared/fragments/page_info.fragment.graphql" query getRunners( 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 b4f2b5cd8c8..8755636a7ad 100644 --- a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql +++ b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql @@ -1,4 +1,4 @@ -#import "~/runner/graphql/list/list_item.fragment.graphql" +#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql" #import "~/graphql_shared/fragments/page_info.fragment.graphql" query getGroupRunners( diff --git a/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql b/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql index 620c18c5bc0..19a5a48ea75 100644 --- a/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql @@ -1,20 +1,5 @@ +#import "./list_item_shared.fragment.graphql" + fragment ListItem on CiRunner { - __typename - id - description - runnerType - shortSha - version - revision - ipAddress - active - locked - jobCount - tagList - contactedAt - status(legacyMode: null) - userPermissions { - updateRunner - deleteRunner - } + ...ListItemShared } 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 new file mode 100644 index 00000000000..cf925359ffb --- /dev/null +++ b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql @@ -0,0 +1,20 @@ +fragment ListItemShared on CiRunner { + __typename + id + description + runnerType + shortSha + version + revision + ipAddress + active + locked + jobCount + tagList + contactedAt + status(legacyMode: null) + userPermissions { + updateRunner + deleteRunner + } +} diff --git a/app/assets/javascripts/runner/graphql/show/runner.query.graphql b/app/assets/javascripts/runner/graphql/show/runner.query.graphql index 178816b58bd..dec434b43a5 100644 --- a/app/assets/javascripts/runner/graphql/show/runner.query.graphql +++ b/app/assets/javascripts/runner/graphql/show/runner.query.graphql @@ -1,41 +1,7 @@ +#import "ee_else_ce/runner/graphql/show/runner_details.fragment.graphql" + query getRunner($id: CiRunnerID!) { runner(id: $id) { - __typename - id - shortSha - runnerType - active - accessLevel - runUntagged - locked - ipAddress - executorName - architectureName - platformName - description - maximumTimeout - jobCount - tagList - createdAt - status(legacyMode: null) - contactedAt - version - editAdminUrl - userPermissions { - updateRunner - deleteRunner - } - groups { - # Only a single group can be loaded here, while projects - # are loaded separately using the query with pagination - # parameters `runner_projects.query.graphql`. - nodes { - id - avatarUrl - name - fullName - webUrl - } - } + ...RunnerDetails } } diff --git a/app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql b/app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql new file mode 100644 index 00000000000..2449ee0fc0f --- /dev/null +++ b/app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql @@ -0,0 +1,5 @@ +#import "./runner_details_shared.fragment.graphql" + +fragment RunnerDetails on CiRunner { + ...RunnerDetailsShared +} diff --git a/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql new file mode 100644 index 00000000000..b79ad4d9280 --- /dev/null +++ b/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql @@ -0,0 +1,39 @@ +fragment RunnerDetailsShared on CiRunner { + __typename + id + shortSha + runnerType + active + accessLevel + runUntagged + locked + ipAddress + executorName + architectureName + platformName + description + maximumTimeout + jobCount + tagList + createdAt + status(legacyMode: null) + contactedAt + version + editAdminUrl + userPermissions { + updateRunner + deleteRunner + } + groups { + # Only a single group can be loaded here, while projects + # are loaded separately using the query with pagination + # parameters `runner_projects.query.graphql`. + nodes { + id + avatarUrl + name + fullName + webUrl + } + } +} diff --git a/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue b/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue new file mode 100644 index 00000000000..c336e091fdf --- /dev/null +++ b/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue @@ -0,0 +1,114 @@ +<script> +import { GlBadge, GlTab, GlTooltipDirective } from '@gitlab/ui'; +import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { redirectTo } from '~/lib/utils/url_utility'; +import { formatJobCount } from '../utils'; +import RunnerDeleteButton from '../components/runner_delete_button.vue'; +import RunnerEditButton from '../components/runner_edit_button.vue'; +import RunnerPauseButton from '../components/runner_pause_button.vue'; +import RunnerHeader from '../components/runner_header.vue'; +import RunnerDetails from '../components/runner_details.vue'; +import RunnerJobs from '../components/runner_jobs.vue'; +import { I18N_FETCH_ERROR } from '../constants'; +import runnerQuery from '../graphql/show/runner.query.graphql'; +import { captureException } from '../sentry_utils'; +import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage'; + +export default { + name: 'GroupRunnerShowApp', + components: { + GlBadge, + GlTab, + RunnerDeleteButton, + RunnerEditButton, + RunnerPauseButton, + RunnerHeader, + RunnerDetails, + RunnerJobs, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + runnerId: { + type: String, + required: true, + }, + runnersPath: { + type: String, + required: true, + }, + }, + data() { + return { + runner: null, + }; + }, + apollo: { + runner: { + query: runnerQuery, + variables() { + return { + id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId), + }; + }, + error(error) { + createAlert({ message: I18N_FETCH_ERROR }); + + this.reportToSentry(error); + }, + }, + }, + computed: { + canUpdate() { + return this.runner.userPermissions?.updateRunner; + }, + canDelete() { + return this.runner.userPermissions?.deleteRunner; + }, + jobCount() { + return formatJobCount(this.runner?.jobCount); + }, + }, + errorCaptured(error) { + this.reportToSentry(error); + }, + methods: { + reportToSentry(error) { + captureException({ error, component: this.$options.name }); + }, + onDeleted({ message }) { + saveAlertToLocalStorage({ message, variant: VARIANT_SUCCESS }); + redirectTo(this.runnersPath); + }, + }, +}; +</script> +<template> + <div> + <runner-header v-if="runner" :runner="runner"> + <template #actions> + <runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" /> + <runner-pause-button v-if="canUpdate" :runner="runner" /> + <runner-delete-button v-if="canDelete" :runner="runner" @deleted="onDeleted" /> + </template> + </runner-header> + + <runner-details :runner="runner"> + <template #jobs-tab> + <gl-tab> + <template #title> + {{ s__('Runners|Jobs') }} + <gl-badge v-if="jobCount" data-testid="job-count-badge" class="gl-ml-1" size="sm"> + {{ jobCount }} + </gl-badge> + </template> + + <runner-jobs v-if="runner" :runner="runner" /> + </gl-tab> + </template> + </runner-details> + </div> +</template> diff --git a/app/assets/javascripts/runner/group_runner_show/index.js b/app/assets/javascripts/runner/group_runner_show/index.js new file mode 100644 index 00000000000..d1b87c8e427 --- /dev/null +++ b/app/assets/javascripts/runner/group_runner_show/index.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage'; +import GroupRunnerShowApp from './group_runner_show_app.vue'; + +Vue.use(VueApollo); + +export const initAdminRunnerShow = (selector = '#js-group-runner-show') => { + showAlertFromLocalStorage(); + + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { runnerId, runnersPath } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(GroupRunnerShowApp, { + props: { + runnerId, + runnersPath, + }, + }); + }, + }); +}; 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 b5bd4b111fd..641b3a8f560 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -8,6 +8,7 @@ import { fetchPolicies } from '~/lib/graphql'; 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 RunnerListEmptyState from '../components/runner_list_empty_state.vue'; import RunnerName from '../components/runner_name.vue'; import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerPagination from '../components/runner_pagination.vue'; @@ -31,6 +32,7 @@ import { fromUrlQueryToSearch, fromSearchToUrl, fromSearchToVariables, + isSearchFiltered, } from '../runner_search_utils'; import { captureException } from '../sentry_utils'; @@ -86,12 +88,14 @@ export default { RegistrationDropdown, RunnerFilteredSearchBar, RunnerList, + RunnerListEmptyState, RunnerName, RunnerStats, RunnerPagination, RunnerTypeTabs, RunnerActionsCell, }, + inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'], props: { registrationToken: { type: String, @@ -196,6 +200,9 @@ export default { filteredSearchNamespace() { return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`; }, + isSearchFiltered() { + return isSearchFiltered(this.search); + }, }, watch: { search: { @@ -299,9 +306,13 @@ export default { :stale-runners-count="staleRunnersTotal" /> - <div v-if="noRunnersFound" class="gl-text-center gl-p-5"> - {{ __('No runners found') }} - </div> + <runner-list-empty-state + v-if="noRunnersFound" + :registration-token="registrationToken" + :is-search-filtered="isSearchFiltered" + :svg-path="emptyStateSvgPath" + :filtered-svg-path="emptyStateFilteredSvgPath" + /> <template v-else> <runner-list :runners="runners.items" :loading="runnersLoading"> <template #runner-name="{ runner }"> diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js index 0dade30f820..feed6b0ceb7 100644 --- a/app/assets/javascripts/runner/group_runners/index.js +++ b/app/assets/javascripts/runner/group_runners/index.js @@ -22,6 +22,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => { groupRunnersLimitedCount, onlineContactTimeoutSecs, staleTimeoutSecs, + emptyStateSvgPath, + emptyStateFilteredSvgPath, } = el.dataset; const apolloProvider = new VueApollo({ @@ -36,6 +38,8 @@ export const initGroupRunners = (selector = '#js-group-runners') => { groupId, onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10), staleTimeoutSecs: parseInt(staleTimeoutSecs, 10), + emptyStateSvgPath, + emptyStateFilteredSvgPath, }, render(h) { return h(GroupRunnersApp, { diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js index 0d688ed65ef..e01878f355a 100644 --- a/app/assets/javascripts/runner/runner_search_utils.js +++ b/app/assets/javascripts/runner/runner_search_utils.js @@ -236,3 +236,17 @@ export const fromSearchToVariables = ({ ...paginationVariables, }; }; + +/** + * Decides whether or not a search object is the "default" or empty. + * + * A search is filtered if the user has entered filtering criteria. + * + * @param {Object} search + * @returns true if this search is filtered, false otherwise + */ +export const isSearchFiltered = ({ runnerType = null, filters = [], pagination = {} } = {}) => { + return Boolean( + runnerType !== null || filters?.length !== 0 || (pagination && pagination?.page !== 1), + ); +}; |