Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ci/admin/jobs_table')
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue271
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue37
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue66
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue28
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue39
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue26
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/constants.js35
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js62
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql90
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql5
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql5
11 files changed, 664 insertions, 0 deletions
diff --git a/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
new file mode 100644
index 00000000000..89582e64f3a
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
@@ -0,0 +1,271 @@
+<script>
+import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
+import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
+import { validateQueryString } from '~/ci/common/private/jobs_filtered_search/utils';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import JobsTableTabs from '~/ci/jobs_page/components/jobs_table_tabs.vue';
+import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
+import JobsTableEmptyState from '~/ci/jobs_page/components/jobs_table_empty_state.vue';
+import { createAlert } from '~/alert';
+import {
+ TOKEN_TYPE_STATUS,
+ TOKEN_TYPE_JOBS_RUNNER_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import {
+ DEFAULT_FIELDS_ADMIN,
+ RAW_TEXT_WARNING_ADMIN,
+ JOBS_COUNT_ERROR_MESSAGE,
+ JOBS_FETCH_ERROR_MSG,
+ LOADING_ARIA_LABEL,
+ CANCELABLE_JOBS_ERROR_MSG,
+} from './constants';
+import JobsSkeletonLoader from './components/jobs_skeleton_loader.vue';
+import GetAllJobs from './graphql/queries/get_all_jobs.query.graphql';
+import GetAllJobsCount from './graphql/queries/get_all_jobs_count.query.graphql';
+import getCancelableJobs from './graphql/queries/get_cancelable_jobs_count.query.graphql';
+
+export default {
+ i18n: {
+ jobsCountErrorMsg: JOBS_COUNT_ERROR_MESSAGE,
+ jobsFetchErrorMsg: JOBS_FETCH_ERROR_MSG,
+ loadingAriaLabel: LOADING_ARIA_LABEL,
+ cancelableJobsErrorMsg: CANCELABLE_JOBS_ERROR_MSG,
+ },
+ filterSearchBoxStyles:
+ 'gl-my-0 gl-p-5 gl-bg-gray-10 gl-text-gray-900 gl-border-b gl-border-gray-100',
+ components: {
+ JobsSkeletonLoader,
+ JobsTableEmptyState,
+ GlAlert,
+ JobsFilteredSearch,
+ JobsTable,
+ JobsTableTabs,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ inject: {
+ jobStatuses: {
+ default: null,
+ required: false,
+ },
+ url: {
+ default: '',
+ required: false,
+ },
+ emptyStateSvgPath: {
+ default: '',
+ required: false,
+ },
+ },
+ apollo: {
+ jobs: {
+ query: GetAllJobs,
+ variables() {
+ return this.variables;
+ },
+ update(data) {
+ const { jobs: { nodes: list = [], pageInfo = {} } = {} } = data || {};
+ return {
+ list,
+ pageInfo,
+ };
+ },
+ error() {
+ this.error = this.$options.i18n.jobsFetchErrorMsg;
+ },
+ },
+ jobsCount: {
+ query: GetAllJobsCount,
+ variables() {
+ return this.variables;
+ },
+ update(data) {
+ return data?.jobs?.count || 0;
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ error() {
+ this.error = this.$options.i18n.jobsCountErrorMsg;
+ },
+ },
+ cancelable: {
+ query: getCancelableJobs,
+ update(data) {
+ this.isCancelable = data.cancelable.count !== 0;
+ },
+ error() {
+ this.error = this.$options.i18n.cancelableJobsErrorMsg;
+ },
+ },
+ },
+ data() {
+ return {
+ jobs: {
+ list: [],
+ },
+ error: '',
+ count: 0,
+ scope: null,
+ infiniteScrollingTriggered: false,
+ filterSearchTriggered: false,
+ DEFAULT_FIELDS_ADMIN,
+ isCancelable: false,
+ jobsCount: null,
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.jobs.loading;
+ },
+ // Show when on All tab with no jobs
+ // Show only when not loading and filtered search has not been triggered
+ // So we don't show empty state when results are empty on a filtered search
+ showEmptyState() {
+ return (
+ this.jobs.list.length === 0 && !this.scope && !this.loading && !this.filterSearchTriggered
+ );
+ },
+ hasNextPage() {
+ return this.jobs?.pageInfo?.hasNextPage;
+ },
+ variables() {
+ return { ...this.validatedQueryString };
+ },
+ validatedQueryString() {
+ const queryStringObject = queryToObject(window.location.search);
+
+ return validateQueryString(queryStringObject);
+ },
+ showFilteredSearch() {
+ return !this.scope;
+ },
+ showLoadingSpinner() {
+ return this.loading && this.infiniteScrollingTriggered;
+ },
+ showSkeletonLoader() {
+ return this.loading && !this.showLoadingSpinner;
+ },
+ },
+ watch: {
+ // this watcher ensures that the count on the all tab
+ // is not updated when switching to the finished tab
+ jobsCount(newCount) {
+ if (this.scope) return;
+
+ this.count = newCount;
+ },
+ },
+ methods: {
+ updateHistoryAndFetchCount(filterParams = {}) {
+ this.$apollo.queries.jobsCount.refetch(filterParams);
+
+ updateHistory({
+ url: setUrlParams(filterParams, window.location.href, true),
+ });
+ },
+ fetchJobsByStatus(scope) {
+ this.infiniteScrollingTriggered = false;
+
+ if (this.scope === scope) return;
+
+ this.scope = scope;
+
+ if (!this.scope) this.updateHistoryAndFetchCount();
+
+ this.$apollo.queries.jobs.refetch({ statuses: scope });
+ },
+ fetchMoreJobs() {
+ if (!this.loading) {
+ this.infiniteScrollingTriggered = true;
+
+ const parameters = this.variables;
+ parameters.after = this.jobs?.pageInfo?.endCursor;
+
+ this.$apollo.queries.jobs.fetchMore({
+ variables: parameters,
+ });
+ }
+ },
+ filterJobsBySearch(filters) {
+ this.infiniteScrollingTriggered = false;
+ this.filterSearchTriggered = true;
+
+ if (filters.some((filter) => !filter.type)) {
+ // Raw text input in filtered search does not have a type
+ // when a user enters raw text we alert them that it is
+ // not supported and we do not make an additional API call
+ createAlert({ message: RAW_TEXT_WARNING_ADMIN, type: 'warning' });
+ return;
+ }
+
+ const defaultFilterParams = this.glFeatures.adminJobsFilterRunnerType
+ ? { statuses: null, runnerTypes: null }
+ : { statuses: null };
+
+ const filterParams = filters.reduce((acc, filter) => {
+ switch (filter.type) {
+ case TOKEN_TYPE_STATUS:
+ return { ...acc, statuses: filter.value.data };
+
+ case TOKEN_TYPE_JOBS_RUNNER_TYPE:
+ if (this.glFeatures.adminJobsFilterRunnerType) {
+ return { ...acc, runnerTypes: filter.value.data };
+ }
+ return acc;
+
+ default:
+ return acc;
+ }
+ }, defaultFilterParams);
+
+ this.updateHistoryAndFetchCount(filterParams);
+ this.$apollo.queries.jobs.refetch(filterParams);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="error" class="gl-mt-2" variant="danger" dismissible @dismiss="error = ''">
+ {{ error }}
+ </gl-alert>
+
+ <jobs-table-tabs
+ :all-jobs-count="count"
+ :loading="loading"
+ :show-cancel-all-jobs-button="isCancelable"
+ @fetchJobsByStatus="fetchJobsByStatus"
+ />
+
+ <div v-if="showFilteredSearch" :class="$options.filterSearchBoxStyles">
+ <jobs-filtered-search
+ :query-string="validatedQueryString"
+ @filterJobsBySearch="filterJobsBySearch"
+ />
+ </div>
+
+ <jobs-skeleton-loader v-if="showSkeletonLoader" class="gl-mt-5" />
+
+ <jobs-table-empty-state v-else-if="showEmptyState" />
+
+ <jobs-table
+ v-else
+ :jobs="jobs.list"
+ :table-fields="DEFAULT_FIELDS_ADMIN"
+ admin
+ class="gl-table-no-top-border"
+ />
+
+ <gl-intersection-observer v-if="hasNextPage" @appear="fetchMoreJobs">
+ <gl-loading-icon
+ v-if="showLoadingSpinner"
+ size="lg"
+ :aria-label="$options.i18n.loadingAriaLabel"
+ />
+ </gl-intersection-observer>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue
new file mode 100644
index 00000000000..fb13fd4b03e
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { CANCEL_JOBS_MODAL_ID, CANCEL_JOBS_BUTTON_TEXT, CANCEL_BUTTON_TOOLTIP } from '../constants';
+import CancelJobsModal from './cancel_jobs_modal.vue';
+
+export default {
+ name: 'CancelJobs',
+ components: {
+ GlButton,
+ CancelJobsModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ },
+ modalId: CANCEL_JOBS_MODAL_ID,
+ buttonText: CANCEL_JOBS_BUTTON_TEXT,
+ buttonTooltip: CANCEL_BUTTON_TOOLTIP,
+};
+</script>
+<template>
+ <div>
+ <gl-button
+ v-gl-modal="$options.modalId"
+ v-gl-tooltip="$options.buttonTooltip"
+ variant="danger"
+ >{{ $options.buttonText }}</gl-button
+ >
+ <cancel-jobs-modal :modal-id="$options.modalId" :url="url" @confirm="$emit('confirm')" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue
new file mode 100644
index 00000000000..7c1cd75609a
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import {
+ CANCEL_TEXT,
+ CANCEL_JOBS_FAILED_TEXT,
+ CANCEL_JOBS_MODAL_TITLE,
+ CANCEL_JOBS_WARNING,
+ PRIMARY_ACTION_TEXT,
+} from '../constants';
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ modalId: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onSubmit() {
+ return axios
+ .post(this.url)
+ .then((response) => {
+ // follow the rediect to refresh the page
+ redirectTo(response.request.responseURL); // eslint-disable-line import/no-deprecated
+ })
+ .catch((error) => {
+ createAlert({
+ message: CANCEL_JOBS_FAILED_TEXT,
+ });
+ throw error;
+ });
+ },
+ },
+ primaryAction: {
+ text: PRIMARY_ACTION_TEXT,
+ attributes: { variant: 'danger' },
+ },
+ cancelAction: {
+ text: CANCEL_TEXT,
+ },
+ CANCEL_JOBS_WARNING,
+ CANCEL_JOBS_MODAL_TITLE,
+};
+</script>
+
+<template>
+ <gl-modal
+ :modal-id="modalId"
+ :action-primary="$options.primaryAction"
+ :action-cancel="$options.cancelAction"
+ :title="$options.CANCEL_JOBS_MODAL_TITLE"
+ @primary="onSubmit"
+ >
+ {{ $options.CANCEL_JOBS_WARNING }}
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
new file mode 100644
index 00000000000..cbb80a5175f
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
@@ -0,0 +1,28 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlLink,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ projectName() {
+ return this.job.pipeline?.project?.fullPath;
+ },
+ projectUrl() {
+ return this.job.pipeline?.project?.webUrl;
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-text-truncate">
+ <gl-link :href="projectUrl"> {{ projectName }}</gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
new file mode 100644
index 00000000000..a76829aa129
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
@@ -0,0 +1,39 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { RUNNER_EMPTY_TEXT, RUNNER_NO_DESCRIPTION } from '../../constants';
+
+export default {
+ i18n: {
+ emptyRunnerText: RUNNER_EMPTY_TEXT,
+ noRunnerDescription: RUNNER_NO_DESCRIPTION,
+ },
+ components: {
+ GlLink,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ adminUrl() {
+ return this.job.runner?.adminUrl;
+ },
+ description() {
+ return this.job.runner?.description
+ ? this.job.runner.description
+ : this.$options.i18n.noRunnerDescription;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-text-truncate">
+ <gl-link v-if="adminUrl" :href="adminUrl">
+ {{ description }}
+ </gl-link>
+ <span v-else data-testid="empty-runner-text"> {{ $options.i18n.emptyRunnerText }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue b/app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue
new file mode 100644
index 00000000000..c305e09af0d
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+
+<template>
+ <gl-skeleton-loader :width="1248" :height="73">
+ <circle cx="748.031" cy="37.7193" r="15.0307" />
+ <circle cx="787.241" cy="37.7193" r="15.0307" />
+ <circle cx="827.759" cy="37.7193" r="15.0307" />
+ <circle cx="866.969" cy="37.7193" r="15.0307" />
+ <circle cx="380" cy="37" r="18" />
+ <rect x="432" y="19" width="126.587" height="15" />
+ <rect x="432" y="41" width="247" height="15" />
+ <rect x="158" y="19" width="86.1" height="15" />
+ <rect x="158" y="41" width="168" height="15" />
+ <rect x="22" y="19" width="96" height="36" />
+ <rect x="924" y="30" width="96" height="15" />
+ <rect x="1057" y="20" width="166" height="35" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/ci/admin/jobs_table/constants.js b/app/assets/javascripts/ci/admin/jobs_table/constants.js
new file mode 100644
index 00000000000..ff0efdb1f5b
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/constants.js
@@ -0,0 +1,35 @@
+import { s__, __ } from '~/locale';
+import { RAW_TEXT_WARNING } from '~/ci/jobs_page/constants';
+
+export const JOBS_COUNT_ERROR_MESSAGE = __('There was an error fetching the number of jobs.');
+export const JOBS_FETCH_ERROR_MSG = __('There was an error fetching the jobs.');
+export const LOADING_ARIA_LABEL = __('Loading');
+export const CANCELABLE_JOBS_ERROR_MSG = __('There was an error fetching the cancelable jobs.');
+export const CANCEL_JOBS_MODAL_ID = 'cancel-jobs-modal';
+export const CANCEL_JOBS_MODAL_TITLE = s__('AdminArea|Are you sure?');
+export const CANCEL_JOBS_BUTTON_TEXT = s__('AdminArea|Cancel all jobs');
+export const CANCEL_BUTTON_TOOLTIP = s__('AdminArea|Cancel all running and pending jobs');
+export const CANCEL_TEXT = __('Cancel');
+export const CANCEL_JOBS_FAILED_TEXT = s__('AdminArea|Canceling jobs failed');
+export const PRIMARY_ACTION_TEXT = s__('AdminArea|Yes, proceed');
+export const CANCEL_JOBS_WARNING = s__(
+ "AdminArea|You're about to cancel all running and pending jobs across this instance. Do you want to proceed?",
+);
+export const RUNNER_EMPTY_TEXT = __('None');
+export const RUNNER_NO_DESCRIPTION = s__('Runners|No description');
+
+/* Admin Table constants */
+/* The field list is based on app/assets/javascripts/jobs/components/table/constants.js */
+export const DEFAULT_FIELDS_ADMIN = [
+ { key: 'status', label: __('Status'), columnClass: 'gl-w-15p' },
+ { key: 'job', label: __('Job'), columnClass: 'gl-w-20p' },
+ { key: 'project', label: __('Project'), columnClass: 'gl-w-20p' },
+ { key: 'runner', label: __('Runner'), columnClass: 'gl-w-15p' },
+ { key: 'pipeline', label: __('Pipeline'), columnClass: 'gl-w-10p' },
+ { key: 'stage', label: __('Stage'), columnClass: 'gl-w-10p' },
+ { key: 'name', label: __('Name'), columnClass: 'gl-w-15p' },
+ { key: 'duration', label: __('Duration'), columnClass: 'gl-w-15p' },
+ { key: 'actions', label: '', columnClass: 'gl-w-10p' },
+];
+
+export const RAW_TEXT_WARNING_ADMIN = RAW_TEXT_WARNING;
diff --git a/app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js b/app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js
new file mode 100644
index 00000000000..fd7ee2a6f8c
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js
@@ -0,0 +1,62 @@
+import { isEqual } from 'lodash';
+
+export default {
+ typePolicies: {
+ Query: {
+ fields: {
+ jobs: {
+ keyArgs: ['statuses'],
+ },
+ },
+ },
+ CiJobConnection: {
+ merge(existing = {}, incoming, { args = {} }) {
+ if (incoming.nodes) {
+ let nodes;
+
+ const areNodesEqual = isEqual(existing.nodes, incoming.nodes);
+ const statuses = Array.isArray(args.statuses) ? [...args.statuses] : args.statuses;
+ const { pageInfo } = incoming;
+
+ if (Object.keys(existing).length !== 0 && isEqual(existing?.statuses, args?.statuses)) {
+ if (areNodesEqual) {
+ if (incoming.pageInfo.hasNextPage) {
+ nodes = [...existing.nodes, ...incoming.nodes];
+ } else {
+ nodes = [...incoming.nodes];
+ }
+ } else {
+ if (!existing.pageInfo?.hasNextPage) {
+ nodes = [...incoming.nodes];
+
+ return {
+ nodes,
+ statuses,
+ pageInfo,
+ count: incoming.count,
+ };
+ }
+
+ nodes = [...existing.nodes, ...incoming.nodes];
+ }
+ } else {
+ nodes = [...incoming.nodes];
+ }
+
+ return {
+ nodes,
+ statuses,
+ pageInfo,
+ count: incoming.count,
+ };
+ }
+
+ return {
+ nodes: existing.nodes,
+ pageInfo: existing.pageInfo,
+ statuses: args.statuses,
+ };
+ },
+ },
+ },
+};
diff --git a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
new file mode 100644
index 00000000000..89fb1782e46
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
@@ -0,0 +1,90 @@
+query getAllJobs(
+ $after: String
+ $first: Int = 50
+ $statuses: [CiJobStatus!]
+ $runnerTypes: [CiRunnerType!]
+) {
+ jobs(after: $after, first: $first, statuses: $statuses, runnerTypes: $runnerTypes) {
+ pageInfo {
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ startCursor
+ }
+ nodes {
+ runner {
+ id
+ description
+ adminUrl
+ }
+ artifacts {
+ nodes {
+ id
+ downloadPath
+ fileType
+ }
+ }
+ allowFailure
+ status
+ scheduledAt
+ manualJob
+ triggered
+ createdByTag
+ detailedStatus {
+ id
+ detailsPath
+ group
+ icon
+ label
+ text
+ tooltip
+ action {
+ id
+ buttonTitle
+ icon
+ method
+ path
+ title
+ }
+ }
+ id
+ refName
+ refPath
+ tags
+ shortSha
+ commitPath
+ pipeline {
+ id
+ project {
+ id
+ fullPath
+ webUrl
+ }
+ path
+ user {
+ id
+ webPath
+ avatarUrl
+ }
+ }
+ stage {
+ id
+ name
+ }
+ name
+ duration
+ finishedAt
+ coverage
+ retryable
+ playable
+ cancelable
+ active
+ stuck
+ userPermissions {
+ readBuild
+ readJobArtifacts
+ updateBuild
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql
new file mode 100644
index 00000000000..bcb0123e9e3
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql
@@ -0,0 +1,5 @@
+query getAllJobsCount($statuses: [CiJobStatus!], $runnerTypes: [CiRunnerType!]) {
+ jobs(statuses: $statuses, runnerTypes: $runnerTypes) {
+ count
+ }
+}
diff --git a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql
new file mode 100644
index 00000000000..9bf5e1449b7
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql
@@ -0,0 +1,5 @@
+query getCancelableJobsCount {
+ cancelable: jobs(statuses: [PENDING, RUNNING]) {
+ count
+ }
+}