diff --git a/assets/translations/en-us.yaml b/assets/translations/en-us.yaml index a30836f82f..312d13942e 100644 --- a/assets/translations/en-us.yaml +++ b/assets/translations/en-us.yaml @@ -140,6 +140,7 @@ product: suffix: percent: "%" milliCpus: mili CPUs + cores: Cores cpus: CPUs ib: iB mib: MiB @@ -1020,6 +1021,83 @@ cluster: =1 {# vCPU} other {# vCPUs} }, {disk} GB Disk ({value}) + vsphere: + hostOptions: + any: Any + vAppOptions: + label: vApp Options + description: Choose OVF environment properties + disable: Do not use vApp + auto: Use vApp to configure networks with network protocol profiles + manual: Provide a custom vApp config + restoreType: Restore Type + transport: + label: OVF environment transport + tooltip: com.vmware.guestInfo or iso + placeholder: e.g. com.vmware.guestInfo + protocol: + label: vApp IP protocol + tooltip: IPv4 or IPv6 + placeholder: e.g. IPv4 + allocation: + label: vApp IP allocation policy + tooltip: dhcp, fixed, transient or fixedAllocated + placeholder: e.g. fixedAllocated + properties: + label: vApp properties + add: Add Property + keyPlaceholder: e.g. guestinfo.interface.0.ip.0.address + valuePlaceholder: e.g. ip:VM Network, expression or string + networks: + label: Networks + add: Add Network + guestinfo: + label: Configuration Parameters used for guestinfo + add: Add Parameter + keyPlaceholder: e.g. guestinfo.hostname + valuePlaceholder: e.g. myrancherhost + creationMethods: + template: 'Deploy from template: Data Center' + library: 'Deploy from template: Content Library' + vm: 'Clone an existing virtual machine' + legacy: 'Install from boot2docker ISO (Legacy)' + scheduling: + label: Scheduling + description: Choose what hypervisor the virtual machine will be scheduled to + dataCenter: Data Center + resourcePool: Resource Pool + dataStore: Data Store + folder: Folder + host: + label: Host + note: Specific host to create VM on (leave blank for standalone ESXi or for cluster with DRS) + instanceOptions: + label: Instance Options + description: Choose the size and OS of the virtual machine + cpus: CPUs + memory: Memory + disk: Disk + creationMethod: Creation method + template: Template + contentLibrary: Content library + libraryTemplate: Library template + virtualMachine: Virtual machine + osIsoUrl: + label: OS ISO URL + placeholder: 'Default: Latest rancheros-vmware image' + cloudInit: + label: Cloud Init + placeholder: e.g. http://my_host/cloud-config.yml + note: Cloud-init file or url to set in the guestinfo + cloudConfigYaml: Cloud Config YAML + tags: + label: Tags + description: Tags allow you to attach metadata to objects in the vSphere inventory to make it easier to sort and search for these objects. + addTag: Add Tag + customAttributes: + label: Custom attributes (legacy) + description: Custom attributes allow you to attach metadata to objects in the vSphere inventory to make it easier to sort and search for these objects. + add: Add custom attribute machinePool: name: diff --git a/cloud-credential/vmwarevsphere.vue b/cloud-credential/vmwarevsphere.vue index 4bf8eec85f..e953e74e74 100644 --- a/cloud-credential/vmwarevsphere.vue +++ b/cloud-credential/vmwarevsphere.vue @@ -6,6 +6,15 @@ export default { components: { LabeledInput }, mixins: [CreateEditView], + watch: { + value: { + deep: true, + handler(neu) { + this.$emit('validationChanged', !!neu); + } + } + }, + methods: { test() { // Vsphere doesn't have a test function. The credential has to be created before we can make calls. diff --git a/components/form/ArrayList.vue b/components/form/ArrayList.vue index 6a36dde5db..473a796ca0 100644 --- a/components/form/ArrayList.vue +++ b/components/form/ArrayList.vue @@ -76,6 +76,11 @@ export default { type: [String, Number, Object, Array], default: '' }, + + loading: { + type: Boolean, + default: false + } }, data() { @@ -280,8 +285,8 @@ export default { diff --git a/components/form/ArrayListSelect.vue b/components/form/ArrayListSelect.vue new file mode 100644 index 0000000000..4c1e695c11 --- /dev/null +++ b/components/form/ArrayListSelect.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/components/form/KeyValue.vue b/components/form/KeyValue.vue index edef29dd71..90a22d1217 100644 --- a/components/form/KeyValue.vue +++ b/components/form/KeyValue.vue @@ -73,6 +73,11 @@ export default { default: true, }, + keyOptionUnique: { + type: Boolean, + default: false, + }, + keyPlaceholder: { type: String, default() { @@ -211,6 +216,10 @@ export default { type: Array, default: () => [': ', '='], }, + loading: { + default: false, + type: Boolean + }, }, data() { @@ -279,6 +288,19 @@ export default { containerStyle() { return `grid-template-columns: repeat(${ 2 + this.extraColumns.length }, 1fr)${ this.removeAllowed ? ' 50px' : '' };`; }, + + usedKeyOptions() { + return this.rows.map(row => row[this.keyName]); + }, + + filteredKeyOptions() { + if (this.keyOptionUnique) { + return this.keyOptions + .filter(option => !this.usedKeyOptions.includes(option.value)); + } + + return this.keyOptions; + } }, created() { @@ -418,6 +440,16 @@ export default { this.queueUpdate(); }, + calculateOptions(value) { + const valueOption = this.keyOptions.find(o => o.value === value); + + if (valueOption) { + return [valueOption, ...this.filteredKeyOptions]; + } + + return this.filteredKeyOptions; + }, + get, } @@ -476,7 +508,8 @@ export default { :searchable="true" :clearable="false" :taggable="keyTaggable" - :options="keyOptions" + :options="calculateOptions(row[keyName])" + @input="queueUpdate" /> - { if ( opt ) { - if ( opt.disabled || opt.kind === 'group' || opt.kind === 'divider' ) { + if ( opt.disabled || opt.kind === 'group' || opt.kind === 'divider' || opt.loading ) { return false; } } @@ -240,7 +244,7 @@ export default { :append-to-body="appendToBody" :calculate-position="withPopper" :class="{ 'no-label': !(label || '').length }" - :disabled="isView || disabled" + :disabled="isView || disabled || loading" :get-option-key=" (opt) => (optionKey ? get(opt, optionKey) : getOptionLabel(opt)) " @@ -252,7 +256,7 @@ export default { :reduce="(x) => reduce(x)" :searchable="isSearchable" :selectable="selectable" - :value="value != null ? value : ''" + :value="value != null && !loading ? value : ''" v-on="$listeners" @search:blur="onBlur" @search:focus="onFocus" @@ -274,6 +278,7 @@ export default { + .labeled-select { + position: relative; + + .icon-spinner { + position: absolute; + left: calc(50% - .5em); + top: calc(50% - .5em); + } + .labeled-container { padding: $input-padding-sm 0 1px $input-padding-sm; diff --git a/machine-config/vmwarevsphere.vue b/machine-config/vmwarevsphere.vue new file mode 100644 index 0000000000..0b90fe421e --- /dev/null +++ b/machine-config/vmwarevsphere.vue @@ -0,0 +1,928 @@ + + + diff --git a/utils/computed.js b/utils/computed.js new file mode 100644 index 0000000000..9daa17a7c2 --- /dev/null +++ b/utils/computed.js @@ -0,0 +1,45 @@ +const { set, get } = require('@/utils/object'); + +/** + * Creates a computed property that handles converting strings to numbers and numbers to strings. Particularly when dealing with UnitInput. + * @param {*} path The path of the real value + * @returns the computed property + */ +export function integerString(path) { + return { + get() { + return Number.parseFloat(get(this, path)); + }, + + set(value) { + set(this, path, value.toString(10)); + } + }; +} + +/** + * Creates a computed property that handles converting strings a list of strings that look like ['key=value'] into { key: value } and back + * @param {*} path The path of the real value + * @param {*} delimeter the character/s used between the key/value. Default value '='. + * @returns the computed property + */ +export function keyValueStrings(path, delimeter = '=') { + return { + get() { + const result = {}; + + get(this, path).forEach((entry) => { + const [key, value] = entry.split(delimeter); + + result[key] = value; + }); + + return result; + }, + set(value) { + const newValue = Object.entries(value).map(([key, value]) => `${ key }${ delimeter }${ value }`); + + set(this, path, newValue); + } + }; +}