diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue')
-rw-r--r-- | app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue | 166 |
1 files changed, 166 insertions, 0 deletions
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue new file mode 100644 index 00000000000..36687129cdd --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue @@ -0,0 +1,166 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import { createAlert } from '~/alert'; +import { __, s__, sprintf } from '~/locale'; +import { getQueryHeaders } from '~/pipelines/components/graph/utils'; +import getPipelineFailedJobs from '../../../graphql/queries/get_pipeline_failed_jobs.query.graphql'; +import { graphqlEtagPipelinePath, sortJobsByStatus } from './utils'; +import FailedJobDetails from './failed_job_details.vue'; + +const POLL_INTERVAL = 10000; + +const JOB_ACTION_HEADER = __('Actions'); +const JOB_ID_HEADER = __('Job ID'); +const JOB_NAME_HEADER = __('Job name'); +const STAGE_HEADER = __('Stage'); + +export default { + components: { + GlLoadingIcon, + FailedJobDetails, + }, + inject: ['fullPath', 'graphqlPath'], + props: { + isPipelineActive: { + required: true, + type: Boolean, + }, + pipelineIid: { + type: Number, + required: true, + }, + }, + data() { + return { + failedJobs: [], + isActive: false, + isLoadingMore: false, + }; + }, + apollo: { + failedJobs: { + context() { + return getQueryHeaders(this.graphqlResourceEtag); + }, + query: getPipelineFailedJobs, + pollInterval: POLL_INTERVAL, + variables() { + return { + fullPath: this.fullPath, + pipelineIid: this.pipelineIid, + }; + }, + update(data) { + const jobs = data?.project?.pipeline?.jobs?.nodes || []; + return sortJobsByStatus(jobs); + }, + result({ data }) { + const pipeline = data?.project?.pipeline; + + if (pipeline?.jobs?.count) { + this.$emit('failed-jobs-count', pipeline.jobs.count); + this.isActive = pipeline.active; + } + }, + error(e) { + createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' }); + }, + }, + }, + computed: { + graphqlResourceEtag() { + return graphqlEtagPipelinePath(this.graphqlPath, this.pipelineIid); + }, + hasFailedJobs() { + return this.failedJobs.length > 0; + }, + isInitialLoading() { + return this.isLoading && !this.isLoadingMore; + }, + isLoading() { + return this.$apollo.queries.failedJobs.loading; + }, + }, + watch: { + isPipelineActive(flag) { + // Turn polling on and off based on REST actions + // By refetching jobs, we will get the graphql `active` + // field to update properly and cascade the polling changes + this.refetchJobs(); + this.handlePolling(flag); + }, + isActive(flag) { + this.handlePolling(flag); + }, + }, + mounted() { + if (!this.isActive && !this.isPipelineActive) { + this.handlePolling(false); + } + }, + methods: { + handlePolling(isActive) { + // If the pipeline status has changed and the widget is not expanded, + // We start polling. + if (isActive) { + this.$apollo.queries.failedJobs.startPolling(POLL_INTERVAL); + } else { + this.$apollo.queries.failedJobs.stopPolling(); + } + }, + async retryJob(jobName) { + await this.refetchJobs(); + + this.$toast.show(sprintf(this.$options.i18n.retriedJobsSuccess, { jobName })); + }, + async refetchJobs() { + this.isLoadingMore = true; + + try { + await this.$apollo.queries.failedJobs.refetch(); + } catch { + createAlert(this.$options.i18n.fetchError); + } finally { + this.isLoadingMore = false; + } + }, + }, + columns: [ + { text: JOB_NAME_HEADER, class: 'col-6' }, + { text: STAGE_HEADER, class: 'col-2' }, + { text: JOB_ID_HEADER, class: 'col-2' }, + { text: JOB_ACTION_HEADER, class: 'col-2' }, + ], + i18n: { + fetchError: __('There was a problem fetching failed jobs'), + noFailedJobs: s__('Pipeline|No failed jobs in this pipeline 🎉'), + retriedJobsSuccess: __('%{jobName} job is being retried'), + }, +}; +</script> + +<template> + <div> + <gl-loading-icon v-if="isInitialLoading" /> + <div v-else-if="!hasFailedJobs">{{ $options.i18n.noFailedJobs }}</div> + <div v-else class="container-fluid gl-grid-tpl-rows-auto"> + <div class="row gl-mb-6 gl-text-gray-900"> + <div + v-for="col in $options.columns" + :key="col.text" + class="gl-font-weight-bold gl-text-left" + :class="col.class" + data-testid="header" + > + {{ col.text }} + </div> + </div> + </div> + <failed-job-details + v-for="job in failedJobs" + :key="job.id" + :job="job" + @job-retried="retryJob" + /> + </div> +</template> |