summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipeline_schedules
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipeline_schedules')
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue134
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue18
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue66
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue32
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue32
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue29
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue36
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue95
-rw-r--r--app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql6
-rw-r--r--app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql40
-rw-r--r--app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js32
-rw-r--r--app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js32
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);
+ },
+ });
+};