| // 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 { api } 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) { |
| api('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' |
| } |
| |
| api(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 |
| api('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> |