diff options
Diffstat (limited to 'app/assets/javascripts/pipeline_schedules')
12 files changed, 552 insertions, 0 deletions
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue new file mode 100644 index 00000000000..4a08a82275a --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue @@ -0,0 +1,134 @@ +<script> +import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui'; +import { s__, __ } from '~/locale'; +import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline_schedule.mutation.graphql'; +import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql'; +import PipelineSchedulesTable from './table/pipeline_schedules_table.vue'; + +export default { + i18n: { + schedulesFetchError: s__('PipelineSchedules|There was a problem fetching pipeline schedules.'), + scheduleDeleteError: s__( + 'PipelineSchedules|There was a problem deleting the pipeline schedule.', + ), + }, + modal: { + id: 'delete-pipeline-schedule-modal', + deleteConfirmation: s__( + 'PipelineSchedules|Are you sure you want to delete this pipeline schedule?', + ), + actionPrimary: { + text: s__('PipelineSchedules|Delete pipeline schedule'), + attributes: [{ variant: 'danger' }], + }, + actionCancel: { + text: __('Cancel'), + attributes: [], + }, + }, + components: { + GlAlert, + GlLoadingIcon, + GlModal, + PipelineSchedulesTable, + }, + inject: { + fullPath: { + default: '', + }, + }, + apollo: { + schedules: { + query: getPipelineSchedulesQuery, + variables() { + return { + projectPath: this.fullPath, + }; + }, + update({ project }) { + return project?.pipelineSchedules?.nodes || []; + }, + error() { + this.reportError(this.$options.i18n.schedulesFetchError); + }, + }, + }, + data() { + return { + schedules: [], + hasError: false, + errorMessage: '', + scheduleToDeleteId: null, + showModal: false, + }; + }, + computed: { + isLoading() { + return this.$apollo.queries.schedules.loading; + }, + }, + methods: { + reportError(error) { + this.hasError = true; + this.errorMessage = error; + }, + showDeleteModal(id) { + this.showModal = true; + this.scheduleToDeleteId = id; + }, + hideModal() { + this.showModal = false; + this.scheduleToDeleteId = null; + }, + async deleteSchedule() { + try { + const { + data: { + pipelineScheduleDelete: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: deletePipelineScheduleMutation, + variables: { id: this.scheduleToDeleteId }, + }); + + if (errors.length > 0) { + throw new Error(); + } else { + this.$apollo.queries.schedules.refetch(); + } + } catch { + this.reportError(this.$options.i18n.scheduleDeleteError); + } + }, + }, +}; +</script> + +<template> + <div> + <gl-alert v-if="hasError" class="gl-mb-2" variant="danger" @dismiss="hasError = false"> + {{ errorMessage }} + </gl-alert> + + <gl-loading-icon v-if="isLoading" size="lg" /> + + <!-- Tabs will be addressed in #371989 --> + + <template v-else> + <pipeline-schedules-table :schedules="schedules" @showDeleteModal="showDeleteModal" /> + + <gl-modal + :visible="showModal" + :title="$options.modal.actionPrimary.text" + :modal-id="$options.modal.id" + :action-primary="$options.modal.actionPrimary" + :action-cancel="$options.modal.actionCancel" + size="sm" + @primary="deleteSchedule" + @hide="hideModal" + > + {{ $options.modal.deleteConfirmation }} + </gl-modal> + </template> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue new file mode 100644 index 00000000000..6e24ac6b8d4 --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue @@ -0,0 +1,18 @@ +<script> +import { GlForm } from '@gitlab/ui'; + +export default { + components: { + GlForm, + }, + inject: { + fullPath: { + default: '', + }, + }, +}; +</script> + +<template> + <gl-form /> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue new file mode 100644 index 00000000000..76d118bf52d --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue @@ -0,0 +1,66 @@ +<script> +import { GlButton, GlButtonGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export const i18n = { + playTooltip: s__('PipelineSchedules|Run pipeline schedule'), + editTooltip: s__('PipelineSchedules|Edit pipeline schedule'), + deleteTooltip: s__('PipelineSchedules|Delete pipeline schedule'), + takeOwnershipTooltip: s__('PipelineSchedules|Take ownership of pipeline schedule'), +}; + +export default { + i18n, + components: { + GlButton, + GlButtonGroup, + }, + directives: { + GlTooltip, + }, + props: { + schedule: { + type: Object, + required: true, + }, + }, + computed: { + canPlay() { + return this.schedule.userPermissions.playPipelineSchedule; + }, + canTakeOwnership() { + return this.schedule.userPermissions.takeOwnershipPipelineSchedule; + }, + canUpdate() { + return this.schedule.userPermissions.updatePipelineSchedule; + }, + canRemove() { + return this.schedule.userPermissions.adminPipelineSchedule; + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-justify-content-end"> + <gl-button-group> + <gl-button v-if="canPlay" v-gl-tooltip :title="$options.i18n.playTooltip" icon="play" /> + <gl-button + v-if="canTakeOwnership" + v-gl-tooltip + :title="$options.i18n.takeOwnershipTooltip" + icon="user" + /> + <gl-button v-if="canUpdate" v-gl-tooltip :title="$options.i18n.editTooltip" icon="pencil" /> + <gl-button + v-if="canRemove" + v-gl-tooltip + :title="$options.i18n.deleteTooltip" + icon="remove" + variant="danger" + data-testid="delete-pipeline-schedule-btn" + @click="$emit('showDeleteModal', schedule.id)" + /> + </gl-button-group> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue new file mode 100644 index 00000000000..216796b357c --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue @@ -0,0 +1,32 @@ +<script> +import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; + +export default { + components: { + CiBadge, + }, + props: { + schedule: { + type: Object, + required: true, + }, + }, + computed: { + hasPipeline() { + return this.schedule.lastPipeline; + }, + lastPipelineStatus() { + return this.schedule?.lastPipeline?.detailedStatus; + }, + }, +}; +</script> + +<template> + <div> + <ci-badge v-if="hasPipeline" :status="lastPipelineStatus" class="gl-vertical-align-middle" /> + <span v-else data-testid="pipeline-schedule-status-text"> + {{ s__('PipelineSchedules|None') }} + </span> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue new file mode 100644 index 00000000000..48d59bf6e7c --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue @@ -0,0 +1,32 @@ +<script> +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + components: { + TimeAgoTooltip, + }, + props: { + schedule: { + type: Object, + required: true, + }, + }, + computed: { + showTimeAgo() { + return this.schedule.active && this.schedule.nextRunAt; + }, + realNextRunTime() { + return this.schedule.realNextRun; + }, + }, +}; +</script> + +<template> + <div> + <time-ago-tooltip v-if="showTimeAgo" :time="realNextRunTime" /> + <span v-else data-testid="pipeline-schedule-inactive"> + {{ s__('PipelineSchedules|Inactive') }} + </span> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue new file mode 100644 index 00000000000..e7fa94eb7fc --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue @@ -0,0 +1,29 @@ +<script> +import { GlAvatar, GlAvatarLink } from '@gitlab/ui'; + +export default { + components: { + GlAvatar, + GlAvatarLink, + }, + props: { + schedule: { + type: Object, + required: true, + }, + }, + computed: { + owner() { + return this.schedule.owner; + }, + }, +}; +</script> + +<template> + <div> + <gl-avatar-link :href="owner.webPath" :title="owner.name" class="gl-ml-3"> + <gl-avatar :size="32" :src="owner.avatarUrl" /> + </gl-avatar-link> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue new file mode 100644 index 00000000000..08efa794bcc --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue @@ -0,0 +1,36 @@ +<script> +import { GlIcon, GlLink } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + GlLink, + }, + props: { + schedule: { + type: Object, + required: true, + }, + }, + computed: { + iconName() { + return this.schedule.forTag ? 'tag' : 'fork'; + }, + refPath() { + return this.schedule.refPath; + }, + refDisplay() { + return this.schedule.refForDisplay; + }, + }, +}; +</script> + +<template> + <div> + <gl-icon :name="iconName" /> + <span v-if="refPath"> + <gl-link :href="refPath" class="gl-text-gray-900">{{ refDisplay }}</gl-link> + </span> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue b/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue new file mode 100644 index 00000000000..d54008b81b2 --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue @@ -0,0 +1,95 @@ +<script> +import { GlTableLite } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import PipelineScheduleActions from './cells/pipeline_schedule_actions.vue'; +import PipelineScheduleLastPipeline from './cells/pipeline_schedule_last_pipeline.vue'; +import PipelineScheduleNextRun from './cells/pipeline_schedule_next_run.vue'; +import PipelineScheduleOwner from './cells/pipeline_schedule_owner.vue'; +import PipelineScheduleTarget from './cells/pipeline_schedule_target.vue'; + +export default { + fields: [ + { + key: 'description', + label: s__('PipelineSchedules|Description'), + columnClass: 'gl-w-40p', + }, + { + key: 'target', + label: s__('PipelineSchedules|Target'), + columnClass: 'gl-w-10p', + }, + { + key: 'pipeline', + label: s__('PipelineSchedules|Last Pipeline'), + columnClass: 'gl-w-10p', + }, + { + key: 'next', + label: s__('PipelineSchedules|Next Run'), + columnClass: 'gl-w-15p', + }, + { + key: 'owner', + label: s__('PipelineSchedules|Owner'), + columnClass: 'gl-w-10p', + }, + { + key: 'actions', + label: '', + columnClass: 'gl-w-15p', + }, + ], + components: { + GlTableLite, + PipelineScheduleActions, + PipelineScheduleLastPipeline, + PipelineScheduleNextRun, + PipelineScheduleOwner, + PipelineScheduleTarget, + }, + props: { + schedules: { + type: Array, + required: true, + }, + }, +}; +</script> + +<template> + <gl-table-lite :fields="$options.fields" :items="schedules" stacked="md"> + <template #table-colgroup="{ fields }"> + <col v-for="field in fields" :key="field.key" :class="field.columnClass" /> + </template> + + <template #cell(description)="{ item }"> + <span data-testid="pipeline-schedule-description"> + {{ item.description }} + </span> + </template> + + <template #cell(target)="{ item }"> + <pipeline-schedule-target :schedule="item" /> + </template> + + <template #cell(pipeline)="{ item }"> + <pipeline-schedule-last-pipeline :schedule="item" /> + </template> + + <template #cell(next)="{ item }"> + <pipeline-schedule-next-run :schedule="item" /> + </template> + + <template #cell(owner)="{ item }"> + <pipeline-schedule-owner :schedule="item" /> + </template> + + <template #cell(actions)="{ item }"> + <pipeline-schedule-actions + :schedule="item" + @showDeleteModal="$emit('showDeleteModal', $event)" + /> + </template> + </gl-table-lite> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql b/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql new file mode 100644 index 00000000000..8aab0b3fbde --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql @@ -0,0 +1,6 @@ +mutation deletePipelineSchedule($id: CiPipelineScheduleID!) { + pipelineScheduleDelete(input: { id: $id }) { + clientMutationId + errors + } +} diff --git a/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql new file mode 100644 index 00000000000..7d9d658b1b6 --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql @@ -0,0 +1,40 @@ +query getPipelineSchedulesQuery($projectPath: ID!) { + project(fullPath: $projectPath) { + id + pipelineSchedules { + nodes { + id + description + forTag + refPath + refForDisplay + lastPipeline { + id + detailedStatus { + id + group + icon + label + text + detailsPath + } + } + active + nextRunAt + realNextRun + owner { + id + avatarUrl + name + webPath + } + userPermissions { + playPipelineSchedule + takeOwnershipPipelineSchedule + updatePipelineSchedule + adminPipelineSchedule + } + } + } + } +} diff --git a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js new file mode 100644 index 00000000000..8f77e06c19a --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import PipelineSchedules from './components/pipeline_schedules.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default () => { + const containerEl = document.querySelector('#pipeline-schedules-app'); + + if (!containerEl) { + return false; + } + + const { fullPath } = containerEl.dataset; + + return new Vue({ + el: containerEl, + name: 'PipelineSchedulesRoot', + apolloProvider, + provide: { + fullPath, + }, + render(createElement) { + return createElement(PipelineSchedules); + }, + }); +}; diff --git a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js new file mode 100644 index 00000000000..d83417ab84a --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import PipelineSchedulesForm from './components/pipeline_schedules_form.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default (selector) => { + const containerEl = document.querySelector(selector); + + if (!containerEl) { + return false; + } + + const { fullPath } = containerEl.dataset; + + return new Vue({ + el: containerEl, + name: 'PipelineSchedulesFormRoot', + apolloProvider, + provide: { + fullPath, + }, + render(createElement) { + return createElement(PipelineSchedulesForm); + }, + }); +}; |