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/pipelines/components/pipelines_list/failure_widget')
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row.vue)71
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue166
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue108
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js4
4 files changed, 270 insertions, 79 deletions
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue
index e40e30f2b8d..6b5e3d77b92 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue
@@ -1,16 +1,20 @@
<script>
-import { GlCollapse, GlIcon, GlLink } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
+import { GlButton, GlCollapse, GlIcon, GlLink, GlTooltip } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __, s__, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
+import RetryMrFailedJobMutation from '../../../graphql/mutations/retry_mr_failed_job.mutation.graphql';
export default {
components: {
CiIcon,
+ GlButton,
GlCollapse,
GlIcon,
GlLink,
+ GlTooltip,
},
directives: {
SafeHtml,
@@ -23,14 +27,21 @@ export default {
},
data() {
return {
- isJobLogVisible: false,
isHovered: false,
+ isJobLogVisible: false,
+ isLoadingAction: false,
};
},
computed: {
activeClass() {
return this.isHovered ? 'gl-bg-gray-50' : '';
},
+ canReadBuild() {
+ return this.job.userPermissions.readBuild;
+ },
+ canRetryJob() {
+ return this.job.retryable && this.job.userPermissions.updateBuild;
+ },
isVisibleId() {
return `log-${this.isJobLogVisible ? 'is-visible' : 'is-hidden'}`;
},
@@ -38,7 +49,11 @@ export default {
return this.isJobLogVisible ? 'chevron-down' : 'chevron-right';
},
jobTrace() {
- return this.job?.trace?.htmlSummary || this.$options.i18n.noTraceText;
+ if (this.canReadBuild) {
+ return this.job?.trace?.htmlSummary || this.$options.i18n.noTraceText;
+ }
+
+ return this.$options.i18n.cannotReadBuild;
},
parsedJobId() {
return getIdFromGraphQLId(this.job.id);
@@ -54,18 +69,45 @@ export default {
resetActiveRow() {
this.isHovered = false;
},
- toggleJobLog(e) {
+ async retryJob() {
+ try {
+ this.isLoadingAction = true;
+
+ const {
+ data: {
+ jobRetry: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: RetryMrFailedJobMutation,
+ variables: { id: this.job.id },
+ });
+
+ if (errors.length > 0) {
+ throw new Error(errors[0]);
+ }
+
+ this.$emit('job-retried', this.job.name);
+ } catch (error) {
+ createAlert({ message: error?.message || this.$options.i18n.retryError });
+ } finally {
+ this.isLoadingAction = false;
+ }
+ },
+ toggleJobLog(event) {
// Do not toggle the log visibility when clicking on a link
- if (e.target.tagName === 'A') {
+ if (event.target.tagName === 'A') {
return;
}
-
this.isJobLogVisible = !this.isJobLogVisible;
},
},
i18n: {
+ cannotReadBuild: s__("Job|You do not have permission to read this job's log"),
+ cannotRetry: s__('Job|You do not have permission to retry this job'),
jobActionTooltipText: s__('Pipelines|Retry %{jobName} Job'),
noTraceText: s__('Job|No job log'),
+ retry: __('Retry'),
+ retryError: __('There was an error while retrying this job'),
},
};
</script>
@@ -93,6 +135,21 @@ export default {
<div class="col-2 gl-text-left">
<gl-link :href="job.webPath">#{{ parsedJobId }}</gl-link>
</div>
+ <gl-tooltip v-if="!canRetryJob" :target="() => $refs.retryBtn" placement="top">
+ {{ $options.i18n.cannotRetry }}
+ </gl-tooltip>
+ <div class="col-2 gl-text-left">
+ <span ref="retryBtn">
+ <gl-button
+ :disabled="!canRetryJob"
+ icon="retry"
+ :loading="isLoadingAction"
+ :title="$options.i18n.retry"
+ :aria-label="$options.i18n.retry"
+ @click.stop="retryJob"
+ />
+ </span>
+ </div>
</div>
<div class="row">
<gl-collapse :visible="isJobLogVisible" class="gl-w-full">
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>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue
index fce0b5f525e..5e49c05f47d 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue
@@ -1,22 +1,7 @@
<script>
-import {
- GlButton,
- GlCollapse,
- GlIcon,
- GlLink,
- GlLoadingIcon,
- GlPopover,
- GlSprintf,
-} from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { __, s__ } from '~/locale';
-import getPipelineFailedJobs from '../../../graphql/queries/get_pipeline_failed_jobs.query.graphql';
-import WidgetFailedJobRow from './widget_failed_job_row.vue';
-import { sortJobsByStatus } from './utils';
-
-const JOB_ID_HEADER = __('Job ID');
-const JOB_NAME_HEADER = __('Job name');
-const STAGE_HEADER = __('Stage');
+import { GlButton, GlCollapse, GlIcon, GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
+import FailedJobsList from './failed_jobs_list.vue';
export default {
components: {
@@ -24,13 +9,20 @@ export default {
GlCollapse,
GlIcon,
GlLink,
- GlLoadingIcon,
GlPopover,
GlSprintf,
- WidgetFailedJobRow,
+ FailedJobsList,
},
inject: ['fullPath'],
props: {
+ failedJobsCount: {
+ required: true,
+ type: Number,
+ },
+ isPipelineActive: {
+ required: true,
+ type: Boolean,
+ },
pipelineIid: {
required: true,
type: Number,
@@ -42,62 +34,44 @@ export default {
},
data() {
return {
- failedJobs: [],
+ currentFailedJobsCount: this.failedJobsCount,
+ isActive: false,
isExpanded: false,
};
},
- apollo: {
- failedJobs: {
- query: getPipelineFailedJobs,
- skip() {
- return !this.isExpanded;
- },
- variables() {
- return {
- fullPath: this.fullPath,
- pipelineIid: this.pipelineIid,
- };
- },
- update(data) {
- const jobs = data?.project?.pipeline?.jobs?.nodes || [];
- return sortJobsByStatus(jobs);
- },
- error(e) {
- createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
- },
- },
- },
computed: {
bodyClasses() {
return this.isExpanded ? '' : 'gl-display-none';
},
- failedJobsCount() {
- return this.failedJobs.length;
+ failedJobsCountText() {
+ return sprintf(this.$options.i18n.showFailedJobs, { count: this.currentFailedJobsCount });
},
iconName() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
},
- isLoading() {
- return this.$apollo.queries.failedJobs.loading;
+ popoverId() {
+ return `popover-${this.pipelineIid}`;
+ },
+ },
+ watch: {
+ failedJobsCount(val) {
+ this.currentFailedJobsCount = val;
},
},
methods: {
+ setFailedJobsCount(count) {
+ this.currentFailedJobsCount = count;
+ },
toggleWidget() {
this.isExpanded = !this.isExpanded;
},
},
- columns: [
- { text: JOB_NAME_HEADER, class: 'col-6' },
- { text: STAGE_HEADER, class: 'col-2' },
- { text: JOB_ID_HEADER, class: 'col-2' },
- ],
i18n: {
additionalInfoPopover: s__(
'Pipelines|You will see a maximum of 100 jobs in this list. To view all failed jobs, %{linkStart}go to the details page%{linkEnd} of this pipeline.',
),
additionalInfoTitle: __('Limitation on this view'),
- fetchError: __('There was a problem fetching failed jobs'),
- showFailedJobs: __('Show failed jobs'),
+ showFailedJobs: __('Show failed jobs (%{count})'),
},
};
</script>
@@ -105,9 +79,9 @@ export default {
<div class="gl-border-none!">
<gl-button variant="link" @click="toggleWidget">
<gl-icon :name="iconName" />
- {{ $options.i18n.showFailedJobs }}
- <gl-icon id="target" name="information-o" />
- <gl-popover target="target" placement="top">
+ {{ failedJobsCountText }}
+ <gl-icon :id="popoverId" name="information-o" />
+ <gl-popover :target="popoverId" placement="top">
<template #title> {{ $options.i18n.additionalInfoTitle }} </template>
<slot>
<gl-sprintf :message="$options.i18n.additionalInfoPopover">
@@ -118,26 +92,16 @@ export default {
</slot>
</gl-popover>
</gl-button>
- <gl-loading-icon v-if="isLoading" />
<gl-collapse
- v-else
v-model="isExpanded"
class="gl-bg-gray-10 gl-border-1 gl-border-t gl-border-color-gray-100 gl-mt-4 gl-pt-3"
>
- <div 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>
- <widget-failed-job-row v-for="job in failedJobs" :key="job.id" :job="job" />
+ <failed-jobs-list
+ v-if="isExpanded"
+ :is-pipeline-active="isPipelineActive"
+ :pipeline-iid="pipelineIid"
+ @failed-jobs-count="setFailedJobsCount"
+ />
</gl-collapse>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js
index 3f395fff7e0..2d0c467c54f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js
@@ -13,3 +13,7 @@ export const sortJobsByStatus = (jobs = []) => {
return 1;
});
};
+
+export const graphqlEtagPipelinePath = (graphqlPath, pipelineId) => {
+ return `${graphqlPath}pipelines/id/${pipelineId}`;
+};