diff options
Diffstat (limited to 'app/assets/javascripts/jobs/components/table')
12 files changed, 268 insertions, 2 deletions
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 376482b0319..6b3a4424a5b 100644 --- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue +++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue @@ -1,14 +1,195 @@ <script> +import { GlButton, GlButtonGroup, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui'; +import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; +import { + ACTIONS_DOWNLOAD_ARTIFACTS, + ACTIONS_START_NOW, + ACTIONS_UNSCHEDULE, + ACTIONS_PLAY, + ACTIONS_RETRY, + CANCEL, + GENERIC_ERROR, + JOB_SCHEDULED, + PLAY_JOB_CONFIRMATION_MESSAGE, + RUN_JOB_NOW_HEADER_TITLE, +} from '../constants'; +import eventHub from '../event_hub'; +import cancelJobMutation from '../graphql/mutations/job_cancel.mutation.graphql'; +import playJobMutation from '../graphql/mutations/job_play.mutation.graphql'; +import retryJobMutation from '../graphql/mutations/job_retry.mutation.graphql'; +import unscheduleJobMutation from '../graphql/mutations/job_unschedule.mutation.graphql'; + export default { + ACTIONS_DOWNLOAD_ARTIFACTS, + ACTIONS_START_NOW, + ACTIONS_UNSCHEDULE, + ACTIONS_PLAY, + ACTIONS_RETRY, + CANCEL, + GENERIC_ERROR, + PLAY_JOB_CONFIRMATION_MESSAGE, + RUN_JOB_NOW_HEADER_TITLE, + jobRetry: 'jobRetry', + jobCancel: 'jobCancel', + jobPlay: 'jobPlay', + jobUnschedule: 'jobUnschedule', + playJobModalId: 'play-job-modal', + components: { + GlButton, + GlButtonGroup, + GlCountdown, + GlModal, + GlSprintf, + }, + directives: { + GlModalDirective, + }, + inject: { + admin: { + default: false, + }, + }, props: { job: { type: Object, required: true, }, }, + computed: { + artifactDownloadPath() { + return this.job.artifacts?.nodes[0]?.downloadPath; + }, + canReadJob() { + return this.job.userPermissions?.readBuild; + }, + isActive() { + return this.job.active; + }, + manualJobPlayable() { + return this.job.playable && !this.admin && this.job.manualJob; + }, + isRetryable() { + return this.job.retryable; + }, + isScheduled() { + return this.job.status === JOB_SCHEDULED; + }, + scheduledAt() { + return this.job.scheduledAt; + }, + currentJobActionPath() { + return this.job.detailedStatus?.action?.path; + }, + currentJobMethod() { + return this.job.detailedStatus?.action?.method; + }, + shouldDisplayArtifacts() { + return this.job.userPermissions?.readJobArtifacts && this.job.artifacts?.nodes.length > 0; + }, + }, + methods: { + async postJobAction(name, mutation) { + try { + const { + data: { + [name]: { errors }, + }, + } = await this.$apollo.mutate({ + mutation, + variables: { id: this.job.id }, + }); + if (errors.length > 0) { + this.reportFailure(); + } else { + eventHub.$emit('jobActionPerformed'); + } + } catch { + this.reportFailure(); + } + }, + reportFailure() { + const toastProps = { + text: this.$options.GENERIC_ERROR, + variant: 'danger', + }; + + this.$toast.show(toastProps.text, { + variant: toastProps.variant, + }); + }, + cancelJob() { + this.postJobAction(this.$options.jobCancel, cancelJobMutation); + }, + retryJob() { + this.postJobAction(this.$options.jobRetry, retryJobMutation); + }, + playJob() { + this.postJobAction(this.$options.jobPlay, playJobMutation); + }, + unscheduleJob() { + this.postJobAction(this.$options.jobUnschedule, unscheduleJobMutation); + }, + }, }; </script> <template> - <div></div> + <gl-button-group> + <template v-if="canReadJob"> + <gl-button v-if="isActive" icon="cancel" :title="$options.CANCEL" @click="cancelJob()" /> + <template v-else-if="isScheduled"> + <gl-button icon="planning" disabled data-testid="countdown"> + <gl-countdown :end-date-string="scheduledAt" /> + </gl-button> + <gl-button + v-gl-modal-directive="$options.playJobModalId" + icon="play" + :title="$options.ACTIONS_START_NOW" + data-testid="play-scheduled" + /> + <gl-modal + :modal-id="$options.playJobModalId" + :title="$options.RUN_JOB_NOW_HEADER_TITLE" + @primary="playJob()" + > + <gl-sprintf :message="$options.PLAY_JOB_CONFIRMATION_MESSAGE"> + <template #job_name>{{ job.name }}</template> + </gl-sprintf> + </gl-modal> + <gl-button + icon="time-out" + :title="$options.ACTIONS_UNSCHEDULE" + data-testid="unschedule" + @click="unscheduleJob()" + /> + </template> + <template v-else> + <!--Note: This is the manual job play button --> + <gl-button + v-if="manualJobPlayable" + icon="play" + :title="$options.ACTIONS_PLAY" + data-testid="play" + @click="playJob()" + /> + <gl-button + v-else-if="isRetryable" + icon="repeat" + :title="$options.ACTIONS_RETRY" + :method="currentJobMethod" + data-testid="retry" + @click="retryJob()" + /> + </template> + </template> + <gl-button + v-if="shouldDisplayArtifacts" + icon="download" + :title="$options.ACTIONS_DOWNLOAD_ARTIFACTS" + :href="artifactDownloadPath" + rel="nofollow" + download + data-testid="download-artifacts" + /> + </gl-button-group> </template> diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/jobs/components/table/constants.js index 7e973a34e5c..e5d1bc01cbf 100644 --- a/app/assets/javascripts/jobs/components/table/constants.js +++ b/app/assets/javascripts/jobs/components/table/constants.js @@ -1,3 +1,5 @@ +import { s__, __ } from '~/locale'; + export const GRAPHQL_PAGE_SIZE = 30; export const initialPaginationState = { @@ -7,3 +9,24 @@ export const initialPaginationState = { first: GRAPHQL_PAGE_SIZE, last: null, }; + +/* Error constants */ +export const POST_FAILURE = 'post_failure'; +export const DEFAULT = 'default'; + +/* Job Status Constants */ +export const JOB_SCHEDULED = 'SCHEDULED'; + +/* i18n */ +export const ACTIONS_DOWNLOAD_ARTIFACTS = __('Download artifacts'); +export const ACTIONS_START_NOW = s__('DelayedJobs|Start now'); +export const ACTIONS_UNSCHEDULE = s__('DelayedJobs|Unschedule'); +export const ACTIONS_PLAY = __('Play'); +export const ACTIONS_RETRY = __('Retry'); + +export const CANCEL = __('Cancel'); +export const GENERIC_ERROR = __('An error occurred while making the request.'); +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?'); diff --git a/app/assets/javascripts/jobs/components/table/event_hub.js b/app/assets/javascripts/jobs/components/table/event_hub.js new file mode 100644 index 00000000000..e31806ad199 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/event_hub.js @@ -0,0 +1,3 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); diff --git a/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql b/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql new file mode 100644 index 00000000000..06b065a86ce --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql @@ -0,0 +1,3 @@ +fragment Job on CiJob { + id +} diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql new file mode 100644 index 00000000000..20935514d51 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql @@ -0,0 +1,10 @@ +#import "../fragments/job.fragment.graphql" + +mutation cancelJob($id: CiBuildID!) { + jobCancel(input: { id: $id }) { + job { + ...Job + } + errors + } +} diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql new file mode 100644 index 00000000000..c94b045ac40 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql @@ -0,0 +1,10 @@ +#import "../fragments/job.fragment.graphql" + +mutation playJob($id: CiBuildID!) { + jobPlay(input: { id: $id }) { + job { + ...Job + } + errors + } +} diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql new file mode 100644 index 00000000000..6e51f9a20fa --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql @@ -0,0 +1,10 @@ +#import "../fragments/job.fragment.graphql" + +mutation retryJob($id: CiBuildID!) { + jobRetry(input: { id: $id }) { + job { + ...Job + } + errors + } +} diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql new file mode 100644 index 00000000000..8be8c42f3c3 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql @@ -0,0 +1,10 @@ +#import "../fragments/job.fragment.graphql" + +mutation unscheduleJob($id: CiBuildID!) { + jobUnschedule(input: { id: $id }) { + job { + ...Job + } + errors + } +} 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 68c6584cda6..c8763d4767e 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 @@ -69,6 +69,7 @@ query getJobs( stuck userPermissions { readBuild + readJobArtifacts } } } diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/jobs/components/table/index.js index 05d6ebfd6d6..f24daf90815 100644 --- a/app/assets/javascripts/jobs/components/table/index.js +++ b/app/assets/javascripts/jobs/components/table/index.js @@ -1,9 +1,12 @@ +import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; import createDefaultClient from '~/lib/graphql'; +import { parseBoolean } from '~/lib/utils/common_utils'; Vue.use(VueApollo); +Vue.use(GlToast); const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), @@ -22,6 +25,7 @@ export default (containerId = 'js-jobs-table') => { jobStatuses, pipelineEditorPath, emptyStateSvgPath, + admin, } = containerEl.dataset; return new Vue({ @@ -33,6 +37,7 @@ export default (containerId = 'js-jobs-table') => { pipelineEditorPath, jobStatuses: JSON.parse(jobStatuses), jobCounts: JSON.parse(jobCounts), + admin: parseBoolean(admin), }, render(createElement) { return createElement(JobsTableApp); diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue index 076c0e78b11..298c99c4162 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue @@ -141,7 +141,7 @@ export default { </template> <template #cell(actions)="{ item }"> - <actions-cell :job="item" /> + <actions-cell class="gl-float-right" :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 2061b1f1eb2..c786d35ac68 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue @@ -2,6 +2,7 @@ import { GlAlert, GlPagination, GlSkeletonLoader } from '@gitlab/ui'; import { __ } from '~/locale'; import { GRAPHQL_PAGE_SIZE, initialPaginationState } from './constants'; +import eventHub from './event_hub'; import GetJobs from './graphql/queries/get_jobs.query.graphql'; import JobsTable from './jobs_table.vue'; import JobsTableEmptyState from './jobs_table_empty_state.vue'; @@ -74,7 +75,16 @@ export default { return Boolean(this.prevPage || this.nextPage) && !this.$apollo.loading; }, }, + mounted() { + eventHub.$on('jobActionPerformed', this.handleJobAction); + }, + beforeDestroy() { + eventHub.$off('jobActionPerformed', this.handleJobAction); + }, methods: { + handleJobAction() { + this.$apollo.queries.jobs.refetch({ statuses: this.scope }); + }, fetchJobsByStatus(scope) { this.scope = scope; |