diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
commit | aee0a117a889461ce8ced6fcf73207fe017f1d99 (patch) | |
tree | 891d9ef189227a8445d83f35c1b0fc99573f4380 /app/assets/javascripts/jobs | |
parent | 8d46af3258650d305f53b819eabf7ab18d22f59e (diff) |
Add latest changes from gitlab-org/gitlab@14-6-stable-eev14.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/jobs')
13 files changed, 307 insertions, 74 deletions
diff --git a/app/assets/javascripts/jobs/bridge/app.vue b/app/assets/javascripts/jobs/bridge/app.vue new file mode 100644 index 00000000000..67c22712776 --- /dev/null +++ b/app/assets/javascripts/jobs/bridge/app.vue @@ -0,0 +1,20 @@ +<script> +import BridgeEmptyState from './components/empty_state.vue'; +import BridgeSidebar from './components/sidebar.vue'; + +export default { + name: 'BridgePageApp', + components: { + BridgeEmptyState, + BridgeSidebar, + }, +}; +</script> +<template> + <div> + <!-- TODO: get job details and show CI header --> + <!-- TODO: add downstream pipeline path --> + <bridge-empty-state downstream-pipeline-path="#" /> + <bridge-sidebar /> + </div> +</template> diff --git a/app/assets/javascripts/jobs/bridge/components/constants.js b/app/assets/javascripts/jobs/bridge/components/constants.js new file mode 100644 index 00000000000..33310b3157a --- /dev/null +++ b/app/assets/javascripts/jobs/bridge/components/constants.js @@ -0,0 +1 @@ +export const SIDEBAR_COLLAPSE_BREAKPOINTS = ['xs', 'sm']; diff --git a/app/assets/javascripts/jobs/bridge/components/empty_state.vue b/app/assets/javascripts/jobs/bridge/components/empty_state.vue new file mode 100644 index 00000000000..bd07d863719 --- /dev/null +++ b/app/assets/javascripts/jobs/bridge/components/empty_state.vue @@ -0,0 +1,45 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + name: 'BridgeEmptyState', + i18n: { + title: __('This job triggers a downstream pipeline'), + linkBtnText: __('View downstream pipeline'), + }, + components: { + GlButton, + }, + inject: { + emptyStateIllustrationPath: { + type: String, + require: true, + }, + }, + props: { + downstreamPipelinePath: { + type: String, + required: false, + default: undefined, + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11"> + <img :src="emptyStateIllustrationPath" /> + <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1> + <gl-button + v-if="downstreamPipelinePath" + class="gl-mt-3" + category="secondary" + variant="confirm" + size="medium" + :href="downstreamPipelinePath" + > + {{ $options.i18n.linkBtnText }} + </gl-button> + </div> +</template> diff --git a/app/assets/javascripts/jobs/bridge/components/sidebar.vue b/app/assets/javascripts/jobs/bridge/components/sidebar.vue new file mode 100644 index 00000000000..68b767408f0 --- /dev/null +++ b/app/assets/javascripts/jobs/bridge/components/sidebar.vue @@ -0,0 +1,98 @@ +<script> +import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; +import { __ } from '~/locale'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; +import { JOB_SIDEBAR } from '../../constants'; +import { SIDEBAR_COLLAPSE_BREAKPOINTS } from './constants'; + +export default { + styles: { + top: '75px', + width: '290px', + }, + name: 'BridgeSidebar', + i18n: { + ...JOB_SIDEBAR, + retryButton: __('Retry'), + retryTriggerJob: __('Retry the trigger job'), + retryDownstreamPipeline: __('Retry the downstream pipeline'), + }, + borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'], + components: { + GlButton, + GlDropdown, + GlDropdownItem, + TooltipOnTruncate, + }, + inject: { + buildName: { + type: String, + default: '', + }, + }, + data() { + return { + isSidebarExpanded: true, + }; + }, + created() { + window.addEventListener('resize', this.onResize); + }, + mounted() { + this.onResize(); + }, + methods: { + toggleSidebar() { + this.isSidebarExpanded = !this.isSidebarExpanded; + }, + onResize() { + const breakpoint = bp.getBreakpointSize(); + if (SIDEBAR_COLLAPSE_BREAKPOINTS.includes(breakpoint)) { + this.isSidebarExpanded = false; + } else if (!this.isSidebarExpanded) { + this.isSidebarExpanded = true; + } + }, + }, +}; +</script> +<template> + <aside + class="gl-fixed gl-right-0 gl-px-5 gl-bg-gray-10 gl-h-full gl-border-l-solid gl-border-1 gl-border-gray-100 gl-z-index-200 gl-overflow-hidden" + :style="this.$options.styles" + :class="{ + 'gl-display-none': !isSidebarExpanded, + }" + > + <div class="gl-py-5 gl-display-flex gl-align-items-center"> + <tooltip-on-truncate :title="buildName" truncate-target="child" + ><h4 class="gl-mb-0 gl-mr-2 gl-text-truncate"> + {{ buildName }} + </h4> + </tooltip-on-truncate> + <!-- TODO: implement retry actions --> + <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right"> + <gl-dropdown + :text="$options.i18n.retryButton" + category="primary" + variant="confirm" + right + size="medium" + > + <gl-dropdown-item>{{ $options.i18n.retryTriggerJob }}</gl-dropdown-item> + <gl-dropdown-item>{{ $options.i18n.retryDownstreamPipeline }}</gl-dropdown-item> + </gl-dropdown> + </div> + <gl-button + :aria-label="$options.i18n.toggleSidebar" + data-testid="sidebar-expansion-toggle" + category="tertiary" + class="gl-md-display-none gl-ml-2" + icon="chevron-double-lg-right" + @click="toggleSidebar" + /> + </div> + <!-- TODO: get job details and show commit block, stage dropdown, jobs list --> + </aside> +</template> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index 6105299e15c..97141a27a5e 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -5,7 +5,7 @@ import { __, s__, sprintf } from '~/locale'; export default { i18n: { - eraseLogButtonLabel: s__('Job|Erase job log'), + eraseLogButtonLabel: s__('Job|Erase job log and artifacts'), scrollToBottomButtonLabel: s__('Job|Scroll to bottom'), scrollToTopButtonLabel: s__('Job|Scroll to top'), showRawButtonLabel: s__('Job|Show complete raw'), diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 1b50006239c..9aa1503c7c3 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -2,7 +2,7 @@ import { GlButton, GlIcon } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import { JOB_SIDEBAR } from '../constants'; import ArtifactsBlock from './artifacts_block.vue'; import CommitBlock from './commit_block.vue'; diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue index d90377029c5..5451cd21c14 100644 --- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue +++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue @@ -20,6 +20,9 @@ export default { duration() { return timeIntervalInWords(this.job.duration); }, + durationTitle() { + return this.job.finished_at ? __('Duration') : __('Elapsed time'); + }, erasedAt() { return this.timeFormatted(this.job.erased_at); }, @@ -76,7 +79,7 @@ export default { <template> <div v-if="shouldRenderBlock"> - <detail-row v-if="job.duration" :value="duration" title="Duration" /> + <detail-row v-if="job.duration" :value="duration" :title="durationTitle" /> <detail-row v-if="job.finished_at" :value="finishedAt" diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue index 51251c0cacc..7dfa963a857 100644 --- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue +++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue @@ -12,6 +12,7 @@ import { JOB_SCHEDULED, PLAY_JOB_CONFIRMATION_MESSAGE, RUN_JOB_NOW_HEADER_TITLE, + FILE_TYPE_ARCHIVE, } from '../constants'; import eventHub from '../event_hub'; import cancelJobMutation from '../graphql/mutations/job_cancel.mutation.graphql'; @@ -58,12 +59,21 @@ export default { }, }, computed: { + hasArtifacts() { + return this.job.artifacts.nodes.find((artifact) => artifact.fileType === FILE_TYPE_ARCHIVE); + }, artifactDownloadPath() { - return this.job.artifacts?.nodes[0]?.downloadPath; + return this.hasArtifacts.downloadPath; }, canReadJob() { return this.job.userPermissions?.readBuild; }, + canUpdateJob() { + return this.job.userPermissions?.updateBuild; + }, + canReadArtifacts() { + return this.job.userPermissions?.readJobArtifacts; + }, isActive() { return this.job.active; }, @@ -86,7 +96,7 @@ export default { return this.job.detailedStatus?.action?.method; }, shouldDisplayArtifacts() { - return this.job.userPermissions?.readJobArtifacts && this.job.artifacts?.nodes.length > 0; + return this.canReadArtifacts && this.hasArtifacts; }, }, methods: { @@ -139,7 +149,7 @@ export default { <template> <gl-button-group> - <template v-if="canReadJob"> + <template v-if="canReadJob && canUpdateJob"> <gl-button v-if="isActive" data-testid="cancel-button" diff --git a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue index 71f9397f5f5..1a6d1a341b0 100644 --- a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue +++ b/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue @@ -35,10 +35,12 @@ export default { </script> <template> - <div class="gl-text-truncate"> - <gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id"> - {{ pipelineId }} - </gl-link> + <div> + <div class="gl-text-truncate"> + <gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id"> + {{ pipelineId }} + </gl-link> + </div> <div> <span>{{ __('created by') }}</span> <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link"> diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/jobs/components/table/constants.js index e5d1bc01cbf..962979ba573 100644 --- a/app/assets/javascripts/jobs/components/table/constants.js +++ b/app/assets/javascripts/jobs/components/table/constants.js @@ -1,4 +1,5 @@ import { s__, __ } from '~/locale'; +import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants'; export const GRAPHQL_PAGE_SIZE = 30; @@ -17,6 +18,9 @@ export const DEFAULT = 'default'; /* Job Status Constants */ export const JOB_SCHEDULED = 'SCHEDULED'; +/* Artifact file types */ +export const FILE_TYPE_ARCHIVE = 'ARCHIVE'; + /* i18n */ export const ACTIONS_DOWNLOAD_ARTIFACTS = __('Download artifacts'); export const ACTIONS_START_NOW = s__('DelayedJobs|Start now'); @@ -30,3 +34,66 @@ export const PLAY_JOB_CONFIRMATION_MESSAGE = s__( `DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after its timer finishes.`, ); export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?'); + +/* Table constants */ + +const defaultTableClasses = { + tdClass: 'gl-p-5!', + thClass: DEFAULT_TH_CLASSES, +}; +// eslint-disable-next-line @gitlab/require-i18n-strings +const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`; + +export const DEFAULT_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'), + tdClass: coverageTdClasses, + thClass: defaultTableClasses.thClass, + columnClass: 'gl-w-10p', + }, + { + key: 'actions', + label: '', + ...defaultTableClasses, + columnClass: 'gl-w-10p', + }, +]; + +export const JOBS_TAB_FIELDS = DEFAULT_FIELDS.filter((field) => field.key !== 'pipeline'); 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 c8763d4767e..88937185a8c 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 @@ -7,6 +7,7 @@ query getJobs( $statuses: [CiJobStatus!] ) { project(fullPath: $fullPath) { + id jobs(after: $after, before: $before, first: $first, last: $last, statuses: $statuses) { pageInfo { endCursor @@ -18,6 +19,7 @@ query getJobs( artifacts { nodes { downloadPath + fileType } } allowFailure @@ -27,6 +29,7 @@ query getJobs( triggered createdByTag detailedStatus { + id detailsPath group icon @@ -34,6 +37,7 @@ query getJobs( text tooltip action { + id buttonTitle icon method @@ -51,11 +55,13 @@ query getJobs( id path user { + id webPath avatarUrl } } stage { + id name } name @@ -70,6 +76,7 @@ query getJobs( userPermissions { readBuild readJobArtifacts + updateBuild } } } diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue index 298c99c4162..f513d2090fa 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue @@ -1,75 +1,17 @@ <script> import { GlTable } from '@gitlab/ui'; -import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants'; -import { s__, __ } 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: DEFAULT_TH_CLASSES, -}; -// eslint-disable-next-line @gitlab/require-i18n-strings -const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`; +import { DEFAULT_FIELDS } from './constants'; 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'), - tdClass: coverageTdClasses, - thClass: defaultTableClasses.thClass, - columnClass: 'gl-w-10p', - }, - { - key: 'actions', - label: '', - ...defaultTableClasses, - columnClass: 'gl-w-10p', - }, - ], components: { ActionsCell, CiBadge, @@ -83,6 +25,11 @@ export default { type: Array, required: true, }, + tableFields: { + type: Array, + required: false, + default: () => DEFAULT_FIELDS, + }, }, methods: { formatCoverage(coverage) { @@ -95,7 +42,7 @@ export default { <template> <gl-table :items="jobs" - :fields="$options.fields" + :fields="tableFields" :tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }" :empty-text="$options.i18n.emptyText" show-empty diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 1fb6a6f9850..e078a6c2319 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -1,10 +1,11 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import BridgeApp from './bridge/app.vue'; import JobApp from './components/job_app.vue'; import createStore from './store'; -export default () => { - const element = document.getElementById('js-job-vue-app'); - +const initializeJobPage = (element) => { const store = createStore(); // Let's start initializing the store (i.e. fetching data) right away @@ -51,3 +52,35 @@ export default () => { }, }); }; + +const initializeBridgePage = (el) => { + const { buildName, emptyStateIllustrationPath } = el.dataset; + + Vue.use(VueApollo); + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + provide: { + buildName, + emptyStateIllustrationPath, + }, + render(h) { + return h(BridgeApp); + }, + }); +}; + +export default () => { + const jobElement = document.getElementById('js-job-page'); + const bridgeElement = document.getElementById('js-bridge-page'); + + if (jobElement) { + initializeJobPage(jobElement); + } else { + initializeBridgePage(bridgeElement); + } +}; |