diff options
Diffstat (limited to 'app/assets/javascripts/pipelines/components/pipelines_list')
6 files changed, 137 insertions, 70 deletions
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue index 6b5e3d77b92..edf4cc87a87 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue @@ -1,17 +1,17 @@ <script> -import { GlButton, GlCollapse, GlIcon, GlLink, GlTooltip } from '@gitlab/ui'; +import { GlButton, 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 { BRIDGE_KIND } from '~/pipelines/components/graph/constants'; import RetryMrFailedJobMutation from '../../../graphql/mutations/retry_mr_failed_job.mutation.graphql'; export default { components: { CiIcon, GlButton, - GlCollapse, GlIcon, GlLink, GlTooltip, @@ -33,17 +33,14 @@ export default { }; }, computed: { - activeClass() { - return this.isHovered ? 'gl-bg-gray-50' : ''; - }, canReadBuild() { return this.job.userPermissions.readBuild; }, canRetryJob() { - return this.job.retryable && this.job.userPermissions.updateBuild; + return this.job.retryable && this.job.userPermissions.updateBuild && !this.isBridgeJob; }, - isVisibleId() { - return `log-${this.isJobLogVisible ? 'is-visible' : 'is-hidden'}`; + isBridgeJob() { + return this.job.kind === BRIDGE_KIND; }, jobChevronName() { return this.isJobLogVisible ? 'chevron-down' : 'chevron-right'; @@ -58,6 +55,11 @@ export default { parsedJobId() { return getIdFromGraphQLId(this.job.id); }, + tooltipErrorText() { + return this.isBridgeJob + ? this.$options.i18n.cannotRetryTrigger + : this.$options.i18n.cannotRetry; + }, tooltipText() { return sprintf(this.$options.i18n.jobActionTooltipText, { jobName: this.job.name }); }, @@ -102,8 +104,9 @@ export default { }, }, 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'), + cannotReadBuild: s__("Job|You do not have permission to read this job's log."), + cannotRetry: s__('Job|You do not have permission to run this job again.'), + cannotRetryTrigger: s__('Job|You cannot rerun trigger jobs from this list.'), jobActionTooltipText: s__('Pipelines|Retry %{jobName} Job'), noTraceText: s__('Job|No job log'), retry: __('Retry'), @@ -114,8 +117,7 @@ export default { <template> <div class="container-fluid gl-grid-tpl-rows-auto"> <div - class="row gl-py-4 gl-cursor-pointer gl-display-flex gl-align-items-center" - :class="activeClass" + class="row gl-my-3 gl-cursor-pointer gl-display-flex gl-align-items-center" :aria-pressed="isJobLogVisible" role="button" tabindex="0" @@ -127,22 +129,23 @@ export default { @mouseout="resetActiveRow" > <div class="col-6 gl-text-gray-900 gl-font-weight-bold gl-text-left"> - <gl-icon :name="jobChevronName" class="gl-fill-blue-500" /> + <gl-icon :name="jobChevronName" /> <ci-icon :status="job.detailedStatus" /> {{ job.name }} </div> <div class="col-2 gl-text-left">{{ job.stage.name }}</div> <div class="col-2 gl-text-left"> - <gl-link :href="job.webPath">#{{ parsedJobId }}</gl-link> + <gl-link :href="job.detailedStatus.detailsPath">#{{ parsedJobId }}</gl-link> </div> <gl-tooltip v-if="!canRetryJob" :target="() => $refs.retryBtn" placement="top"> - {{ $options.i18n.cannotRetry }} + {{ tooltipErrorText }} </gl-tooltip> - <div class="col-2 gl-text-left"> + <div class="col-2 gl-text-right"> <span ref="retryBtn"> <gl-button :disabled="!canRetryJob" icon="retry" + category="tertiary" :loading="isLoadingAction" :title="$options.i18n.retry" :aria-label="$options.i18n.retry" @@ -151,14 +154,12 @@ export default { </span> </div> </div> - <div class="row"> - <gl-collapse :visible="isJobLogVisible" class="gl-w-full"> - <pre - v-safe-html="jobTrace" - class="gl-bg-gray-900 gl-text-white" - :data-testid="isVisibleId" - ></pre> - </gl-collapse> + <div v-if="isJobLogVisible" class="row"> + <pre + v-safe-html="jobTrace" + class="gl-bg-gray-900 gl-text-white gl-w-full" + data-testid="job-log" + ></pre> </div> </div> </template> 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 index 36687129cdd..2c5aa84bc4f 100644 --- 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 @@ -9,9 +9,8 @@ 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 JOB_ID_HEADER = __('ID'); +const JOB_NAME_HEADER = __('Name'); const STAGE_HEADER = __('Stage'); export default { @@ -19,8 +18,12 @@ export default { GlLoadingIcon, FailedJobDetails, }, - inject: ['fullPath', 'graphqlPath'], + inject: ['graphqlPath'], props: { + failedJobsCount: { + required: true, + type: Number, + }, isPipelineActive: { required: true, type: Boolean, @@ -29,6 +32,10 @@ export default { type: Number, required: true, }, + projectPath: { + type: String, + required: true, + }, }, data() { return { @@ -46,7 +53,7 @@ export default { pollInterval: POLL_INTERVAL, variables() { return { - fullPath: this.fullPath, + fullPath: this.projectPath, pipelineIid: this.pipelineIid, }; }, @@ -92,6 +99,13 @@ export default { isActive(flag) { this.handlePolling(flag); }, + failedJobsCount(count) { + // If the REST data is updated first, we force a refetch + // to keep them in sync + if (this.failedJobs.length !== count) { + this.$apollo.queries.failedJobs.refetch(); + } + }, }, mounted() { if (!this.isActive && !this.isPipelineActive) { @@ -129,7 +143,6 @@ export default { { 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'), @@ -141,10 +154,10 @@ export default { <template> <div> - <gl-loading-icon v-if="isInitialLoading" /> - <div v-else-if="!hasFailedJobs">{{ $options.i18n.noFailedJobs }}</div> + <gl-loading-icon v-if="isInitialLoading" class="gl-p-4" /> + <div v-else-if="!hasFailedJobs" class="gl-p-4">{{ $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 class="row gl-my-4 gl-text-gray-900"> <div v-for="col in $options.columns" :key="col.text" 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 5e49c05f47d..60c429459bf 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,12 +1,12 @@ <script> -import { GlButton, GlCollapse, GlIcon, GlLink, GlPopover, GlSprintf } from '@gitlab/ui'; +import { GlButton, GlCard, GlIcon, GlLink, GlPopover, GlSprintf } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; import FailedJobsList from './failed_jobs_list.vue'; export default { components: { GlButton, - GlCollapse, + GlCard, GlIcon, GlLink, GlPopover, @@ -31,6 +31,10 @@ export default { required: true, type: String, }, + projectPath: { + required: true, + type: String, + }, }, data() { return { @@ -44,7 +48,7 @@ export default { return this.isExpanded ? '' : 'gl-display-none'; }, failedJobsCountText() { - return sprintf(this.$options.i18n.showFailedJobs, { count: this.currentFailedJobsCount }); + return sprintf(this.$options.i18n.failedJobsLabel, { count: this.currentFailedJobsCount }); }, iconName() { return this.isExpanded ? 'chevron-down' : 'chevron-right'; @@ -71,37 +75,47 @@ export default { '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'), - showFailedJobs: __('Show failed jobs (%{count})'), + failedJobsLabel: __('Failed jobs (%{count})'), }, }; </script> <template> - <div class="gl-border-none!"> - <gl-button variant="link" @click="toggleWidget"> - <gl-icon :name="iconName" /> - {{ 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"> - <template #link="{ content }"> - <gl-link class="gl-font-sm" :href="pipelinePath"> {{ content }}</gl-link> - </template> - </gl-sprintf> - </slot> - </gl-popover> - </gl-button> - <gl-collapse - 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" - > - <failed-jobs-list - v-if="isExpanded" - :is-pipeline-active="isPipelineActive" - :pipeline-iid="pipelineIid" - @failed-jobs-count="setFailedJobsCount" - /> - </gl-collapse> - </div> + <gl-card + class="gl-new-card" + :class="{ 'gl-border-white gl-hover-border-gray-100': !isExpanded }" + header-class="gl-new-card-header gl-px-3 gl-py-3" + body-class="gl-new-card-body" + data-testid="failed-jobs-card" + :aria-expanded="isExpanded.toString()" + > + <template #header> + <gl-button + variant="link" + class="gl-text-gray-700! gl-font-weight-semibold" + @click="toggleWidget" + > + <gl-icon :name="iconName" /> + {{ failedJobsCountText }} + <gl-icon :id="popoverId" name="information-o" class="gl-ml-2" /> + <gl-popover :target="popoverId" placement="top"> + <template #title> {{ $options.i18n.additionalInfoTitle }} </template> + <slot> + <gl-sprintf :message="$options.i18n.additionalInfoPopover"> + <template #link="{ content }"> + <gl-link class="gl-font-sm" :href="pipelinePath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </slot> + </gl-popover> + </gl-button> + </template> + <failed-jobs-list + v-if="isExpanded" + :failed-jobs-count="failedJobsCount" + :is-pipeline-active="isPipelineActive" + :pipeline-iid="pipelineIid" + :project-path="projectPath" + @failed-jobs-count="setFailedJobsCount" + /> + </gl-card> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue index 73a255f392b..747d94d92f2 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue @@ -16,6 +16,9 @@ import { TRACKING_CATEGORIES } from '../../constants'; export const i18n = { downloadArtifacts: __('Download artifacts'), artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'), + artifactsFetchWarningMessage: s__( + 'Pipelines|Failed to update. Please reload page to update the list of artifacts.', + ), emptyArtifactsMessage: __('No artifacts found'), }; @@ -52,6 +55,7 @@ export default { hasError: false, isLoading: false, searchQuery: '', + isNewPipeline: false, }; }, computed: { @@ -64,13 +68,24 @@ export default { : this.artifacts; }, }, + watch: { + pipelineId() { + this.isNewPipeline = true; + }, + }, methods: { fetchArtifacts() { // refactor tracking based on action once this dropdown supports // actions other than artifacts this.track('click_artifacts_dropdown', { label: TRACKING_CATEGORIES.table }); + // Preserve the last good list and present it if a request fails + const oldArtifacts = [...this.artifacts]; + this.artifacts = []; + + this.hasError = false; this.isLoading = true; + // Replace the placeholder with the ID of the pipeline we are viewing const endpoint = this.artifactsEndpoint.replace( this.artifactsEndpointPlaceholder, @@ -80,9 +95,13 @@ export default { .get(endpoint) .then(({ data }) => { this.artifacts = data.artifacts; + this.isNewPipeline = false; }) .catch(() => { this.hasError = true; + if (!this.isNewPipeline) { + this.artifacts = oldArtifacts; + } }) .finally(() => { this.isLoading = false; @@ -108,10 +127,10 @@ export default { right lazy text-sr-only - @show.once="fetchArtifacts" + @show="fetchArtifacts" @shown="handleDropdownShown" > - <gl-alert v-if="hasError" variant="danger" :dismissible="false"> + <gl-alert v-if="hasError && !hasArtifacts" variant="danger" :dismissible="false"> {{ $options.i18n.artifactsFetchErrorMessage }} </gl-alert> @@ -136,5 +155,18 @@ export default { > {{ artifact.name }} </gl-dropdown-item> + + <template #footer> + <gl-dropdown-item + v-if="hasError && hasArtifacts" + class="gl-list-style-none" + disabled + data-testid="artifacts-fetch-warning" + > + <span class="gl-font-sm"> + {{ $options.i18n.artifactsFetchWarningMessage }} + </span> + </gl-dropdown-item> + </template> </gl-dropdown> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 7d41700c492..574d291a767 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -1,3 +1,4 @@ +<!-- eslint-disable vue/multi-word-component-names --> <script> import { GlEmptyState, GlIcon, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui'; import { isEqual } from 'lodash'; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index dbb0b443235..c03085e6419 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -1,10 +1,11 @@ <script> import { GlTableLite, GlTooltipDirective } from '@gitlab/ui'; +import { cleanLeadingSeparator } from '~/lib/utils/url_utility'; import { s__, __ } from '~/locale'; import Tracking from '~/tracking'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils'; -import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; +import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue'; import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue'; import eventHub from '../../event_hub'; import { TRACKING_CATEGORIES } from '../../constants'; @@ -21,8 +22,8 @@ const DEFAULT_TH_CLASSES = export default { components: { GlTableLite, + LegacyPipelineMiniGraph, PipelineFailedJobsWidget, - PipelineMiniGraph, PipelineOperations, PipelinesStatusBadge, PipelineStopModal, @@ -146,6 +147,9 @@ export default { const downstream = pipeline.triggered; return keepLatestDownstreamPipelines(downstream); }, + getProjectPath(item) { + return cleanLeadingSeparator(item.project.full_path); + }, failedJobsCount(pipeline) { return pipeline?.failed_builds?.length || 0; }, @@ -204,7 +208,7 @@ export default { </template> <template #cell(stages)="{ item }"> - <pipeline-mini-graph + <legacy-pipeline-mini-graph :downstream-pipelines="getDownstreamPipelines(item)" :pipeline-path="item.path" :stages="item.details.stages" @@ -225,6 +229,8 @@ export default { :is-pipeline-active="item.active" :pipeline-iid="item.iid" :pipeline-path="item.path" + :project-path="getProjectPath(item)" + class="gl-ml-n4 gl-mt-n3 gl-mb-n1" /> </template> </gl-table-lite> |