summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/runner
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/runner')
-rw-r--r--app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue30
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue18
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js4
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_status_cell.vue7
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_dropdown.vue26
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_token.vue18
-rw-r--r--app/assets/javascripts/runner/components/runner_details.vue27
-rw-r--r--app/assets/javascripts/runner/components/runner_jobs.vue8
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue2
-rw-r--r--app/assets/javascripts/runner/components/runner_list_empty_state.vue75
-rw-r--r--app/assets/javascripts/runner/components/runner_projects.vue8
-rw-r--r--app/assets/javascripts/runner/components/runner_update_form.vue12
-rw-r--r--app/assets/javascripts/runner/graphql/list/admin_runners.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/list/group_runners.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql21
-rw-r--r--app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql20
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner.query.graphql40
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql5
-rw-r--r--app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql39
-rw-r--r--app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue114
-rw-r--r--app/assets/javascripts/runner/group_runner_show/index.js36
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue17
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js4
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js14
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),
+ );
+};