| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| <template> | |
| <div> | |
| <a-button | |
| type="dashed" | |
| style="width: 100%; margin-bottom: 10px" | |
| @click="showAddModal" | |
| :loading="loading" | |
| :disabled="!('createVMSchedule' in $store.getters.apis)" | |
| > | |
| <template #icon><plus-outlined /></template> {{ $t('label.schedule.add') }} | |
| </a-button> | |
| <list-view | |
| :loading="tabLoading" | |
| :columns="columns" | |
| :items="schedules" | |
| :columnKeys="columnKeys" | |
| :selectedColumns="selectedColumnKeys" | |
| ref="listview" | |
| @update-selected-columns="updateSelectedColumns" | |
| @update-vm-schedule="updateVMSchedule" | |
| @remove-vm-schedule="removeVMSchedule" | |
| @refresh="this.fetchData" | |
| /> | |
| <a-pagination | |
| class="row-element" | |
| style="margin-top: 10px" | |
| size="small" | |
| :current="page" | |
| :pageSize="pageSize" | |
| :total="totalCount" | |
| :showTotal="total => `${$t('label.showing')} ${Math.min(total, 1 + ((page - 1) * pageSize))}-${Math.min(page * pageSize, total)} ${$t('label.of')} ${total} ${$t('label.items')}`" | |
| :pageSizeOptions="pageSizeOptions" | |
| @change="changePage" | |
| @showSizeChange="changePage" | |
| showSizeChanger | |
| showQuickJumper | |
| > | |
| <template #buildOptionText="props"> | |
| <span>{{ props.value }} / {{ $t('label.page') }}</span> | |
| </template> | |
| </a-pagination> | |
| </div> | |
| <a-modal | |
| :visible="showModal" | |
| :title="$t('label.schedule')" | |
| :maskClosable="false" | |
| :closable="true" | |
| @cancel="closeModal" | |
| @ok="submitForm" | |
| > | |
| <a-form | |
| layout="vertical" | |
| :ref="formRef" | |
| :model="form" | |
| :rules="rules" | |
| @finish="submitForm" | |
| v-ctrl-enter="submitForm" | |
| > | |
| <a-form-item | |
| name="description" | |
| ref="description" | |
| :wrapperCol="{ span: 24 }" | |
| > | |
| <template #label> | |
| <tooltip-label | |
| :title="$t('label.description')" | |
| :tooltip="apiParams.description.description" | |
| /> | |
| </template> | |
| <a-input | |
| v-model:value="form.description" | |
| v-focus="true" | |
| /> | |
| </a-form-item> | |
| <a-form-item | |
| name="action" | |
| ref="action" | |
| :wrapperCol="{ span: 24 }" | |
| > | |
| <template #label> | |
| <tooltip-label | |
| :title="$t('label.action')" | |
| :tooltip="apiParams.action.description" | |
| /> | |
| </template> | |
| <a-radio-group | |
| v-model:value="form.action" | |
| button-style="solid" | |
| :disabled="isEdit" | |
| > | |
| <a-radio-button | |
| v-for="action in actions" | |
| :key="action.id" | |
| :value="action.value" | |
| > | |
| {{ $t(action.label) }} | |
| </a-radio-button> | |
| </a-radio-group> | |
| </a-form-item> | |
| <a-form-item | |
| name="timezone" | |
| ref="timezone" | |
| :wrapperCol="{ span: 24 }" | |
| > | |
| <template #label> | |
| <tooltip-label | |
| :title="$t('label.timezone')" | |
| :tooltip="apiParams.timezone.description" | |
| /> | |
| </template> | |
| <a-select | |
| showSearch | |
| v-model:value="form.timezone" | |
| optionFilterProp="label" | |
| :filterOption="(input, option) => { | |
| return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 | |
| }" | |
| :loading="fetching" | |
| > | |
| <a-select-option | |
| v-for="opt in timeZoneMap" | |
| :key="opt.id" | |
| :label="opt.name || opt.description" | |
| > | |
| {{ opt.name || opt.description }} | |
| </a-select-option> | |
| </a-select> | |
| </a-form-item> | |
| <a-row justify="space-between"> | |
| <a-col> | |
| <a-form-item | |
| name="startDate" | |
| ref="startDate" | |
| > | |
| <template #label> | |
| <tooltip-label | |
| :title="$t('label.start.date.and.time')" | |
| :tooltip="apiParams.startdate.description" | |
| /> | |
| </template> | |
| <a-date-picker | |
| v-model:value="form.startDate" | |
| show-time | |
| :locale="this.$i18n.locale" | |
| :placeholder="$t('message.select.start.date.and.time')" | |
| /> | |
| </a-form-item> | |
| </a-col> | |
| <a-col> | |
| <a-form-item | |
| name="endDate" | |
| ref="endDate" | |
| > | |
| <template #label> | |
| <tooltip-label | |
| :title="$t('label.end.date.and.time')" | |
| :tooltip="apiParams.enddate.description" | |
| /> | |
| </template> | |
| <a-date-picker | |
| v-model:value="form.endDate" | |
| show-time | |
| :locale="this.$i18n.locale" | |
| :placeholder="$t('message.select.end.date.and.time')" | |
| /> | |
| </a-form-item> | |
| </a-col> | |
| </a-row> | |
| <a-form-item | |
| name="schedule" | |
| ref="schedule" | |
| :wrapperCol="{ span: 24 }" | |
| > | |
| <template #label> | |
| <tooltip-label | |
| :title="$t('label.schedule')" | |
| :tooltip="apiParams.schedule.description" | |
| /> | |
| </template> | |
| <a-row | |
| style="margin-bottom: 15px; text-align: center;" | |
| justify="space-around" | |
| align="middle" | |
| > | |
| <cron-ant | |
| v-if="!form.useCronFormat" | |
| v-model="form.schedule" | |
| :periods="periods" | |
| :button-props="{ type: 'primary', size: 'small', disabled: form.useCronFormat }" | |
| @error="error = $event" | |
| /> | |
| <label | |
| v-if="form.useCronFormat"> | |
| {{ generateHumanReadableSchedule(form.schedule) }} | |
| </label> | |
| </a-row> | |
| <a-row | |
| justify="space-between" | |
| align="middle" | |
| > | |
| <a-col> | |
| <label>{{ $t('label.cron.mode') }}</label> | |
| </a-col> | |
| <a-col> | |
| <a-switch v-model:checked="form.useCronFormat"> | |
| </a-switch> | |
| </a-col> | |
| <a-col :span="12"> | |
| <a-input | |
| :addonBefore="$t('label.cron')" | |
| v-model:value="form.schedule" | |
| :disabled="!form.useCronFormat" | |
| v-focus="true" | |
| /> | |
| </a-col> | |
| </a-row> | |
| </a-form-item> | |
| <a-form-item | |
| name="enabled" | |
| ref="enabled" | |
| :wrapperCol="{ span: 24}" | |
| > | |
| <template #label> | |
| <tooltip-label | |
| :title="$t('label.enabled')" | |
| :tooltip="apiParams.enabled.description" | |
| /> | |
| </template> | |
| <a-switch v-model:checked="form.enabled" /> | |
| </a-form-item> | |
| </a-form> | |
| </a-modal> | |
| </template> | |
| <script> | |
| import { reactive, ref, toRaw } from 'vue' | |
| import { getAPI, postAPI } from '@/api' | |
| import ListView from '@/components/view/ListView' | |
| import Status from '@/components/widgets/Status' | |
| import TooltipLabel from '@/components/widgets/TooltipLabel' | |
| import { mixinForm } from '@/utils/mixin' | |
| import { timeZone } from '@/utils/timezone' | |
| import debounce from 'lodash/debounce' | |
| import cronstrue from 'cronstrue/i18n' | |
| import dayjs from 'dayjs' | |
| import utc from 'dayjs/plugin/utc' | |
| import timezone from 'dayjs/plugin/timezone' | |
| dayjs.extend(utc) | |
| dayjs.extend(timezone) | |
| export default { | |
| name: 'InstanceSchedules', | |
| mixins: [mixinForm], | |
| components: { | |
| Status, | |
| ListView, | |
| TooltipLabel | |
| }, | |
| props: { | |
| virtualmachine: { | |
| type: Object, | |
| required: true | |
| }, | |
| loading: { | |
| type: Boolean, | |
| required: true | |
| } | |
| }, | |
| data () { | |
| this.fetchTimeZone = debounce(this.fetchTimeZone, 800) | |
| return { | |
| tabLoading: false, | |
| columnKeys: ['action', 'enabled', 'description', 'schedule', 'timezone', 'startdate', 'enddate', 'created', 'vmScheduleActions'], | |
| selectedColumnKeys: [], | |
| columns: [], | |
| schedules: [], | |
| timeZoneMap: [], | |
| actions: [ | |
| { value: 'START', label: 'label.start' }, | |
| { value: 'STOP', label: 'label.stop' }, | |
| { value: 'REBOOT', label: 'label.reboot' }, | |
| { value: 'FORCE_STOP', label: 'label.force.stop' }, | |
| { value: 'FORCE_REBOOT', label: 'label.force.reboot' } | |
| ], | |
| periods: [ | |
| { id: 'year', value: ['month', 'day', 'dayOfWeek', 'hour', 'minute'] }, | |
| { id: 'month', value: ['day', 'dayOfWeek', 'hour', 'minute'] }, | |
| { id: 'week', value: ['dayOfWeek', 'hour', 'minute'] }, | |
| { id: 'day', value: ['hour', 'minute'] } | |
| ], | |
| page: 1, | |
| pageSize: 20, | |
| totalCount: 0, | |
| showModal: false, | |
| isSubmitted: false, | |
| isEdit: false, | |
| error: '', | |
| pattern: 'YYYY-MM-DD HH:mm:ss' | |
| } | |
| }, | |
| beforeCreate () { | |
| this.apiParams = this.$getApiParams('createVMSchedule') | |
| }, | |
| computed: { | |
| pageSizeOptions () { | |
| var sizes = [20, 50, 100, 200, this.$store.getters.defaultListViewPageSize] | |
| if (this.device !== 'desktop') { | |
| sizes.unshift(10) | |
| } | |
| return [...new Set(sizes)].sort(function (a, b) { | |
| return a - b | |
| }).map(String) | |
| } | |
| }, | |
| created () { | |
| this.selectedColumnKeys = this.columnKeys | |
| this.updateColumns() | |
| this.pageSize = this.pageSizeOptions[0] * 1 | |
| this.initForm() | |
| this.fetchData() | |
| this.fetchTimeZone() | |
| }, | |
| watch: { | |
| virtualmachine: { | |
| handler () { | |
| this.fetchSchedules() | |
| } | |
| } | |
| }, | |
| methods: { | |
| initForm () { | |
| this.formRef = ref() | |
| this.form = reactive({ | |
| action: 'START', | |
| schedule: '* * * * *', | |
| description: '', | |
| timezone: 'UTC', | |
| startDate: '', | |
| endDate: '', | |
| enabled: true, | |
| useCronFormat: false | |
| }) | |
| this.rules = reactive({ | |
| schedule: [{ type: 'string', required: true, message: this.$t('message.error.required.input') }], | |
| action: [{ type: 'string', required: true, message: this.$t('message.error.required.input') }], | |
| timezone: [{ required: true, message: `${this.$t('message.error.select')}` }], | |
| startDate: [{ required: false, message: `${this.$t('message.error.select')}` }], | |
| endDate: [{ required: false, message: `${this.$t('message.error.select')}` }] | |
| }) | |
| }, | |
| createVMSchedule (schedule) { | |
| this.resetForm() | |
| this.showAddModal() | |
| }, | |
| removeVMSchedule (schedule) { | |
| postAPI('deleteVMSchedule', { | |
| id: schedule.id, | |
| virtualmachineid: this.virtualmachine.id | |
| }).then(() => { | |
| if (this.totalCount - 1 === this.pageSize * (this.page - 1)) { | |
| this.page = this.page - 1 > 0 ? this.page - 1 : 1 | |
| } | |
| const message = `${this.$t('label.removing')} ${schedule.description}` | |
| this.$message.success(message) | |
| }).catch(error => { | |
| console.error(error) | |
| this.$message.error(this.$t('message.error.remove.vm.schedule')) | |
| this.$notification.error({ | |
| message: this.$t('label.error'), | |
| description: this.$t('message.error.remove.vm.schedule') | |
| }) | |
| }).finally(() => { | |
| this.fetchData() | |
| }) | |
| }, | |
| updateVMSchedule (schedule) { | |
| this.resetForm() | |
| this.isEdit = true | |
| Object.assign(this.form, schedule) | |
| // Some weird issue when we directly pass in the moment with tz object | |
| this.form.startDate = dayjs(schedule.startdate).tz(schedule.timezone) | |
| this.form.endDate = schedule.enddate ? dayjs(dayjs(schedule.enddate).tz(schedule.timezone)) : null | |
| this.showAddModal() | |
| }, | |
| showAddModal () { | |
| this.showModal = true | |
| }, | |
| submitForm () { | |
| if (this.isSubmitted) return | |
| this.isSubmitted = true | |
| this.formRef.value.validate().then(() => { | |
| const formRaw = toRaw(this.form) | |
| const values = this.handleRemoveFields(formRaw) | |
| var params = { | |
| description: values.description, | |
| schedule: values.schedule, | |
| timezone: values.timezone, | |
| action: values.action, | |
| virtualmachineid: this.virtualmachine.id, | |
| enabled: values.enabled, | |
| startdate: (values.startDate) ? values.startDate.format(this.pattern) : null, | |
| enddate: (values.endDate) ? values.endDate.format(this.pattern) : null | |
| } | |
| let command = null | |
| if (this.form.id === null || this.form.id === undefined) { | |
| command = 'createVMSchedule' | |
| } else { | |
| params.id = this.form.id | |
| command = 'updateVMSchedule' | |
| } | |
| postAPI(command, params).then(response => { | |
| this.$notification.success({ | |
| message: this.$t('label.schedule'), | |
| description: this.$t('message.success.config.vm.schedule') | |
| }) | |
| this.isSubmitted = false | |
| this.fetchData() | |
| this.closeModal() | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| this.isSubmitted = false | |
| }) | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| if (error.errorFields !== undefined) { | |
| this.formRef.value.scrollToField(error.errorFields[0].name) | |
| } | |
| }).finally(() => { | |
| this.isSubmitted = false | |
| }) | |
| }, | |
| resetForm () { | |
| this.isEdit = false | |
| if (this.formRef.value) { | |
| this.formRef.value.resetFields() | |
| } | |
| }, | |
| fetchTimeZone (value) { | |
| this.timeZoneMap = [] | |
| this.fetching = true | |
| timeZone(value).then(json => { | |
| this.timeZoneMap = json | |
| this.fetching = false | |
| }) | |
| }, | |
| closeModal () { | |
| this.resetForm() | |
| this.initForm() | |
| this.showModal = false | |
| }, | |
| fetchData () { | |
| this.fetchSchedules() | |
| }, | |
| fetchSchedules () { | |
| this.schedules = [] | |
| if (!this.virtualmachine.id) { | |
| return | |
| } | |
| const params = { | |
| page: this.page, | |
| pagesize: this.pageSize, | |
| virtualmachineid: this.virtualmachine.id, | |
| listall: true | |
| } | |
| this.tabLoading = true | |
| getAPI('listVMSchedule', params).then(json => { | |
| this.schedules = [] | |
| this.totalCount = json?.listvmscheduleresponse?.count || 0 | |
| this.schedules = json?.listvmscheduleresponse?.vmschedule || [] | |
| this.tabLoading = false | |
| }) | |
| }, | |
| changePage (page, pageSize) { | |
| this.page = page | |
| this.pageSize = pageSize | |
| this.fetchData() | |
| }, | |
| updateSelectedColumns (key) { | |
| if (this.selectedColumnKeys.includes(key)) { | |
| this.selectedColumnKeys = this.selectedColumnKeys.filter(x => x !== key) | |
| } else { | |
| this.selectedColumnKeys.push(key) | |
| } | |
| this.updateColumns() | |
| }, | |
| generateHumanReadableSchedule (schedule) { | |
| return cronstrue.toString(schedule, { locale: this.$i18n.locale, throwExceptionOnParseError: false, verbose: true }) | |
| }, | |
| updateColumns () { | |
| this.columns = [] | |
| for (var columnKey of this.columnKeys) { | |
| if (!this.selectedColumnKeys.includes(columnKey)) continue | |
| this.columns.push({ | |
| key: columnKey, | |
| // If columnKey is 'enabled', then title is 'state' | |
| // If columnKey is 'startdate', then the title is `start.date.and.time` | |
| // else title is columnKey | |
| title: columnKey === 'enabled' | |
| ? this.$t('label.state') | |
| : columnKey === 'startdate' | |
| ? this.$t('label.start.date.and.time') | |
| : columnKey === 'enddate' | |
| ? this.$t('label.end.date.and.time') | |
| : this.$t('label.' + String(columnKey).toLowerCase()), | |
| dataIndex: columnKey | |
| }) | |
| } | |
| if (this.columns.length > 0) { | |
| this.columns[this.columns.length - 1].customFilterDropdown = true | |
| } | |
| } | |
| } | |
| } | |
| </script> |