diff options
Diffstat (limited to 'app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue')
-rw-r--r-- | app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue | 271 |
1 files changed, 271 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> |