| // 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 class="form-layout" v-ctrl-enter="handleSubmit"> | |
| <a-form | |
| :ref="formRef" | |
| :model="form" | |
| :rules="rules" | |
| :loading="loading" | |
| layout="vertical" | |
| @finish="handleSubmit"> | |
| <a-form-item name="customactionid" ref="customactionid"> | |
| <template #label> | |
| <tooltip-label :title="$t('label.action')" :tooltip="apiParams.customactionid.description"/> | |
| </template> | |
| <a-select | |
| showSearch | |
| v-model:value="form.customactionid" | |
| :placeholder="apiParams.customactionid.description" | |
| optionFilterProp="label" | |
| :filterOption="(input, option) => { | |
| return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 | |
| }" | |
| @change="onCustomActionChange" > | |
| <a-select-option v-for="opt in customActions" :key="opt.id" :label="opt.name || opt.id"> | |
| {{ opt.name || opt.id }} | |
| </a-select-option> | |
| </a-select> | |
| </a-form-item> | |
| <a-card v-if="form.customactionid && (!!currentDescription || !!currentParameters && currentParameters.length > 0)" style="margin-bottom: 10px;"> | |
| <div v-if="!!currentDescription"> | |
| {{ currentDescription }} | |
| <a-divider v-if="!!currentParameters && currentParameters.length > 0"/> | |
| </div> | |
| <div v-for="(field, fieldIndex) in currentParameters" :key="fieldIndex"> | |
| <a-form-item :name="field.name" :ref="field.name"> | |
| <template #label> | |
| <tooltip-label :title="field.name" :tooltip="field.name"/> | |
| </template> | |
| <a-switch | |
| v-if="field.type === 'BOOLEAN'" | |
| v-model:checked="form[field.name]" | |
| :placeholder="field.name" | |
| v-focus="fieldIndex === 0" | |
| /> | |
| <a-date-picker | |
| v-else-if="field.type === 'DATE'" | |
| show-time | |
| v-model:value="form[field.name]" | |
| :placeholder="field.name" | |
| v-focus="fieldIndex === 0" | |
| /> | |
| <a-select | |
| v-else-if="field.valueoptions && field.valueoptions.length > 0" | |
| v-model:value="form[field.name]" | |
| :placeholder="field.name"> | |
| <a-select-option v-for="t in field.valueoptions" :key="t" :value="t">{{ t }}</a-select-option> | |
| </a-select> | |
| <a-input-number | |
| v-else-if="['NUMBER'].includes(field.type)" | |
| :precision="['DECIMAL'].includes(field.validationformat) ? 2 : 0" | |
| v-focus="fieldIndex === 0" | |
| v-model:value="form[field.name]" | |
| :placeholder="field.name" /> | |
| <a-input-password | |
| v-else-if="['STRING'].includes(field.type) && ['PASSWORD'].includes(field.validationformat)" | |
| v-focus="fieldIndex === 0" | |
| v-model:value="form[field.name]" | |
| :placeholder="field.name" /> | |
| <a-input | |
| v-else | |
| v-focus="fieldIndex === 0" | |
| v-model:value="form[field.name]" | |
| :placeholder="field.name" /> | |
| </a-form-item> | |
| </div> | |
| </a-card> | |
| <div :span="24" class="action-button"> | |
| <a-button @click="closeAction">{{ $t('label.cancel') }}</a-button> | |
| <a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ $t('label.ok') }}</a-button> | |
| </div> | |
| </a-form> | |
| </div> | |
| </template> | |
| <script> | |
| import { ref, reactive, toRaw, h } from 'vue' | |
| import { getAPI, postAPI } from '@/api' | |
| import TooltipLabel from '@/components/widgets/TooltipLabel' | |
| import DetailsInput from '@/components/widgets/DetailsInput' | |
| export default { | |
| name: 'RunCustomAction', | |
| components: { | |
| TooltipLabel, | |
| DetailsInput | |
| }, | |
| props: { | |
| resource: { | |
| type: Object, | |
| required: true | |
| } | |
| }, | |
| data () { | |
| return { | |
| customActions: [], | |
| loading: false | |
| } | |
| }, | |
| beforeCreate () { | |
| this.apiParams = this.$getApiParams('runCustomAction') | |
| }, | |
| created () { | |
| this.initForm() | |
| this.fetchCustomActions() | |
| }, | |
| computed: { | |
| datePattern () { | |
| return 'YYYY-MM-DDTHH:mm:ssZ' | |
| }, | |
| resourceType () { | |
| const metaResourceType = this.$route.meta.resourceType | |
| if (metaResourceType && !['UserVm', 'DomainRouter', 'SystemVm'].includes(metaResourceType)) { | |
| return metaResourceType | |
| } | |
| return 'VirtualMachine' | |
| }, | |
| currentAction () { | |
| if (!this.customActions || this.customActions.length === 0 || !this.form.customactionid) { | |
| return [] | |
| } | |
| return this.customActions.find(i => i.id === this.form.customactionid) | |
| }, | |
| currentDescription () { | |
| return this.currentAction?.description || '' | |
| }, | |
| currentParameters () { | |
| return this.currentAction?.parameters || [] | |
| } | |
| }, | |
| methods: { | |
| initForm () { | |
| this.formRef = ref() | |
| this.form = reactive({}) | |
| this.rules = reactive({ | |
| customactionid: [{ required: true, message: `${this.$t('message.error.select')}` }] | |
| }) | |
| }, | |
| fetchCustomActions () { | |
| this.loading = true | |
| this.customActions = [] | |
| const params = { | |
| resourcetype: this.resourceType, | |
| resourceid: this.resource.id, | |
| enabled: true | |
| } | |
| getAPI('listCustomActions', params).then(json => { | |
| this.customActions = json?.listcustomactionsresponse?.extensioncustomaction || [] | |
| }).finally(() => { | |
| this.loading = false | |
| }) | |
| }, | |
| onCustomActionChange () { | |
| Object.keys(this.rules).forEach(key => { | |
| if (key !== 'customactionid') { | |
| delete this.rules[key] | |
| } | |
| }) | |
| Object.keys(this.form).forEach(key => { | |
| if (key !== 'customactionid') { | |
| delete this.form[key] | |
| } | |
| }) | |
| if (this.currentParameters && this.currentParameters.length > 0) { | |
| this.currentParameters.forEach(field => { | |
| const required = !!field.required | |
| const fieldRules = [] | |
| if (field.type === 'BOOLEAN') { | |
| this.form[field.name] = false | |
| } | |
| // Required rule | |
| if (required) { | |
| fieldRules.push({ | |
| required: true, | |
| message: `${this.$t('message.error.input.value', { field: field.name })}` | |
| }) | |
| } | |
| if (field.type === 'STRING' && field.validationformat) { | |
| let validator | |
| switch (field.validationformat) { | |
| case 'EMAIL': | |
| validator = { | |
| type: 'email', | |
| message: this.$t('message.error.input.invalidemail') | |
| } | |
| break | |
| case 'URL': | |
| validator = { | |
| type: 'url', | |
| message: this.$t('message.error.input.invalidurl') | |
| } | |
| break | |
| case 'UUID': | |
| validator = { | |
| pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, | |
| message: this.$t('message.error.input.invaliduuid') | |
| } | |
| break | |
| } | |
| if (validator) { | |
| fieldRules.push(validator) | |
| } | |
| } | |
| if (fieldRules.length > 0) { | |
| this.rules[field.name] = fieldRules | |
| } | |
| }) | |
| } | |
| }, | |
| handleSubmit (e) { | |
| e.preventDefault() | |
| if (this.loading) return | |
| this.formRef.value.validate().then(() => { | |
| const values = toRaw(this.form) | |
| this.loading = true | |
| const params = { | |
| resourcetype: this.resourceType, | |
| resourceid: this.resource.id, | |
| customactionid: values.customactionid | |
| } | |
| var keys = Object.keys(values) | |
| keys = keys.filter(k => k !== 'customactionid') | |
| for (const key of keys) { | |
| var value = values[key] | |
| if (value !== undefined && value != null && | |
| (typeof value !== 'string' || (typeof value === 'string' && value.trim().length > 0))) { | |
| const fieldDef = this.currentParameters.find(f => f.name === key) | |
| if (fieldDef?.type === 'DATE') { | |
| value = value.format(this.datePattern) | |
| } | |
| params['parameters[0].' + key] = value | |
| } | |
| } | |
| postAPI('runCustomAction', params).then(response => { | |
| this.$pollJob({ | |
| jobId: response.runcustomactionresponse.jobid, | |
| title: this.currentAction.name || this.$t('label.run.custom.action'), | |
| description: this.currentAction.description || this.currentAction.name, | |
| showSuccessMessage: false, | |
| successMethod: (result) => { | |
| this.$emit('refresh-data') | |
| const actionResponse = result.jobresult?.customactionresult || {} | |
| const success = actionResponse.success || false | |
| const message = actionResponse?.result?.message || (success ? 'Success' : 'Failed') | |
| const details = actionResponse?.result?.details | |
| let printExtensionMessage = false | |
| let extensionMessage = null | |
| try { | |
| const parsedDetails = JSON.parse(details) | |
| printExtensionMessage = String(parsedDetails?.printmessage).toLowerCase() === 'true' | |
| extensionMessage = parsedDetails?.message | |
| } catch (_) {} | |
| const modalType = success ? this.$success : this.$error | |
| const contentElements = [h('p', `${message}`)] | |
| if (extensionMessage && !Array.isArray(extensionMessage) && typeof extensionMessage === 'object' && Object.keys(extensionMessage).length > 0) { | |
| extensionMessage = [extensionMessage] | |
| } | |
| if (Array.isArray(extensionMessage) && extensionMessage.length > 0 && printExtensionMessage) { | |
| contentElements.push( | |
| h('div', { | |
| style: { | |
| marginTop: '1em', | |
| maxHeight: '50vh', | |
| maxWidth: '100%', | |
| overflow: 'auto', | |
| backgroundColor: '#f6f6f6', | |
| border: '1px solid #ddd', | |
| borderRadius: '4px', | |
| display: 'block' | |
| } | |
| }, [ | |
| h('table', { | |
| style: { | |
| width: '100%', | |
| minWidth: 'max-content', | |
| borderCollapse: 'collapse', | |
| whiteSpace: 'pre-wrap' | |
| } | |
| }, [ | |
| h('thead', [ | |
| h('tr', Object.keys(extensionMessage[0]).map(key => | |
| h('th', { | |
| style: { | |
| padding: '8px', | |
| border: '1px solid #ddd', | |
| textAlign: 'left', | |
| fontWeight: 'bold', | |
| backgroundColor: '#fafafa' | |
| } | |
| }, key) | |
| )) | |
| ]), | |
| h('tbody', extensionMessage.map(row => | |
| h('tr', Object.values(row).map(value => | |
| h('td', { | |
| style: { | |
| padding: '8px', | |
| border: '1px solid #ddd', | |
| fontFamily: 'monospace' | |
| } | |
| }, String(value)) | |
| )) | |
| )) | |
| ]) | |
| ]) | |
| ) | |
| } else if (printExtensionMessage === 'true') { | |
| contentElements.push( | |
| h('div', { | |
| style: { | |
| marginTop: '1em', | |
| maxHeight: '50vh', | |
| padding: '10px', | |
| overflowY: 'auto', | |
| backgroundColor: '#f6f6f6', | |
| border: '1px solid #ddd', | |
| borderRadius: '4px', | |
| fontFamily: 'monospace', | |
| whiteSpace: 'pre-wrap', | |
| wordBreak: 'break-word' | |
| } | |
| }, String(extensionMessage)) | |
| ) | |
| } | |
| modalType({ | |
| title: this.currentAction.name || this.$t('label.run.custom.action'), | |
| content: h('div', contentElements), | |
| width: '700px', | |
| okText: this.$t('label.ok') | |
| }) | |
| }, | |
| errorMessage: this.currentAction.name || this.$t('label.run.custom.action'), | |
| loadingMessage: this.$t('message.running.custom.action'), | |
| catchMessage: this.$t('error.fetching.async.job.result') | |
| }) | |
| this.closeAction() | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }).finally(() => { | |
| this.loading = false | |
| }) | |
| }).catch(error => { | |
| this.$notifyError(error) | |
| }) | |
| }, | |
| closeAction () { | |
| this.$emit('close-action') | |
| } | |
| } | |
| } | |
| </script> | |
| <style scoped lang="less"> | |
| .form-layout { | |
| width: 80vw; | |
| @media (min-width: 600px) { | |
| width: 550px; | |
| } | |
| } | |
| </style> |