diff options
Diffstat (limited to 'app/assets/javascripts/ci/pipeline_details')
11 files changed, 139 insertions, 95 deletions
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue index c6340e6787a..afe66588fb9 100644 --- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue +++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue @@ -5,7 +5,7 @@ import delayedJobMixin from '~/ci/mixins/delayed_job_mixin'; import { helpPagePath } from '~/helpers/help_page_helper'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { __, s__, sprintf } from '~/locale'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue'; import ActionComponent from '../../../common/private/job_action_component.vue'; import JobNameComponent from '../../../common/private/job_name_component.vue'; import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from '../constants'; @@ -343,7 +343,7 @@ export default { </div> <gl-badge v-if="isBridge" - class="gl-mt-3" + class="gl-mt-3 gl-ml-7" variant="info" size="sm" data-testid="job-bridge-badge" diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue index 26521f87426..76ff662cd3f 100644 --- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue +++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue @@ -14,7 +14,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { __, sprintf } from '~/locale'; import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql'; import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue'; import { reportToSentry } from '~/ci/utils'; import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants'; @@ -287,7 +287,7 @@ export default { /> <div v-else :class="$options.styles.actionSizeClasses"></div> </div> - <div class="gl-pt-2"> + <div class="gl-pt-2 gl-ml-7"> <gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label"> {{ label }} </gl-badge> diff --git a/app/assets/javascripts/ci/pipeline_details/header/constants.js b/app/assets/javascripts/ci/pipeline_details/header/constants.js new file mode 100644 index 00000000000..a4aed7b8f46 --- /dev/null +++ b/app/assets/javascripts/ci/pipeline_details/header/constants.js @@ -0,0 +1,9 @@ +export const DELETE_MODAL_ID = 'pipeline-delete-modal'; + +export const POLL_INTERVAL = 10000; + +export const SCHEDULE_SOURCE = 'schedule'; +export const AUTO_DEVOPS_SOURCE = 'AUTO_DEVOPS_SOURCE'; +export const DETACHED_EVENT_TYPE = 'DETACHED'; +export const MERGED_RESULT_EVENT_TYPE = 'MERGED_RESULT'; +export const MERGE_TRAIN_EVENT_TYPE = 'MERGE_TRAIN'; diff --git a/app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql b/app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql new file mode 100644 index 00000000000..80fc8b92a47 --- /dev/null +++ b/app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql @@ -0,0 +1,4 @@ +fragment PipelineHeaderData on Pipeline { + id + iid +} diff --git a/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql index eb5643126a2..4ef79aaa03c 100644 --- a/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql +++ b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql @@ -1,15 +1,16 @@ +#import "ee_else_ce/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql" + query getPipelineHeaderData($fullPath: ID!, $iid: ID!) { project(fullPath: $fullPath) { id pipeline(iid: $iid) { - id - iid status retryable cancelable userPermissions { destroyPipeline updatePipeline + cancelPipeline } detailedStatus { id @@ -41,6 +42,19 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) { finishedAt queuedDuration duration + name + totalJobs + refText + triggeredByPath + stuck + child + complete + latest + mergeRequestEventType + configSource + failureReason + source + ...PipelineHeaderData } } } diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue index 651662d6395..1ecc4b2e1c1 100644 --- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue +++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue @@ -17,7 +17,7 @@ import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint- import { __, s__, sprintf, formatNumber } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import SafeHtml from '~/vue_shared/directives/safe_html'; import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants'; @@ -26,9 +26,15 @@ import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutatio import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql'; import { getQueryHeaders } from '../graph/utils'; import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.graphql'; - -const DELETE_MODAL_ID = 'pipeline-delete-modal'; -const POLL_INTERVAL = 10000; +import { + DELETE_MODAL_ID, + POLL_INTERVAL, + DETACHED_EVENT_TYPE, + AUTO_DEVOPS_SOURCE, + SCHEDULE_SOURCE, + MERGE_TRAIN_EVENT_TYPE, + MERGED_RESULT_EVENT_TYPE, +} from './constants'; export default { name: 'PipelineDetailsHeader', @@ -129,40 +135,14 @@ export default { }, }, props: { - name: { - type: String, - required: false, - default: '', - }, - totalJobs: { - type: String, - required: false, - default: '', - }, - computeMinutes: { - type: String, - required: false, - default: '', - }, yamlErrors: { type: String, required: false, default: '', }, - failureReason: { - type: String, - required: false, - default: '', - }, - refText: { - type: String, - required: false, - default: '', - }, - badges: { - type: Object, - required: false, - default: () => {}, + trigger: { + type: Boolean, + required: true, }, }, apollo: { @@ -270,7 +250,7 @@ export default { }, totalJobsText() { return sprintf(__('%{jobs} Jobs'), { - jobs: this.totalJobs, + jobs: this.pipeline?.totalJobs || 0, }); }, triggeredText() { @@ -312,10 +292,61 @@ export default { canCancelPipeline() { const { cancelable, userPermissions } = this.pipeline; - return cancelable && userPermissions.updatePipeline; + return cancelable && userPermissions.cancelPipeline; + }, + computeMinutes() { + return this.pipeline?.computeMinutes; }, showComputeMinutes() { - return this.isFinished && this.computeMinutes !== '0.0'; + return this.isFinished && this.computeMinutes; + }, + pipelineName() { + return this.pipeline?.name; + }, + refText() { + return this.pipeline?.refText; + }, + triggeredByPath() { + return this.pipeline?.triggeredByPath; + }, + mergeRequestEventType() { + return this.pipeline.mergeRequestEventType; + }, + isMergeTrainPipeline() { + return this.mergeRequestEventType === MERGE_TRAIN_EVENT_TYPE; + }, + isMergedResultsPipeline() { + return this.mergeRequestEventType === MERGED_RESULT_EVENT_TYPE; + }, + isDetachedPipeline() { + return this.mergeRequestEventType === DETACHED_EVENT_TYPE; + }, + isAutoDevopsPipeline() { + return this.pipeline.configSource === AUTO_DEVOPS_SOURCE; + }, + isScheduledPipeline() { + return this.pipeline.source === SCHEDULE_SOURCE; + }, + isInvalidPipeline() { + return Boolean(this.yamlErrors); + }, + failureReason() { + return this.pipeline.failureReason; + }, + badges() { + return { + schedule: this.isScheduledPipeline, + trigger: this.trigger, + invalid: this.isInvalidPipeline, + child: this.pipeline.child, + latest: this.pipeline.latest, + mergeTrainPipeline: this.isMergeTrainPipeline, + mergedResultsPipeline: this.isMergedResultsPipeline, + detached: this.isDetachedPipeline, + failed: Boolean(this.failureReason), + autoDevops: this.isAutoDevopsPipeline, + stuck: this.pipeline.stuck, + }; }, }, methods: { @@ -406,7 +437,9 @@ export default { <gl-loading-icon v-if="loading" class="gl-text-left" size="lg" /> <div v-else class="gl-display-flex gl-justify-content-space-between gl-flex-wrap"> <div> - <h3 v-if="name" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name">{{ name }}</h3> + <h3 v-if="pipelineName" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name"> + {{ pipelineName }} + </h3> <h3 v-else class="gl-mt-0 gl-mb-3" data-testid="pipeline-commit-title"> {{ commitTitle }} </h3> @@ -483,7 +516,7 @@ export default { > <gl-sprintf :message="$options.i18n.childBadgeText"> <template #link="{ content }"> - <gl-link :href="paths.triggeredByPath" target="_blank"> + <gl-link :href="triggeredByPath" target="_blank"> {{ content }} </gl-link> </template> diff --git a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue index 287f6e045c6..1823908c231 100644 --- a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue +++ b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue @@ -5,7 +5,7 @@ import { __, s__ } from '~/locale'; import { createAlert } from '~/alert'; import Tracking from '~/tracking'; import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated -import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue'; import { TRACKING_CATEGORIES } from '~/ci/constants'; import RetryFailedJobMutation from '../graphql/mutations/retry_failed_job.mutation.graphql'; import { DEFAULT_FIELDS } from '../../constants'; diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js index 4966b657887..0430bc83dd7 100644 --- a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js +++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js @@ -12,29 +12,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph return; } - const { - fullPath, - pipelineIid, - pipelinesPath, - name, - totalJobs, - computeMinutes, - yamlErrors, - failureReason, - triggeredByPath, - schedule, - trigger, - child, - latest, - mergeTrainPipeline, - mergedResultsPipeline, - invalid, - failed, - autoDevops, - detached, - stuck, - refText, - } = el.dataset; + const { fullPath, pipelineIid, pipelinesPath, yamlErrors, trigger } = el.dataset; // eslint-disable-next-line no-new new Vue({ @@ -46,32 +24,14 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph fullProject: fullPath, graphqlResourceEtag, pipelinesPath, - triggeredByPath, }, pipelineIid, }, render(createElement) { return createElement(PipelineDetailsHeader, { props: { - name, - totalJobs, - computeMinutes, yamlErrors, - failureReason, - refText, - badges: { - schedule: parseBoolean(schedule), - trigger: parseBoolean(trigger), - child: parseBoolean(child), - latest: parseBoolean(latest), - mergeTrainPipeline: parseBoolean(mergeTrainPipeline), - mergedResultsPipeline: parseBoolean(mergedResultsPipeline), - invalid: parseBoolean(invalid), - failed: parseBoolean(failed), - autoDevops: parseBoolean(autoDevops), - detached: parseBoolean(detached), - stuck: parseBoolean(stuck), - }, + trigger: parseBoolean(trigger), }, }); }, diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js index ea2875713a9..b4528ab895d 100644 --- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js +++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js @@ -44,6 +44,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { params, fullPath, visibilityPipelineIdType, + showJenkinsCiPrompt, } = el.dataset; return new Vue({ @@ -57,6 +58,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { pipelineEditorPath, pipelineSchedulesPath, suggestedCiTemplates: JSON.parse(suggestedCiTemplates), + showJenkinsCiPrompt: parseBoolean(showJenkinsCiPrompt), }, data() { return { diff --git a/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js index 6b616601bc5..e3984685094 100644 --- a/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js +++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js @@ -1,4 +1,5 @@ import { __, sprintf } from '~/locale'; +import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import { TestStatus } from '../../constants'; /** @@ -25,15 +26,27 @@ export function iconForTestStatus(status) { return 'status_notfound'; } } - export const formattedTime = (seconds = 0) => { if (seconds < 1) { - const milliseconds = seconds * 1000; - return sprintf(__('%{milliseconds}ms'), { milliseconds: milliseconds.toFixed(2) }); + return sprintf(__('%{milliseconds}ms'), { + milliseconds: (seconds * 1000).toFixed(2), + }); + } + if (seconds < 60) { + return sprintf(__('%{seconds}s'), { + seconds: (seconds % 60).toFixed(2), + }); } - return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) }); -}; + const hoursAndMinutes = stringifyTime(parseSeconds(seconds)); + const remainingSeconds = + seconds % 60 >= 1 + ? sprintf(__('%{seconds}s'), { + seconds: Math.floor(seconds % 60), + }) + : ''; + return `${hoursAndMinutes} ${remainingSeconds}`.trim(); +}; export const addIconStatus = (testCase) => ({ ...testCase, icon: iconForTestStatus(testCase.status), diff --git a/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue index a7737d33285..6e9a705c046 100644 --- a/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue +++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue @@ -2,6 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; // eslint-disable-next-line no-restricted-imports import { mapActions, mapGetters, mapState } from 'vuex'; +import { getParameterValues } from '~/lib/utils/url_utility'; import EmptyState from './empty_state.vue'; import TestSuiteTable from './test_suite_table.vue'; import TestSummary from './test_summary.vue'; @@ -19,7 +20,7 @@ export default { inject: ['blobPath', 'summaryEndpoint', 'suiteEndpoint'], computed: { ...mapState('testReports', ['isLoading', 'selectedSuiteIndex', 'testReports']), - ...mapGetters('testReports', ['getSelectedSuite']), + ...mapGetters('testReports', ['getSelectedSuite', 'getTestSuites']), showSuite() { return this.selectedSuiteIndex !== null; }, @@ -28,8 +29,16 @@ export default { return testSuites.length > 0; }, }, - created() { - this.fetchSummary(); + async created() { + await this.fetchSummary(); + const jobName = getParameterValues('job_name')[0] || ''; + if (jobName.length > 0) { + // get the index from the job name + const indexToSelect = this.getTestSuites.findIndex((test) => test.name === jobName); + + this.setSelectedSuiteIndex(indexToSelect); + this.fetchTestSuite(indexToSelect); + } }, methods: { ...mapActions('testReports', [ |