diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 18:44:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 18:44:42 +0300 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/jobs | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/jobs')
15 files changed, 479 insertions, 24 deletions
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 91ab68d5f39..be95001a396 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -3,6 +3,7 @@ import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml, GlAlert } from import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { throttle, isEmpty } from 'lodash'; import { mapGetters, mapState, mapActions } from 'vuex'; +import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.vue'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { sprintf } from '~/locale'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; @@ -32,6 +33,7 @@ export default { GlLoadingIcon, SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'), GlAlert, + CodeQualityWalkthrough, }, directives: { SafeHtml, @@ -72,6 +74,11 @@ export default { required: false, default: null, }, + codeQualityHelpUrl: { + type: String, + required: false, + default: null, + }, }, computed: { ...mapState([ @@ -120,6 +127,10 @@ export default { shouldRenderHeaderCallout() { return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure; }, + + shouldRenderCodeQualityWalkthrough() { + return this.job.status.group === 'failed-with-warnings'; + }, }, watch: { // Once the job log is loaded, @@ -190,7 +201,7 @@ export default { </script> <template> <div> - <gl-loading-icon v-if="isLoading" size="lg" class="qa-loading-animation prepend-top-20" /> + <gl-loading-icon v-if="isLoading" size="lg" class="qa-loading-animation gl-mt-6" /> <template v-else-if="shouldRenderContent"> <div class="build-page" data-testid="job-content"> @@ -216,6 +227,11 @@ export default { > <div v-safe-html="job.callout_message"></div> </gl-alert> + <code-quality-walkthrough + v-if="shouldRenderCodeQualityWalkthrough" + step="troubleshoot_job" + :link="codeQualityHelpUrl" + /> </header> <!-- EO Header Section --> @@ -256,17 +272,17 @@ export default { <div v-if="job.archived" - class="gl-mt-3 archived-job" - :class="{ 'sticky-top border-bottom-0': hasTrace }" + class="gl-mt-3 gl-py-2 gl-px-3 gl-align-items-center gl-z-index-1 gl-m-auto archived-job" + :class="{ 'sticky-top gl-border-bottom-0': hasTrace }" data-testid="archived-job" > - <gl-icon name="lock" class="align-text-bottom" /> + <gl-icon name="lock" class="gl-vertical-align-bottom" /> {{ __('This job is archived. Only the complete pipeline can be retried.') }} </div> <!-- job log --> <div v-if="hasTrace" - class="build-trace-container position-relative" + class="build-trace-container gl-relative" :class="{ 'gl-mt-3': !job.archived }" > <log-top-bar diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue index 00a570fe2f8..c08ac0317b8 100644 --- a/app/assets/javascripts/jobs/components/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job_container_item.vue @@ -35,33 +35,40 @@ export default { return text; }, + jobName() { + return this.job.name ? this.job.name : this.job.id; + }, + classes() { + return { + retried: this.job.retried, + 'gl-font-weight-bold': this.isActive, + }; + }, + dataTestId() { + return this.isActive ? 'active-job' : null; + }, }, }; </script> <template> - <div - class="build-job position-relative" - :class="{ - retried: job.retried, - active: isActive, - }" - > + <div class="build-job gl-relative" :class="classes"> <gl-link v-gl-tooltip:tooltip-container.left :href="job.status.details_path" :title="tooltipText" - class="js-job-link gl-display-flex gl-align-items-center" + class="gl-display-flex gl-align-items-center" + :data-testid="dataTestId" > <gl-icon v-if="isActive" name="arrow-right" - class="js-arrow-right icon-arrow-right position-absolute d-block" + class="icon-arrow-right gl-absolute gl-display-block" /> <ci-icon :status="job.status" /> - <span class="text-truncate w-100">{{ job.name ? job.name : job.id }}</span> + <span class="gl-text-truncate gl-w-full">{{ jobName }}</span> <gl-icon v-if="job.retried" name="retry" /> </gl-link> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index ea50a11bed6..957e8243f33 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -69,7 +69,10 @@ export default { <template> <div class="top-bar"> <!-- truncate information --> - <div class="truncated-info d-none d-sm-block float-left" data-testid="log-truncated-info"> + <div + class="truncated-info gl-display-none gl-sm-display-block gl-float-left" + data-testid="log-truncated-info" + > <template v-if="isTraceSizeVisible"> {{ jobLogSize }} <gl-link @@ -83,7 +86,7 @@ export default { </div> <!-- eo truncate information --> - <div class="controllers float-right"> + <div class="controllers gl-float-right"> <!-- links --> <gl-button v-if="rawPath" diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue new file mode 100644 index 00000000000..376482b0319 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue @@ -0,0 +1,14 @@ +<script> +export default { + props: { + job: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <div></div> +</template> diff --git a/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue b/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue new file mode 100644 index 00000000000..ba5732d3d43 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue @@ -0,0 +1,49 @@ +<script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; + +export default { + iconSize: 12, + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + GlIcon, + }, + mixins: [timeagoMixin], + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { + finishedTime() { + return this.job?.finishedAt; + }, + duration() { + return this.job?.duration; + }, + }, +}; +</script> + +<template> + <div> + <div v-if="duration" data-testid="job-duration"> + <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" /> + {{ durationTimeFormatted(duration) }} + </div> + <div v-if="finishedTime" data-testid="job-finished-time"> + <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" /> + <time + v-gl-tooltip + :title="tooltipTitle(finishedTime)" + data-placement="top" + data-container="body" + > + {{ timeFormatted(finishedTime) }} + </time> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue new file mode 100644 index 00000000000..88a9f73258f --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/cells/job_cell.vue @@ -0,0 +1,163 @@ +<script> +import { GlBadge, GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { s__ } from '~/locale'; +import { SUCCESS_STATUS } from '../../../constants'; + +export default { + iconSize: 12, + badgeSize: 'sm', + i18n: { + stuckText: s__('Jobs|Job is stuck. Check runners.'), + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + GlBadge, + GlIcon, + GlLink, + }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { + jobId() { + const id = getIdFromGraphQLId(this.job.id); + return `#${id}`; + }, + jobPath() { + return this.job.detailedStatus?.detailsPath; + }, + jobRef() { + return this.job?.refName; + }, + jobRefPath() { + return this.job?.refPath; + }, + jobTags() { + return this.job.tags; + }, + createdByTag() { + return this.job.createdByTag; + }, + triggered() { + return this.job.triggered; + }, + isManualJob() { + return this.job.manualJob; + }, + successfulJob() { + return this.job.status === SUCCESS_STATUS; + }, + showAllowedToFailBadge() { + return this.job.allowFailure && !this.successfulJob; + }, + isScheduledJob() { + return Boolean(this.job.scheduledAt); + }, + canReadJob() { + return this.job?.userPermissions?.readBuild; + }, + jobStuck() { + return this.job?.stuck; + }, + }, +}; +</script> + +<template> + <div> + <div class="gl-text-truncate"> + <gl-link + v-if="canReadJob" + class="gl-text-gray-500!" + :href="jobPath" + data-testid="job-id-link" + > + {{ jobId }} + </gl-link> + + <span v-else data-testid="job-id-limited-access">{{ jobId }}</span> + + <gl-icon + v-if="jobStuck" + v-gl-tooltip="$options.i18n.stuckText" + name="warning" + :size="$options.iconSize" + data-testid="stuck-icon" + /> + + <div + class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end" + > + <div v-if="jobRef" class="gl-max-w-15 gl-text-truncate"> + <gl-icon + v-if="createdByTag" + name="label" + :size="$options.iconSize" + data-testid="label-icon" + /> + <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" /> + <gl-link + class="gl-font-weight-bold gl-text-gray-500!" + :href="job.refPath" + data-testid="job-ref" + >{{ job.refName }}</gl-link + > + </div> + + <span v-else>{{ __('none') }}</span> + + <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" /> + + <gl-link :href="job.commitPath" data-testid="job-sha">{{ job.shortSha }}</gl-link> + </div> + </div> + + <div> + <gl-badge + v-for="tag in jobTags" + :key="tag" + variant="info" + :size="$options.badgeSize" + data-testid="job-tag-badge" + > + {{ tag }} + </gl-badge> + + <gl-badge + v-if="triggered" + variant="info" + :size="$options.badgeSize" + data-testid="triggered-job-badge" + >{{ s__('Job|triggered') }} + </gl-badge> + <gl-badge + v-if="showAllowedToFailBadge" + variant="warning" + :size="$options.badgeSize" + data-testid="fail-job-badge" + >{{ s__('Job|allowed to fail') }} + </gl-badge> + <gl-badge + v-if="isScheduledJob" + variant="info" + :size="$options.badgeSize" + data-testid="delayed-job-badge" + >{{ s__('Job|delayed') }} + </gl-badge> + <gl-badge + v-if="isManualJob" + variant="info" + :size="$options.badgeSize" + data-testid="manual-job-badge" + > + {{ s__('Job|manual') }} + </gl-badge> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue new file mode 100644 index 00000000000..71f9397f5f5 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue @@ -0,0 +1,50 @@ +<script> +import { GlAvatar, GlLink } from '@gitlab/ui'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +export default { + components: { + GlAvatar, + GlLink, + }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { + pipelineId() { + const id = getIdFromGraphQLId(this.job.pipeline.id); + return `#${id}`; + }, + pipelinePath() { + return this.job.pipeline?.path; + }, + pipelineUserAvatar() { + return this.job.pipeline?.user?.avatarUrl; + }, + userPath() { + return this.job.pipeline?.user?.webPath; + }, + showAvatar() { + return this.job.pipeline?.user; + }, + }, +}; +</script> + +<template> + <div class="gl-text-truncate"> + <gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id"> + {{ pipelineId }} + </gl-link> + <div> + <span>{{ __('created by') }}</span> + <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link"> + <gl-avatar :src="pipelineUserAvatar" :size="16" /> + </gl-link> + <span v-else>{{ __('API') }}</span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql index d9e51b0345a..c2104754bad 100644 --- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql +++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql @@ -8,7 +8,20 @@ query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) { startCursor } nodes { + artifacts { + nodes { + downloadPath + } + } + allowFailure + status + scheduledAt + manualJob + triggered + createdByTag detailedStatus { + detailsPath + group icon label text @@ -46,6 +59,10 @@ query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) { playable cancelable active + stuck + userPermissions { + readBuild + } } } } diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/jobs/components/table/index.js index b6b3bb6d379..05d6ebfd6d6 100644 --- a/app/assets/javascripts/jobs/components/table/index.js +++ b/app/assets/javascripts/jobs/components/table/index.js @@ -16,13 +16,21 @@ export default (containerId = 'js-jobs-table') => { return false; } - const { fullPath, jobCounts, jobStatuses } = containerEl.dataset; + const { + fullPath, + jobCounts, + jobStatuses, + pipelineEditorPath, + emptyStateSvgPath, + } = containerEl.dataset; return new Vue({ el: containerEl, apolloProvider, provide: { + emptyStateSvgPath, fullPath, + pipelineEditorPath, jobStatuses: JSON.parse(jobStatuses), jobCounts: JSON.parse(jobCounts), }, diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue index 32b26d45dfe..4fe5bbf79cd 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue @@ -1,57 +1,81 @@ <script> import { GlTable } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { s__, __ } from '~/locale'; +import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; +import ActionsCell from './cells/actions_cell.vue'; +import DurationCell from './cells/duration_cell.vue'; +import JobCell from './cells/job_cell.vue'; +import PipelineCell from './cells/pipeline_cell.vue'; const defaultTableClasses = { tdClass: 'gl-p-5!', thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!', }; +// eslint-disable-next-line @gitlab/require-i18n-strings +const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`; export default { + i18n: { + emptyText: s__('Jobs|No jobs to show'), + }, fields: [ { key: 'status', label: __('Status'), ...defaultTableClasses, + columnClass: 'gl-w-10p', }, { key: 'job', label: __('Job'), ...defaultTableClasses, + columnClass: 'gl-w-20p', }, { key: 'pipeline', label: __('Pipeline'), ...defaultTableClasses, + columnClass: 'gl-w-10p', }, { key: 'stage', label: __('Stage'), ...defaultTableClasses, + columnClass: 'gl-w-10p', }, { key: 'name', label: __('Name'), ...defaultTableClasses, + columnClass: 'gl-w-15p', }, { key: 'duration', label: __('Duration'), ...defaultTableClasses, + columnClass: 'gl-w-15p', }, { key: 'coverage', label: __('Coverage'), - ...defaultTableClasses, + tdClass: coverageTdClasses, + thClass: defaultTableClasses.thClass, + columnClass: 'gl-w-10p', }, { key: 'actions', label: '', ...defaultTableClasses, + columnClass: 'gl-w-10p', }, ], components: { + ActionsCell, + CiBadge, + DurationCell, GlTable, + JobCell, + PipelineCell, }, props: { jobs: { @@ -59,9 +83,64 @@ export default { required: true, }, }, + methods: { + formatCoverage(coverage) { + return coverage ? `${coverage}%` : ''; + }, + }, }; </script> <template> - <gl-table :items="jobs" :fields="$options.fields" /> + <gl-table + :items="jobs" + :fields="$options.fields" + :tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }" + :empty-text="$options.i18n.emptyText" + show-empty + stacked="lg" + fixed + > + <template #table-colgroup="{ fields }"> + <col v-for="field in fields" :key="field.key" :class="field.columnClass" /> + </template> + + <template #cell(status)="{ item }"> + <ci-badge :status="item.detailedStatus" /> + </template> + + <template #cell(job)="{ item }"> + <job-cell :job="item" /> + </template> + + <template #cell(pipeline)="{ item }"> + <pipeline-cell :job="item" /> + </template> + + <template #cell(stage)="{ item }"> + <div class="gl-text-truncate"> + <span data-testid="job-stage-name">{{ item.stage.name }}</span> + </div> + </template> + + <template #cell(name)="{ item }"> + <div class="gl-text-truncate"> + <span data-testid="job-name">{{ item.name }}</span> + </div> + </template> + + <template #cell(duration)="{ item }"> + <duration-cell :job="item" /> + </template> + + <template #cell(coverage)="{ item }"> + <span v-if="item.coverage" data-testid="job-coverage">{{ + formatCoverage(item.coverage) + }}</span> + </template> + + <template #cell(actions)="{ item }"> + <actions-cell :job="item" /> + </template> + </gl-table> </template> diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue index 55954e31654..cf7970f41b1 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue @@ -3,6 +3,7 @@ import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; import { __ } from '~/locale'; import GetJobs from './graphql/queries/get_jobs.query.graphql'; import JobsTable from './jobs_table.vue'; +import JobsTableEmptyState from './jobs_table_empty_state.vue'; import JobsTableTabs from './jobs_table_tabs.vue'; export default { @@ -13,6 +14,7 @@ export default { GlAlert, GlSkeletonLoader, JobsTable, + JobsTableEmptyState, JobsTableTabs, }, inject: { @@ -29,7 +31,7 @@ export default { }; }, update({ project }) { - return project?.jobs; + return project?.jobs?.nodes || []; }, error() { this.hasError = true; @@ -41,15 +43,21 @@ export default { jobs: null, hasError: false, isAlertDismissed: false, + scope: null, }; }, computed: { shouldShowAlert() { return this.hasError && !this.isAlertDismissed; }, + showEmptyState() { + return this.jobs.length === 0 && !this.scope; + }, }, methods: { fetchJobsByStatus(scope) { + this.scope = scope; + this.$apollo.queries.jobs.refetch({ statuses: scope }); }, }, @@ -80,6 +88,8 @@ export default { /> </div> - <jobs-table v-else :jobs="jobs.nodes" /> + <jobs-table-empty-state v-else-if="showEmptyState" /> + + <jobs-table v-else :jobs="jobs" /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue b/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue new file mode 100644 index 00000000000..fcdd52b719c --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue @@ -0,0 +1,35 @@ +<script> +import { GlEmptyState } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + i18n: { + title: s__('Jobs|Use jobs to automate your tasks'), + description: s__( + 'Jobs|Jobs are the building blocks of a GitLab CI/CD pipeline. Each job has a specific task, like testing code. To set up jobs in a CI/CD pipeline, add a CI/CD configuration file to your project.', + ), + buttonText: s__('Jobs|Create CI/CD configuration file'), + }, + components: { + GlEmptyState, + }, + inject: { + pipelineEditorPath: { + default: '', + }, + emptyStateSvgPath: { + default: '', + }, + }, +}; +</script> + +<template> + <gl-empty-state + :title="$options.i18n.title" + :description="$options.i18n.description" + :svg-path="emptyStateSvgPath" + :primary-button-link="pipelineEditorPath" + :primary-button-text="$options.i18n.buttonText" + /> +</template> diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue index 95d265fce60..26791e4284d 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue @@ -50,7 +50,7 @@ export default { </script> <template> - <gl-tabs> + <gl-tabs content-class="gl-pb-0"> <gl-tab v-for="tab in tabs" :key="tab.text" diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js index d0d625d794d..3040d4e2379 100644 --- a/app/assets/javascripts/jobs/constants.js +++ b/app/assets/javascripts/jobs/constants.js @@ -22,3 +22,5 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = { primaryText: __('Retry job'), title: s__('Jobs|Are you sure you want to retry this job?'), }; + +export const SUCCESS_STATUS = 'SUCCESS'; diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 3e00056ee81..260190f5043 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -13,6 +13,7 @@ export default () => { const { artifactHelpUrl, deploymentHelpUrl, + codeQualityHelpUrl, runnerSettingsUrl, variablesSettingsUrl, subscriptionsMoreMinutesUrl, @@ -38,6 +39,7 @@ export default () => { props: { artifactHelpUrl, deploymentHelpUrl, + codeQualityHelpUrl, runnerSettingsUrl, variablesSettingsUrl, subscriptionsMoreMinutesUrl, |