-
{{ $t('label.serviceofferingname') }}
+
{{ $t('label.system.offering') }}
+
{{ $t('label.serviceofferingname') }}
{{ resource.serviceofferingname || resource.serviceofferingid }}
diff --git a/ui/src/components/view/ListResourceTable.vue b/ui/src/components/view/ListResourceTable.vue
index a7e805b5443..a95927f00cf 100644
--- a/ui/src/components/view/ListResourceTable.vue
+++ b/ui/src/components/view/ListResourceTable.vue
@@ -50,6 +50,10 @@
{{ $toLocaleDate(text) }}
+
+ {{ bytesToHumanReadableSize(text) }}
+
+
{{ text }}
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue
index 5a779da182b..032985c730e 100644
--- a/ui/src/components/view/ListView.vue
+++ b/ui/src/components/view/ListView.vue
@@ -90,6 +90,13 @@
{{ text }}
{{ text }}
+
+
+
+ {{ $t('label.restartrequired') }}
+
+
+
@@ -174,7 +181,7 @@
{{ text <= 0 || !text ? 'N/A' : text }}
-
+
{{ text }}
@@ -241,6 +248,9 @@
{{ text }}
+
+ {{ text }}
+
@@ -253,6 +263,12 @@
{{ text }}
+
+
+ {{ item.name }}
+ ,
+
+
{{ text }}
@@ -262,6 +278,11 @@
{{ text }}
+
+
+ {{ text }}
+
+
{{ text }}
{{ text }}
@@ -381,7 +402,7 @@
{{ record.enabled ? 'Enabled' : 'Disabled' }}
-
+
{{ $toLocaleDate(text) }}
@@ -596,6 +617,12 @@ export default {
explicitlyAllowRowSelection: {
type: Boolean,
default: false
+ },
+ currentPage: {
+ type: Number
+ },
+ pageSize: {
+ type: Number
}
},
inject: ['parentFetchData', 'parentToggleLoading'],
@@ -684,7 +711,8 @@ export default {
'/project', '/account', 'buckets', 'objectstore',
'/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', '/systemvm', '/router', '/ilbvm', '/annotation',
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering',
- '/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', '/quotatariff'].join('|'))
+ '/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', '/quotatariff', '/sharedfs',
+ '/ipv4subnets'].join('|'))
.test(this.$route.path)
},
enableGroupAction () {
@@ -692,7 +720,7 @@ export default {
'vmsnapshot', 'backup', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', 'vnfapp',
'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering',
'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment', 'buckets',
- 'webhook', 'webhookdeliveries'
+ 'webhook', 'webhookdeliveries', 'sharedfs', 'ipv4subnets', 'asnumbers'
].includes(this.$route.name)
},
getDateAtTimeZone (date, timezone) {
@@ -733,7 +761,7 @@ export default {
}).then(json => {
this.editableValueKey = null
this.$store.dispatch('RefreshFeatures')
- this.$message.success(`${this.$t('message.setting.updated')} ${record.name}`)
+ this.$messageConfigSuccess(`${this.$t('message.setting.updated')} ${record.name}`, record)
if (json.updateconfigurationresponse &&
json.updateconfigurationresponse.configuration &&
!json.updateconfigurationresponse.configuration.isdynamic &&
@@ -754,8 +782,7 @@ export default {
api('resetConfiguration', {
name: item.name
}).then(() => {
- const message = `${this.$t('label.setting')} ${item.name} ${this.$t('label.reset.config.value')}`
- this.$message.success(message)
+ this.$messageConfigSuccess(`${this.$t('label.setting')} ${item.name} ${this.$t('label.reset.config.value')}`, item)
}).catch(error => {
console.error(error)
this.$message.error(this.$t('message.error.reset.config'))
@@ -821,8 +848,9 @@ export default {
},
updateOrder (data) {
const promises = []
+ const previousSortKeys = this.pageSize && this.currentPage ? this.pageSize * (this.currentPage - 1) : 0
data.forEach((item, index) => {
- promises.push(this.handleUpdateOrder(item.id, index + 1))
+ promises.push(this.handleUpdateOrder(item.id, previousSortKeys + index + 1))
})
Promise.all(promises).catch((reason) => {
console.log(reason)
diff --git a/ui/src/components/view/SearchView.vue b/ui/src/components/view/SearchView.vue
index 07e2d14e8bc..c82b1172f2d 100644
--- a/ui/src/components/view/SearchView.vue
+++ b/ui/src/components/view/SearchView.vue
@@ -90,6 +90,13 @@
{{ $t((['storageid'].includes(field.name) || !opt.path) ? opt.name : opt.path) }}
+
+
+
+
+
+
+ {{ $t(opt.path || opt.name) }}
@@ -295,6 +302,9 @@ export default {
if (item === 'groupid' && !('listInstanceGroups' in this.$store.getters.apis)) {
return true
}
+ if (item === 'associatednetworkid' && this.$route.meta.name === 'asnumbers') {
+ item = 'networkid'
+ }
if (item === 'usagetype' && !('listUsageTypes' in this.$store.getters.apis)) {
return true
}
@@ -303,7 +313,7 @@ export default {
}
if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 'account', 'hypervisor', 'level',
'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 'systemvmtype', 'scope', 'provider',
- 'type', 'scope', 'managementserverid', 'serviceofferingid', 'diskofferingid', 'usagetype'].includes(item)
+ 'type', 'scope', 'managementserverid', 'serviceofferingid', 'diskofferingid', 'networkid', 'usagetype', 'restartrequired'].includes(item)
) {
type = 'list'
} else if (item === 'tags') {
@@ -395,6 +405,16 @@ export default {
this.fields[providerIndex].loading = false
}
+ if (arrayField.includes('restartrequired')) {
+ const restartRequiredIndex = this.fields.findIndex(item => item.name === 'restartrequired')
+ this.fields[restartRequiredIndex].loading = true
+ this.fields[restartRequiredIndex].opts = [
+ { id: 'true', name: 'label.yes' },
+ { id: 'false', name: 'label.no' }
+ ]
+ this.fields[restartRequiredIndex].loading = false
+ }
+
if (arrayField.includes('resourcetype')) {
const resourceTypeIndex = this.fields.findIndex(item => item.name === 'resourcetype')
this.fields[resourceTypeIndex].loading = true
@@ -427,6 +447,7 @@ export default {
let managementServerIdIndex = -1
let serviceOfferingIndex = -1
let diskOfferingIndex = -1
+ let networkIndex = -1
let usageTypeIndex = -1
let volumeIndex = -1
@@ -514,6 +535,12 @@ export default {
promises.push(await this.fetchDiskOfferings(searchKeyword))
}
+ if (arrayField.includes('networkid')) {
+ networkIndex = this.fields.findIndex(item => item.name === 'networkid')
+ this.fields[networkIndex].loading = true
+ promises.push(await this.fetchNetworks(searchKeyword))
+ }
+
if (arrayField.includes('usagetype')) {
usageTypeIndex = this.fields.findIndex(item => item.name === 'usagetype')
this.fields[usageTypeIndex].loading = true
@@ -608,6 +635,14 @@ export default {
this.fields[diskOfferingIndex].opts = this.sortArray(diskOfferings[0].data)
}
}
+
+ if (networkIndex > -1) {
+ const networks = response.filter(item => item.type === 'networkid')
+ if (networks && networks.length > 0) {
+ this.fields[networkIndex].opts = this.sortArray(networks[0].data)
+ }
+ }
+
if (usageTypeIndex > -1) {
const usageTypes = response.filter(item => item.type === 'usagetype')
if (usageTypes?.length > 0) {
@@ -651,12 +686,19 @@ export default {
if (diskOfferingIndex > -1) {
this.fields[diskOfferingIndex].loading = false
}
+ if (networkIndex > -1) {
+ this.fields[networkIndex].loading = false
+ }
if (usageTypeIndex > -1) {
this.fields[usageTypeIndex].loading = false
}
if (Array.isArray(arrayField)) {
this.fillFormFieldValues()
}
+ if (networkIndex > -1) {
+ this.fields[networkIndex].loading = false
+ }
+ this.fillFormFieldValues()
})
},
initFormFieldData () {
@@ -706,7 +748,7 @@ export default {
},
fetchDomains (searchKeyword) {
return new Promise((resolve, reject) => {
- api('listDomains', { listAll: true, showicon: true, keyword: searchKeyword }).then(json => {
+ api('listDomains', { listAll: true, details: 'min', showicon: true, keyword: searchKeyword }).then(json => {
const domain = json.listdomainsresponse.domain
resolve({
type: 'domainid',
@@ -841,6 +883,19 @@ export default {
})
})
},
+ fetchNetworks (searchKeyword) {
+ return new Promise((resolve, reject) => {
+ api('listNetworks', { listAll: true, keyword: searchKeyword }).then(json => {
+ const networks = json.listnetworksresponse.network
+ resolve({
+ type: 'networkid',
+ data: networks
+ })
+ }).catch(error => {
+ reject(error.response.headers['x-description'])
+ })
+ })
+ },
fetchAlertTypes () {
if (this.alertTypes.length > 0) {
return new Promise((resolve, reject) => {
diff --git a/ui/src/components/view/SettingsTab.vue b/ui/src/components/view/SettingsTab.vue
index cddbe56b83a..be74e7b32dd 100644
--- a/ui/src/components/view/SettingsTab.vue
+++ b/ui/src/components/view/SettingsTab.vue
@@ -174,8 +174,8 @@ export default {
name: item.name,
value: this.editableValue
}).then(() => {
- const message = `${this.$t('label.setting')} ${item.name} ${this.$t('label.update.to')} ${this.editableValue}`
- this.handleSuccessMessage(item.name, this.$route.meta.name, message)
+ var message = `${this.$t('label.setting')} ${item.name} ${this.$t('label.update.to')} ${this.editableValue}`
+ this.handleSuccessMessage(item, this.$route.meta.name, message)
}).catch(error => {
console.error(error)
this.$message.error(this.$t('message.error.save.setting'))
@@ -204,8 +204,8 @@ export default {
[this.scopeKey]: this.resource.id,
name: item.name
}).then(() => {
- const message = `${this.$t('label.setting')} ${item.name} ${this.$t('label.reset.config.value')}`
- this.handleSuccessMessage(item.name, this.$route.meta.name, message)
+ var message = `${this.$t('label.setting')} ${item.name} ${this.$t('label.reset.config.value')}`
+ this.handleSuccessMessage(item, this.$route.meta.name, message)
}).catch(error => {
console.error(error)
this.$message.error(this.$t('message.error.reset.config'))
@@ -220,12 +220,16 @@ export default {
})
})
},
- handleSuccessMessage (name, scope, message) {
- var obj = this.warningMessages[name]
+ handleSuccessMessage (config, scope, message) {
+ var obj = this.warningMessages[config.name]
if (obj && obj.scope === scope) {
- this.$warning({ title: message, content: obj.warning })
+ var content = obj.warning
+ if (config.isdynamic) {
+ content = `this.$t('message.setting.update.delay').\n ${content}`
+ }
+ this.$warning({ title: message, content: content })
} else {
- this.$message.success(message)
+ this.$messageConfigSuccess(message, config)
}
}
}
diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js
index 4c5a61e3bdc..51bbbb1cff4 100644
--- a/ui/src/config/section/compute.js
+++ b/ui/src/config/section/compute.js
@@ -108,6 +108,7 @@ export default {
docHelp: 'adminguide/virtual_machines.html#changing-the-vm-name-os-or-group',
dataView: true,
popup: true,
+ show: (record) => { return record.vmtype !== 'sharedfsvm' },
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/EditVM.vue')))
},
{
@@ -133,7 +134,10 @@ export default {
dataView: true,
groupAction: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) },
- args: ['forced'],
+ args: (record, store, group) => {
+ return (['Admin'].includes(store.userInfo.roletype) || store.features.allowuserforcestopvm)
+ ? ['forced'] : []
+ },
show: (record) => { return ['Running'].includes(record.state) }
},
{
@@ -166,7 +170,7 @@ export default {
message: 'message.reinstall.vm',
dataView: true,
popup: true,
- show: (record) => { return ['Running', 'Stopped'].includes(record.state) },
+ show: (record) => { return ['Running', 'Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' },
disabled: (record) => { return record.hostcontrolstate === 'Offline' },
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ReinstallVm.vue')))
},
@@ -178,9 +182,9 @@ export default {
dataView: true,
args: ['virtualmachineid', 'name', 'description', 'snapshotmemory', 'quiescevm'],
show: (record) => {
- return ((['Running'].includes(record.state) && record.hypervisor !== 'LXC') ||
+ return (((['Running'].includes(record.state) && record.hypervisor !== 'LXC') ||
(['Stopped'].includes(record.state) && ((record.hypervisor !== 'KVM' && record.hypervisor !== 'LXC') ||
- (record.hypervisor === 'KVM' && record.pooltype === 'PowerFlex'))))
+ (record.hypervisor === 'KVM' && record.pooltype === 'PowerFlex')))) && record.vmtype !== 'sharedfsvm')
},
disabled: (record) => { return record.hostcontrolstate === 'Offline' && record.hypervisor === 'KVM' },
mapping: {
@@ -211,7 +215,7 @@ export default {
docHelp: 'adminguide/virtual_machines.html#backup-offerings',
dataView: true,
args: ['virtualmachineid', 'backupofferingid'],
- show: (record) => { return !record.backupofferingid },
+ show: (record) => { return !record.backupofferingid && record.vmtype !== 'sharedfsvm' },
mapping: {
virtualmachineid: {
value: (record, params) => { return record.id }
@@ -226,7 +230,7 @@ export default {
docHelp: 'adminguide/virtual_machines.html#creating-vm-backups',
dataView: true,
args: ['virtualmachineid'],
- show: (record) => { return record.backupofferingid },
+ show: (record) => { return record.backupofferingid && record.vmtype !== 'sharedfsvm' },
mapping: {
virtualmachineid: {
value: (record, params) => { return record.id }
@@ -240,7 +244,7 @@ export default {
docHelp: 'adminguide/virtual_machines.html#creating-vm-backups',
dataView: true,
popup: true,
- show: (record) => { return record.backupofferingid },
+ show: (record) => { return record.backupofferingid && record.vmtype !== 'sharedfsvm' },
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/BackupScheduleWizard.vue'))),
mapping: {
virtualmachineid: {
@@ -259,7 +263,7 @@ export default {
docHelp: 'adminguide/virtual_machines.html#restoring-vm-backups',
dataView: true,
args: ['virtualmachineid', 'forced'],
- show: (record) => { return record.backupofferingid },
+ show: (record) => { return record.backupofferingid && record.vmtype !== 'sharedfsvm' },
mapping: {
virtualmachineid: {
value: (record, params) => { return record.id }
@@ -273,7 +277,7 @@ export default {
docHelp: 'adminguide/templates.html#attaching-an-iso-to-a-vm',
dataView: true,
popup: true,
- show: (record) => { return ['Running', 'Stopped'].includes(record.state) && !record.isoid },
+ show: (record) => { return ['Running', 'Stopped'].includes(record.state) && !record.isoid && record.vmtype !== 'sharedfsvm' },
disabled: (record) => { return record.hostcontrolstate === 'Offline' || record.hostcontrolstate === 'Maintenance' },
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/AttachIso.vue')))
},
@@ -290,7 +294,7 @@ export default {
}
return args
},
- show: (record) => { return ['Running', 'Stopped'].includes(record.state) && 'isoid' in record && record.isoid },
+ show: (record) => { return ['Running', 'Stopped'].includes(record.state) && 'isoid' in record && record.isoid && record.vmtype !== 'sharedfsvm' },
disabled: (record) => { return record.hostcontrolstate === 'Offline' || record.hostcontrolstate === 'Maintenance' },
mapping: {
virtualmachineid: {
@@ -305,7 +309,7 @@ export default {
docHelp: 'adminguide/virtual_machines.html#change-affinity-group-for-an-existing-vm',
dataView: true,
args: ['affinitygroupids'],
- show: (record) => { return ['Stopped'].includes(record.state) },
+ show: (record) => { return ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' },
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ChangeAffinity'))),
popup: true
},
@@ -315,7 +319,7 @@ export default {
label: 'label.scale.vm',
docHelp: 'adminguide/virtual_machines.html#how-to-dynamically-scale-cpu-and-ram',
dataView: true,
- show: (record) => { return ['Stopped'].includes(record.state) || (['Running'].includes(record.state) && record.hypervisor !== 'LXC') },
+ show: (record) => { return (['Stopped'].includes(record.state) || (['Running'].includes(record.state) && record.hypervisor !== 'LXC')) && record.vmtype !== 'sharedfsvm' },
disabled: (record) => { return record.state === 'Running' && !record.isdynamicallyscalable },
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ScaleVM.vue')))
@@ -366,7 +370,7 @@ export default {
message: 'message.desc.reset.ssh.key.pair',
docHelp: 'adminguide/virtual_machines.html#resetting-ssh-keys',
dataView: true,
- show: (record) => { return ['Stopped'].includes(record.state) },
+ show: (record) => { return ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' },
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ResetSshKeyPair')))
},
@@ -377,7 +381,7 @@ export default {
message: 'message.desc.reset.userdata',
docHelp: 'adminguide/virtual_machines.html#resetting-userdata',
dataView: true,
- show: (record) => { return ['Stopped'].includes(record.state) },
+ show: (record) => { return ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' },
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/ResetUserData')))
},
@@ -388,7 +392,7 @@ export default {
dataView: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/AssignInstance'))),
popup: true,
- show: (record) => { return ['Stopped'].includes(record.state) }
+ show: (record) => { return ['Stopped'].includes(record.state) && record.vmtype !== 'sharedfsvm' }
},
{
api: 'recoverVirtualMachine',
@@ -396,7 +400,7 @@ export default {
label: 'label.recover.vm',
message: 'message.recover.vm',
dataView: true,
- show: (record, store) => { return ['Destroyed'].includes(record.state) && store.features.allowuserexpungerecovervm }
+ show: (record, store) => { return ['Destroyed'].includes(record.state) && store.features.allowuserexpungerecovervm && record.vmtype !== 'sharedfsvm' }
},
{
api: 'unmanageVirtualMachine',
@@ -404,7 +408,7 @@ export default {
label: 'label.action.unmanage.virtualmachine',
message: 'message.action.unmanage.virtualmachine',
dataView: true,
- show: (record) => { return ['Running', 'Stopped'].includes(record.state) && ['VMware', 'KVM'].includes(record.hypervisor) }
+ show: (record) => { return ['Running', 'Stopped'].includes(record.state) && ['VMware', 'KVM'].includes(record.hypervisor) && record.vmtype !== 'sharedfsvm' }
},
{
api: 'expungeVirtualMachine',
@@ -413,7 +417,7 @@ export default {
message: (record) => { return record.backupofferingid ? 'message.action.expunge.instance.with.backups' : 'message.action.expunge.instance' },
docHelp: 'adminguide/virtual_machines.html#deleting-vms',
dataView: true,
- show: (record, store) => { return ['Destroyed', 'Expunging'].includes(record.state) && store.features.allowuserexpungerecovervm }
+ show: (record, store) => { return ['Destroyed', 'Expunging'].includes(record.state) && store.features.allowuserexpungerecovervm && record.vmtype !== 'sharedfsvm' }
},
{
api: 'destroyVirtualMachine',
@@ -429,7 +433,7 @@ export default {
},
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, expunge: values.expunge } }) },
- show: (record) => { return ['Running', 'Stopped', 'Error'].includes(record.state) },
+ show: (record) => { return ['Running', 'Stopped', 'Error'].includes(record.state) && record.vmtype !== 'sharedfsvm' },
component: shallowRef(defineAsyncComponent(() => import('@/views/compute/DestroyVM.vue')))
}
]
diff --git a/ui/src/config/section/config.js b/ui/src/config/section/config.js
index 8f792e51ac9..1736adf79c4 100644
--- a/ui/src/config/section/config.js
+++ b/ui/src/config/section/config.js
@@ -131,6 +131,43 @@ export default {
}
]
},
+ {
+ name: 'backuprepository',
+ title: 'label.backup.repository',
+ icon: 'inbox-outlined',
+ docHelp: 'adminguide/backup_and_recovery.html',
+ permission: ['listBackupRepositories'],
+ searchFilters: ['zoneid'],
+ columns: ['name', 'provider', 'type', 'address', 'zonename'],
+ details: ['name', 'type', 'address', 'provider', 'zonename'],
+ actions: [
+ {
+ api: 'addBackupRepository',
+ icon: 'plus-outlined',
+ label: 'label.backup.repository.add',
+ listView: true,
+ args: [
+ 'name', 'provider', 'address', 'type', 'mountopts', 'zoneid'
+ ],
+ mapping: {
+ type: {
+ options: ['nfs']
+ },
+ provider: {
+ value: (record) => { return 'nas' }
+ }
+ }
+ },
+ {
+ api: 'deleteBackupRepository',
+ icon: 'delete-outlined',
+ label: 'label.backup.repository.remove',
+ message: 'message.action.delete.backup.repository',
+ dataView: true,
+ popup: true
+ }
+ ]
+ },
{
name: 'hypervisorcapability',
title: 'label.hypervisor.capabilities',
diff --git a/ui/src/config/section/image.js b/ui/src/config/section/image.js
index 72cde6afe9c..5b561ef877c 100644
--- a/ui/src/config/section/image.js
+++ b/ui/src/config/section/image.js
@@ -57,7 +57,7 @@ export default {
return fields
},
details: () => {
- var fields = ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'format', 'ostypename', 'size', 'physicalsize', 'isready', 'passwordenabled',
+ var fields = ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'arch', 'format', 'ostypename', 'size', 'physicalsize', 'isready', 'passwordenabled',
'crossZones', 'templatetype', 'directdownload', 'deployasis', 'ispublic', 'isfeatured', 'isextractable', 'isdynamicallyscalable', 'crosszones', 'type',
'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy']
if (['Admin'].includes(store.getters.userInfo.roletype)) {
@@ -231,7 +231,7 @@ export default {
}
return fields
},
- details: ['name', 'id', 'displaytext', 'checksum', 'ostypename', 'size', 'bootable', 'isready', 'passwordenabled', 'directdownload', 'isextractable', 'ispublic', 'isfeatured', 'isdynamicallyscalable', 'crosszones', 'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy', 'url'],
+ details: ['name', 'id', 'displaytext', 'checksum', 'ostypename', 'size', 'arch', 'bootable', 'isready', 'passwordenabled', 'directdownload', 'isextractable', 'ispublic', 'isfeatured', 'isdynamicallyscalable', 'crosszones', 'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy', 'url'],
searchFilters: () => {
var filters = ['name', 'zoneid', 'tags']
if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
diff --git a/ui/src/config/section/infra/clusters.js b/ui/src/config/section/infra/clusters.js
index 8b2f37d5b7b..c7edc6560ef 100644
--- a/ui/src/config/section/infra/clusters.js
+++ b/ui/src/config/section/infra/clusters.js
@@ -35,7 +35,7 @@ export default {
fields.push('zonename')
return fields
},
- details: ['name', 'id', 'allocationstate', 'clustertype', 'managedstate', 'hypervisortype', 'podname', 'zonename', 'drsimbalance'],
+ details: ['name', 'id', 'allocationstate', 'clustertype', 'managedstate', 'arch', 'hypervisortype', 'podname', 'zonename', 'drsimbalance'],
related: [{
name: 'host',
title: 'label.hosts',
@@ -83,7 +83,12 @@ export default {
icon: 'edit-outlined',
label: 'label.edit',
dataView: true,
- args: ['clustername']
+ args: ['clustername', 'arch'],
+ mapping: {
+ arch: {
+ options: ['x86_64', 'aarch64']
+ }
+ }
},
{
api: 'updateCluster',
diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js
index 92688cf9280..727da7242d7 100644
--- a/ui/src/config/section/infra/hosts.js
+++ b/ui/src/config/section/infra/hosts.js
@@ -41,7 +41,7 @@ export default {
fields.push('zonename')
return fields
},
- details: ['name', 'id', 'resourcestate', 'ipaddress', 'hypervisor', 'type', 'clustername', 'podname', 'zonename', 'disconnected', 'created'],
+ details: ['name', 'id', 'resourcestate', 'ipaddress', 'hypervisor', 'arch', 'type', 'clustername', 'podname', 'zonename', 'disconnected', 'created'],
tabs: [{
name: 'details',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
diff --git a/ui/src/config/section/infra/zones.js b/ui/src/config/section/infra/zones.js
index 929add5210d..25535979e4d 100644
--- a/ui/src/config/section/infra/zones.js
+++ b/ui/src/config/section/infra/zones.js
@@ -34,7 +34,7 @@ export default {
fields.push('order')
return fields
},
- details: ['name', 'id', 'allocationstate', 'type', 'networktype', 'guestcidraddress', 'localstorageenabled', 'securitygroupsenabled', 'dns1', 'dns2', 'internaldns1', 'internaldns2'],
+ details: ['name', 'id', 'allocationstate', 'type', 'networktype', 'guestcidraddress', 'localstorageenabled', 'securitygroupsenabled', 'dns1', 'dns2', 'internaldns1', 'internaldns2', 'asnrange'],
related: [{
name: 'pod',
title: 'label.pods',
@@ -72,6 +72,15 @@ export default {
}, {
name: 'physical.network',
component: shallowRef(defineAsyncComponent(() => import('@/views/infra/zone/PhysicalNetworksTab.vue')))
+ }, {
+ name: 'ipv4.subnets',
+ component: shallowRef(defineAsyncComponent(() => import('@/views/infra/zone/Ipv4GuestSubnetsTab.vue')))
+ }, {
+ name: 'asnumber',
+ component: shallowRef(defineAsyncComponent(() => import('@/views/infra/zone/AsNumbersTab.vue')))
+ }, {
+ name: 'bgp.peers',
+ component: shallowRef(defineAsyncComponent(() => import('@/views/infra/zone/BgpPeersTab.vue')))
}, {
name: 'system.vms',
component: shallowRef(defineAsyncComponent(() => import('@/views/infra/zone/SystemVmsTab.vue'))),
diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js
index a4a598103e3..79eae34f537 100644
--- a/ui/src/config/section/network.js
+++ b/ui/src/config/section/network.js
@@ -47,14 +47,14 @@ export default {
return fields
},
details: () => {
- var fields = ['name', 'id', 'description', 'type', 'traffictype', 'vpcid', 'vlan', 'broadcasturi', 'cidr', 'ip6cidr', 'netmask', 'gateway', 'aclname', 'ispersistent', 'restartrequired', 'reservediprange', 'redundantrouter', 'networkdomain', 'egressdefaultpolicy', 'zonename', 'account', 'domainpath', 'associatednetwork', 'associatednetworkid', 'ip6firewall', 'ip6routing', 'ip6routes', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu', 'privatemtu']
+ var fields = ['name', 'id', 'description', 'type', 'traffictype', 'vpcid', 'vlan', 'broadcasturi', 'cidr', 'ip6cidr', 'netmask', 'gateway', 'asnumber', 'aclname', 'ispersistent', 'restartrequired', 'reservediprange', 'redundantrouter', 'networkdomain', 'egressdefaultpolicy', 'zonename', 'account', 'domainpath', 'associatednetwork', 'associatednetworkid', 'ip4routing', 'ip6routing', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu', 'privatemtu']
if (!isAdmin()) {
fields = fields.filter(function (e) { return e !== 'broadcasturi' })
}
return fields
},
filters: ['all', 'account', 'domainpath', 'shared'],
- searchFilters: ['keyword', 'zoneid', 'domainid', 'account', 'type', 'tags'],
+ searchFilters: ['keyword', 'zoneid', 'domainid', 'account', 'type', 'restartrequired', 'tags'],
related: [{
name: 'vm',
title: 'label.instances',
@@ -66,7 +66,15 @@ export default {
}, {
name: 'egress.rules',
component: shallowRef(defineAsyncComponent(() => import('@/views/network/EgressRulesTab.vue'))),
- show: (record, route, user) => { return record.type === 'Isolated' && !('vpcname' in record) && 'listEgressFirewallRules' in store.getters.apis && (['Admin', 'DomainAdmin'].includes(user.roletype) || record.account === user.account || record.projectid) }
+ show: (record, route, user) => { return record.type === 'Isolated' && !record.ip4routing && !('vpcname' in record) && 'listEgressFirewallRules' in store.getters.apis && (['Admin', 'DomainAdmin'].includes(user.roletype) || record.account === user.account || record.projectid) }
+ }, {
+ name: 'bgp.peers',
+ component: shallowRef(defineAsyncComponent(() => import('@/views/infra/zone/BgpPeersTab.vue'))),
+ show: (record, route, user) => { return !record.vpcid && ['Admin'].includes(user.roletype) && record.ip4routing === 'Dynamic' }
+ }, {
+ name: 'routing.firewall',
+ component: shallowRef(defineAsyncComponent(() => import('@/views/network/RoutingFirewallRulesTab.vue'))),
+ show: (record, route, user) => { return record.type === 'Isolated' && record.ip4routing && !('vpcname' in record) && 'listRoutingFirewallRules' in store.getters.apis && (['Admin', 'DomainAdmin'].includes(user.roletype) || record.account === user.account || record.projectid) }
}, {
name: 'ip.v6.firewall',
component: shallowRef(defineAsyncComponent(() => import('@/views/network/Ipv6FirewallRulesTab.vue'))),
@@ -74,7 +82,7 @@ export default {
}, {
name: (record) => { return record.type === 'Shared' ? 'ip.addresses' : 'public.ip.addresses' },
component: shallowRef(defineAsyncComponent(() => import('@/views/network/IpAddressesTab.vue'))),
- show: (record, route, user) => { return 'listPublicIpAddresses' in store.getters.apis && (record.type === 'Shared' || (record.type === 'Isolated' && !('vpcname' in record) && (['Admin', 'DomainAdmin'].includes(user.roletype) || record.account === user.account || record.projectid))) }
+ show: (record, route, user) => { return 'listPublicIpAddresses' in store.getters.apis && (record.type === 'Shared' || (record.type === 'Isolated' && !record.ip4routing && !('vpcname' in record) && (['Admin', 'DomainAdmin'].includes(user.roletype) || record.account === user.account || record.projectid))) }
}, {
name: 'virtual.routers',
component: shallowRef(defineAsyncComponent(() => import('@/views/network/RoutersTab.vue'))),
@@ -217,8 +225,8 @@ export default {
fields.push(...['domain', 'zonename'])
return fields
},
- details: ['name', 'id', 'displaytext', 'cidr', 'networkdomain', 'ip6routes', 'ispersistent', 'redundantvpcrouter', 'restartrequired', 'zonename', 'account', 'domain', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu'],
- searchFilters: ['name', 'zoneid', 'domainid', 'account', 'tags'],
+ details: ['name', 'id', 'displaytext', 'cidr', 'networkdomain', 'ip4routing', 'ip4routes', 'ip6routes', 'ispersistent', 'redundantvpcrouter', 'restartrequired', 'zonename', 'account', 'domain', 'dns1', 'dns2', 'ip6dns1', 'ip6dns2', 'publicmtu'],
+ searchFilters: ['name', 'zoneid', 'domainid', 'account', 'restartrequired', 'tags'],
related: [{
name: 'vm',
title: 'label.instances',
@@ -307,10 +315,7 @@ export default {
return false
}
const listZoneHaveSGEnabled = store.getters.zones.filter(zone => zone.securitygroupsenabled === true)
- if (!listZoneHaveSGEnabled || listZoneHaveSGEnabled.length === 0) {
- return false
- }
- return true
+ return (listZoneHaveSGEnabled && listZoneHaveSGEnabled.length > 0) || store.getters.showSecurityGroups
},
actions: [
{
@@ -861,6 +866,47 @@ export default {
}
]
},
+ {
+ name: 'asnumbers',
+ title: 'label.asnumbers',
+ icon: 'partition-outlined',
+ permission: ['listASNumbers'],
+ show: () => {
+ return ['Admin'].includes(store.getters.userInfo.roletype)
+ },
+ filters: ['all', 'allocatedonly', 'free'],
+ columns: ['asnumber', 'allocationstate', 'asnrange', 'associatednetworkname', 'vpcname', 'allocated', 'account', 'domain', 'zonename'],
+ searchFilters: ['zoneid', 'associatednetworkid', 'account', 'domainid'],
+ resourceType: 'ASNumber',
+ actions: [
+ {
+ api: 'releaseASNumber',
+ icon: 'delete-outlined',
+ label: 'label.action.release.asnumber',
+ message: 'message.action.release.asnumber',
+ show: (record) => { return record.allocationstate === 'Allocated' },
+ args: ['zoneid', 'asnumber'],
+ mapping: {
+ zoneid: {
+ value: (record) => { return record.zoneid }
+ },
+ asnumber: {
+ value: (record) => { return record.asnumber }
+ }
+ },
+ dataView: true,
+ groupAction: true,
+ popup: true,
+ groupShow: (selectedItems, storegetters) => {
+ return selectedItems.length === 1 && selectedItems[0].allocationstate === 'Allocated'
+ },
+ groupMap: (selectedId, values, records) => {
+ const record = records.filter(x => { return x.id === selectedId[0] })
+ return record
+ }
+ }
+ ]
+ },
{
name: 'privategw',
title: 'label.private.gateway',
@@ -1370,7 +1416,7 @@ export default {
permission: ['listGuestVlans'],
resourceType: 'GuestVlan',
filters: ['allocatedonly', 'all'],
- columns: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'zonename'],
+ columns: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'zonename', 'guest.networks'],
details: ['vlan', 'allocationstate', 'physicalnetworkname', 'taken', 'account', 'project', 'domain', 'isdedicated', 'zonename'],
searchFilters: ['zoneid'],
tabs: [{
@@ -1387,6 +1433,42 @@ export default {
}
return true
}
+ },
+ {
+ name: 'ipv4subnets',
+ title: 'label.ipv4.subnets',
+ icon: 'pic-center-outlined',
+ permission: ['listIpv4SubnetsForGuestNetwork'],
+ columns: ['subnet', 'zonename', 'parentsubnet', 'networkname', 'vpcname', 'created', 'allocated'],
+ details: ['subnet', 'zonename', 'zoneid', 'parentsubnet', 'networkname', 'networkid', 'vpcname', 'vpcid', 'created', 'allocated', 'state'],
+ searchFilters: ['zoneid'],
+ show: () => {
+ if (!store.getters.zones || store.getters.zones.length === 0) {
+ return false
+ }
+ return isAdmin()
+ },
+ actions: [
+ {
+ api: 'createIpv4SubnetForGuestNetwork',
+ icon: 'plus-outlined',
+ label: 'label.add.ipv4.subnet',
+ listView: true,
+ popup: true,
+ component: shallowRef(defineAsyncComponent(() => import('@/views/network/CreateIpv4SubnetForNetwork.vue')))
+ },
+ {
+ api: 'deleteIpv4SubnetForGuestNetwork',
+ icon: 'delete-outlined',
+ label: 'label.delete.ipv4.subnet',
+ message: 'message.action.delete.ipv4.subnet',
+ dataView: true,
+ show: (record) => { return !record.networkid },
+ groupAction: true,
+ popup: true,
+ groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
+ }
+ ]
}
]
}
diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js
index 37973344f65..e542f3d8df3 100644
--- a/ui/src/config/section/offering.js
+++ b/ui/src/config/section/offering.js
@@ -375,7 +375,7 @@ export default {
permission: ['listNetworkOfferings'],
searchFilters: ['name', 'zoneid', 'domainid', 'tags'],
columns: ['name', 'state', 'guestiptype', 'traffictype', 'networkrate', 'domain', 'zone', 'order'],
- details: ['name', 'id', 'displaytext', 'guestiptype', 'traffictype', 'internetprotocol', 'networkrate', 'ispersistent', 'egressdefaultpolicy', 'availability', 'conservemode', 'specifyvlan', 'specifyipranges', 'supportspublicaccess', 'supportsstrechedl2subnet', 'forvpc', 'fornsx', 'nsxmode', 'service', 'tags', 'domain', 'zone'],
+ details: ['name', 'id', 'displaytext', 'guestiptype', 'traffictype', 'internetprotocol', 'networkrate', 'ispersistent', 'egressdefaultpolicy', 'availability', 'conservemode', 'specifyvlan', 'routingmode', 'specifyasnumber', 'specifyipranges', 'supportspublicaccess', 'supportsstrechedl2subnet', 'forvpc', 'fornsx', 'networkmode', 'service', 'tags', 'domain', 'zone'],
resourceType: 'NetworkOffering',
tabs: [
{
@@ -475,7 +475,7 @@ export default {
searchFilters: ['name', 'zoneid', 'domainid'],
resourceType: 'VpcOffering',
columns: ['name', 'state', 'displaytext', 'domain', 'zone', 'order'],
- details: ['name', 'id', 'displaytext', 'internetprotocol', 'distributedvpcrouter', 'tags', 'service', 'fornsx', 'nsxmode', 'domain', 'zone', 'created'],
+ details: ['name', 'id', 'displaytext', 'internetprotocol', 'distributedvpcrouter', 'tags', 'routingmode', 'specifyasnumber', 'service', 'fornsx', 'networkmode', 'domain', 'zone', 'created'],
related: [{
name: 'vpc',
title: 'label.vpc',
diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js
index b4debc83a85..66c6cd4d0d1 100644
--- a/ui/src/config/section/storage.js
+++ b/ui/src/config/section/storage.js
@@ -432,7 +432,7 @@ export default {
title: 'label.backup',
icon: 'cloud-upload-outlined',
permission: ['listBackups'],
- columns: [{ name: (record) => { return record.virtualmachinename } }, 'status', 'virtualmachinename', 'type', 'created', 'account', 'domain', 'zone'],
+ columns: [{ name: (record) => { return record.virtualmachinename } }, 'status', 'size', 'virtualsize', 'type', 'created', 'account', 'domain', 'zone'],
details: ['virtualmachinename', 'id', 'type', 'externalid', 'size', 'virtualsize', 'volumes', 'backupofferingname', 'zone', 'account', 'domain', 'created'],
actions: [
{
@@ -543,6 +543,149 @@ export default {
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
+ },
+ {
+ name: 'sharedfs',
+ title: 'label.sharedfs',
+ icon: 'file-text-outlined',
+ permission: ['listSharedFileSystems'],
+ resourceType: 'SharedFS',
+ columns: () => {
+ const fields = ['name', 'state', 'sizegb']
+ const metricsFields = ['diskkbsread', 'diskkbswrite', 'utilization', 'physicalsize']
+
+ if (store.getters.metrics) {
+ fields.push(...metricsFields)
+ }
+ if (store.getters.userInfo.roletype === 'Admin') {
+ fields.push('storage')
+ fields.push('account')
+ } else if (store.getters.userInfo.roletype === 'DomainAdmin') {
+ fields.push('account')
+ }
+ if (store.getters.listAllProjects) {
+ fields.push('project')
+ }
+ fields.push('zonename')
+
+ return fields
+ },
+ details: ['id', 'name', 'description', 'state', 'filesystem', 'diskofferingdisplaytext', 'ipaddress', 'sizegb', 'provider', 'protocol', 'provisioningtype', 'utilization', 'diskkbsread', 'diskkbswrite', 'diskioread', 'diskiowrite', 'account', 'domain', 'created'],
+ tabs: [{
+ component: shallowRef(defineAsyncComponent(() => import('@/views/storage/SharedFSTab.vue')))
+ }],
+ searchFilters: () => {
+ var filters = ['name', 'zoneid', 'domainid', 'account', 'networkid', 'serviceofferingid', 'diskofferingid']
+ return filters
+ },
+ actions: [
+ {
+ api: 'createSharedFileSystem',
+ icon: 'plus-outlined',
+ docHelp: 'adminguide/storage.html#creating-a-new-file-share',
+ label: 'label.create.sharedfs',
+ listView: true,
+ popup: true,
+ component: shallowRef(defineAsyncComponent(() => import('@/views/storage/CreateSharedFS.vue')))
+ },
+ {
+ api: 'updateSharedFileSystem',
+ icon: 'edit-outlined',
+ docHelp: 'adminguide/storage.html#lifecycle-operations',
+ label: 'label.update.sharedfs',
+ dataView: true,
+ popup: true,
+ component: shallowRef(defineAsyncComponent(() => import('@/views/storage/UpdateSharedFS.vue')))
+ },
+ {
+ api: 'startSharedFileSystem',
+ icon: 'caret-right-outlined',
+ label: 'label.action.start.sharedfs',
+ message: 'message.action.start.sharedfs',
+ docHelp: 'adminguide/storage.html#lifecycle-operations',
+ dataView: true,
+ popup: true,
+ groupAction: true,
+ groupMap: (selection) => { return selection.map(x => { return { id: x } }) },
+ show: (record) => { return ['Stopped'].includes(record.state) }
+ },
+ {
+ api: 'stopSharedFileSystem',
+ icon: 'poweroff-outlined',
+ label: 'label.action.stop.sharedfs',
+ message: 'message.action.stop.sharedfs',
+ docHelp: 'adminguide/storage.html#lifecycle-operations',
+ dataView: true,
+ popup: true,
+ groupAction: true,
+ groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) },
+ args: ['forced'],
+ show: (record) => { return ['Ready'].includes(record.state) }
+ },
+ {
+ api: 'restartSharedFileSystem',
+ icon: 'reload-outlined',
+ docHelp: 'adminguide/storage.html#lifecycle-operations',
+ label: 'label.action.restart.sharedfs',
+ message: 'message.action.restart.sharedfs',
+ dataView: true,
+ popup: true,
+ args: ['cleanup'],
+ show: (record) => { return ['Stopped', 'Ready', 'Detached'].includes(record.state) }
+ },
+ {
+ api: 'changeSharedFileSystemDiskOffering',
+ icon: 'swap-outlined',
+ docHelp: 'adminguide/storage.html#lifecycle-operations',
+ label: 'label.change.disk.offering',
+ dataView: true,
+ popup: true,
+ component: shallowRef(defineAsyncComponent(() => import('@/views/storage/ChangeSharedFSDiskOffering.vue'))),
+ show: (record) => { return ['Stopped', 'Ready'].includes(record.state) }
+ },
+ {
+ api: 'changeSharedFileSystemServiceOffering',
+ icon: 'arrows-alt-outlined',
+ docHelp: 'adminguide/storage.html#lifecycle-operations',
+ label: 'label.change.service.offering',
+ dataView: true,
+ popup: true,
+ component: shallowRef(defineAsyncComponent(() => import('@/views/storage/ChangeSharedFSServiceOffering.vue'))),
+ show: (record) => { return ['Stopped'].includes(record.state) }
+ },
+ {
+ api: 'destroySharedFileSystem',
+ icon: 'delete-outlined',
+ docHelp: 'adminguide/storage.html#lifecycle-operations',
+ label: 'label.destroy.sharedfs',
+ message: 'message.action.destroy.sharedfs',
+ dataView: true,
+ popup: true,
+ groupAction: true,
+ groupMap: (selection, values) => { return selection.map(x => { return { id: x, expunge: values.expunge, forced: values.forced } }) },
+ args: ['expunge', 'forced'],
+ show: (record) => { return !['Destroyed', 'Expunging', 'Error'].includes(record.state) }
+ },
+ {
+ api: 'recoverSharedFileSystem',
+ icon: 'medicine-box-outlined',
+ docHelp: 'adminguide/storage.html#lifecycle-operations',
+ label: 'label.recover.sharedfs',
+ message: 'message.action.recover.sharedfs',
+ dataView: true,
+ show: (record) => { return record.state === 'Destroyed' }
+ },
+ {
+ api: 'expungeSharedFileSystem',
+ icon: 'delete-outlined',
+ docHelp: 'adminguide/storage.html#lifecycle-operations',
+ label: 'label.expunge.sharedfs',
+ message: 'message.action.expunge.sharedfs',
+ dataView: true,
+ popup: true,
+ show: (record) => { return ['Destroyed', 'Expunging', 'Error'].includes(record.state) }
+ }
+ ]
}
]
}
diff --git a/ui/src/core/lazy_lib/icons_use.js b/ui/src/core/lazy_lib/icons_use.js
index d877fe8bf23..8595454b679 100644
--- a/ui/src/core/lazy_lib/icons_use.js
+++ b/ui/src/core/lazy_lib/icons_use.js
@@ -82,6 +82,7 @@ import {
FieldTimeOutlined,
FileDoneOutlined,
FileProtectOutlined,
+ FileTextOutlined,
FilterOutlined,
FilterTwoTone,
FireOutlined,
@@ -128,7 +129,9 @@ import {
LaptopOutlined,
OrderedListOutlined,
PaperClipOutlined,
+ PartitionOutlined,
PauseCircleOutlined,
+ PicCenterOutlined,
PictureOutlined,
PieChartOutlined,
PlayCircleOutlined,
@@ -156,6 +159,7 @@ import {
SettingOutlined,
ShareAltOutlined,
ShoppingOutlined,
+ SplitCellsOutlined,
StopOutlined,
SwapOutlined,
SyncOutlined,
@@ -248,6 +252,7 @@ export default {
app.component('FieldTimeOutlined', FieldTimeOutlined)
app.component('FileDoneOutlined', FileDoneOutlined)
app.component('FileProtectOutlined', FileProtectOutlined)
+ app.component('FileTextOutlined', FileTextOutlined)
app.component('FilterOutlined', FilterOutlined)
app.component('FilterTwoTone', FilterTwoTone)
app.component('FireOutlined', FireOutlined)
@@ -294,7 +299,9 @@ export default {
app.component('LaptopOutlined', LaptopOutlined)
app.component('OrderedListOutlined', OrderedListOutlined)
app.component('PaperClipOutlined', PaperClipOutlined)
+ app.component('PartitionOutlined', PartitionOutlined)
app.component('PauseCircleOutlined', PauseCircleOutlined)
+ app.component('PicCenterOutlined', PicCenterOutlined)
app.component('PictureOutlined', PictureOutlined)
app.component('PieChartOutlined', PieChartOutlined)
app.component('PlayCircleOutlined', PlayCircleOutlined)
@@ -322,6 +329,7 @@ export default {
app.component('SettingOutlined', SettingOutlined)
app.component('ShareAltOutlined', ShareAltOutlined)
app.component('ShoppingOutlined', ShoppingOutlined)
+ app.component('SplitCellsOutlined', SplitCellsOutlined)
app.component('StopOutlined', StopOutlined)
app.component('SwapOutlined', SwapOutlined)
app.component('SyncOutlined', SyncOutlined)
diff --git a/ui/src/store/getters.js b/ui/src/store/getters.js
index 67b168be8c2..645d031fc3e 100644
--- a/ui/src/store/getters.js
+++ b/ui/src/store/getters.js
@@ -28,6 +28,7 @@ const getters = {
apis: state => state.user.apis,
features: state => state.user.features,
userInfo: state => state.user.info,
+ latestVersion: state => state.user.latestVersion,
addRouters: state => state.permission.addRouters,
multiTab: state => state.app.multiTab,
listAllProjects: state => state.app.listAllProjects,
@@ -35,6 +36,7 @@ const getters = {
isLdapEnabled: state => state.user.isLdapEnabled,
cloudian: state => state.user.cloudian,
zones: state => state.user.zones,
+ showSecurityGroups: state => state.user.showSecurityGroups,
timezoneoffset: state => state.user.timezoneoffset,
usebrowsertimezone: state => state.user.usebrowsertimezone,
server: state => state.app.server,
diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js
index 24302a94033..22a2fab959d 100644
--- a/ui/src/store/modules/user.js
+++ b/ui/src/store/modules/user.js
@@ -18,12 +18,15 @@
import Cookies from 'js-cookie'
import message from 'ant-design-vue/es/message'
import notification from 'ant-design-vue/es/notification'
+import semver from 'semver'
import { vueProps } from '@/vue-app'
import router from '@/router'
import store from '@/store'
import { oauthlogin, login, logout, api } from '@/api'
import { i18n } from '@/locales'
+import { axios } from '../../utils/request'
+import { getParsedVersion } from '@/utils/util'
import {
ACCESS_TOKEN,
@@ -31,6 +34,7 @@ import {
DEFAULT_THEME,
APIS,
ZONES,
+ SHOW_SECURTIY_GROUPS,
TIMEZONE_OFFSET,
USE_BROWSER_TIMEZONE,
HEADER_NOTICES,
@@ -38,7 +42,8 @@ import {
DARK_MODE,
CUSTOM_COLUMNS,
OAUTH_DOMAIN,
- OAUTH_PROVIDER
+ OAUTH_PROVIDER,
+ LATEST_CS_VERSION
} from '@/store/mutation-types'
const user = {
@@ -120,6 +125,10 @@ const user = {
state.zones = zones
vueProps.$localStorage.set(ZONES, zones)
},
+ SET_SHOW_SECURITY_GROUPS: (state, show) => {
+ state.showSecurityGroups = show
+ vueProps.$localStorage.set(SHOW_SECURTIY_GROUPS, show)
+ },
SET_DOMAIN_STORE (state, domainStore) {
state.domainStore = domainStore
vueProps.$localStorage.set(DOMAIN_STORE, domainStore)
@@ -167,6 +176,12 @@ const user = {
},
SET_OAUTH_PROVIDER_USED_TO_LOGIN: (state, provider) => {
vueProps.$localStorage.set(OAUTH_PROVIDER, provider)
+ },
+ SET_LATEST_VERSION: (state, version) => {
+ if (version?.fetchedTs > 0) {
+ vueProps.$localStorage.set(LATEST_CS_VERSION, version)
+ state.latestVersion = version
+ }
}
},
@@ -212,6 +227,8 @@ const user = {
commit('SET_2FA_PROVIDER', result.providerfor2fa)
commit('SET_2FA_ISSUER', result.issuerfor2fa)
commit('SET_LOGIN_FLAG', false)
+ const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
+ commit('SET_LATEST_VERSION', latestVersion)
notification.destroy()
resolve()
@@ -259,6 +276,8 @@ const user = {
commit('SET_2FA_PROVIDER', result.providerfor2fa)
commit('SET_2FA_ISSUER', result.issuerfor2fa)
commit('SET_LOGIN_FLAG', false)
+ const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
+ commit('SET_LATEST_VERSION', latestVersion)
notification.destroy()
resolve()
@@ -276,14 +295,18 @@ const user = {
const cachedUseBrowserTimezone = vueProps.$localStorage.get(USE_BROWSER_TIMEZONE, false)
const cachedCustomColumns = vueProps.$localStorage.get(CUSTOM_COLUMNS, {})
const domainStore = vueProps.$localStorage.get(DOMAIN_STORE, {})
+ const cachedShowSecurityGroups = vueProps.$localStorage.get(SHOW_SECURTIY_GROUPS, false)
const darkMode = vueProps.$localStorage.get(DARK_MODE, false)
+ const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
const hasAuth = Object.keys(cachedApis).length > 0
commit('SET_DOMAIN_STORE', domainStore)
commit('SET_DARK_MODE', darkMode)
+ commit('SET_LATEST_VERSION', latestVersion)
if (hasAuth) {
console.log('Login detected, using cached APIs')
commit('SET_ZONES', cachedZones)
+ commit('SET_SHOW_SECURITY_GROUPS', cachedShowSecurityGroups)
commit('SET_APIS', cachedApis)
commit('SET_TIMEZONE_OFFSET', cachedTimezoneOffset)
commit('SET_USE_BROWSER_TIMEZONE', cachedUseBrowserTimezone)
@@ -294,6 +317,7 @@ const user = {
const result = response.listusersresponse.user[0]
commit('SET_INFO', result)
commit('SET_NAME', result.firstname + ' ' + result.lastname)
+ store.dispatch('SetCsLatestVersion', result.rolename)
resolve(cachedApis)
}).catch(error => {
reject(error)
@@ -332,16 +356,54 @@ const user = {
}).catch(error => {
reject(error)
})
+
+ api('listNetworks', { restartrequired: true, forvpc: false }).then(response => {
+ if (response.listnetworksresponse.count > 0) {
+ store.dispatch('AddHeaderNotice', {
+ key: 'NETWORK_RESTART_REQUIRED',
+ title: i18n.global.t('label.network.restart.required'),
+ description: i18n.global.t('message.network.restart.required'),
+ path: '/guestnetwork/',
+ query: { restartrequired: true, forvpc: false },
+ status: 'done',
+ timestamp: new Date()
+ })
+ }
+ }).catch(ignored => {})
+
+ api('listVPCs', { restartrequired: true }).then(response => {
+ if (response.listvpcsresponse.count > 0) {
+ store.dispatch('AddHeaderNotice', {
+ key: 'VPC_RESTART_REQUIRED',
+ title: i18n.global.t('label.vpc.restart.required'),
+ description: i18n.global.t('message.vpc.restart.required'),
+ path: '/vpc/',
+ query: { restartrequired: true },
+ status: 'done',
+ timestamp: new Date()
+ })
+ }
+ }).catch(ignored => {})
}
api('listUsers', { username: Cookies.get('username') }).then(response => {
const result = response.listusersresponse.user[0]
commit('SET_INFO', result)
commit('SET_NAME', result.firstname + ' ' + result.lastname)
+ store.dispatch('SetCsLatestVersion', result.rolename)
}).catch(error => {
reject(error)
})
+ api(
+ 'listNetworkServiceProviders',
+ { name: 'SecurityGroupProvider', state: 'Enabled' }
+ ).then(response => {
+ const showSecurityGroups = response.listnetworkserviceprovidersresponse.count > 0
+ commit('SET_SHOW_SECURITY_GROUPS', showSecurityGroups)
+ }).catch(ignored => {
+ })
+
api('listCapabilities').then(response => {
const result = response.listcapabilitiesresponse.capability
commit('SET_FEATURES', result)
@@ -351,6 +413,9 @@ const user = {
if (result && result.customhypervisordisplayname) {
commit('SET_CUSTOM_HYPERVISOR_NAME', result.customhypervisordisplayname)
}
+ if (result && result.securitygroupsenabled) {
+ commit('SET_SHOW_SECURITY_GROUPS', result.securitygroupsenabled)
+ }
}).catch(error => {
reject(error)
})
@@ -367,6 +432,8 @@ const user = {
commit('SET_CLOUDIAN', cloudian)
}).catch(ignored => {
})
+ }).catch(error => {
+ console.error(error)
})
},
@@ -488,6 +555,29 @@ const user = {
SetDomainStore ({ commit }, domainStore) {
commit('SET_DOMAIN_STORE', domainStore)
},
+ SetCsLatestVersion ({ commit }, rolename) {
+ const lastFetchTs = store.getters.latestVersion?.fetchedTs ? store.getters.latestVersion.fetchedTs : 0
+ if (rolename === 'Root Admin' && (+new Date() - lastFetchTs) > 24 * 60 * 60 * 1000) {
+ axios.get(
+ 'https://api.github.com/repos/apache/cloudstack/releases'
+ ).then(response => {
+ let latestReleaseVersion = getParsedVersion(response[0].tag_name)
+ let latestTag = response[0].tag_name
+
+ for (const release of response) {
+ if (release.tag_name.toLowerCase().includes('rc')) {
+ continue
+ }
+ const parsedVersion = getParsedVersion(release.tag_name)
+ if (semver.gte(parsedVersion, latestReleaseVersion)) {
+ latestReleaseVersion = parsedVersion
+ latestTag = release.tag_name
+ commit('SET_LATEST_VERSION', { version: latestTag, fetchedTs: (+new Date()) })
+ }
+ }
+ }).catch(ignored => {})
+ }
+ },
SetDarkMode ({ commit }, darkMode) {
commit('SET_DARK_MODE', darkMode)
},
diff --git a/ui/src/store/mutation-types.js b/ui/src/store/mutation-types.js
index 77aeb8fb7b6..94f28beabdf 100644
--- a/ui/src/store/mutation-types.js
+++ b/ui/src/store/mutation-types.js
@@ -29,12 +29,14 @@ export const DEFAULT_CONTENT_WIDTH_TYPE = 'DEFAULT_CONTENT_WIDTH_TYPE'
export const DEFAULT_MULTI_TAB = 'DEFAULT_MULTI_TAB'
export const APIS = 'APIS'
export const ZONES = 'ZONES'
+export const SHOW_SECURTIY_GROUPS = 'SHOW_SECURITY_GROUPS'
export const HEADER_NOTICES = 'HEADER_NOTICES'
export const TIMEZONE_OFFSET = 'TIMEZONE_OFFSET'
export const USE_BROWSER_TIMEZONE = 'USE_BROWSER_TIMEZONE'
export const SERVER_MANAGER = 'SERVER_MANAGER'
export const DOMAIN_STORE = 'DOMAIN_STORE'
export const DARK_MODE = 'DARK_MODE'
+export const LATEST_CS_VERSION = 'LATEST_CS_VERSION'
export const VUE_VERSION = 'VUE_VERSION'
export const CUSTOM_COLUMNS = 'CUSTOM_COLUMNS'
export const RELOAD_ALL_PROJECTS = 'RELOAD_ALL_PROJECTS'
diff --git a/ui/src/utils/plugins.js b/ui/src/utils/plugins.js
index 87ad11fbd46..353fe2ac052 100644
--- a/ui/src/utils/plugins.js
+++ b/ui/src/utils/plugins.js
@@ -289,6 +289,13 @@ export const notifierPlugin = {
close: (key) => notification.close(key),
destroy: () => notification.destroy()
}
+
+ app.config.globalProperties.$messageConfigSuccess = function (msg, configrecord) {
+ if (configrecord.isdynamic) {
+ msg += `. ${this.$t('message.setting.update.delay')}`
+ }
+ message.success(msg)
+ }
}
}
@@ -495,7 +502,7 @@ export const genericUtilPlugin = {
if (isBase64(text)) {
return text
}
- return encodeURIComponent(btoa(unescape(encodeURIComponent(text))))
+ return encodeURI(btoa(unescape(encodeURIComponent(text))))
}
}
}
diff --git a/ui/src/utils/util.js b/ui/src/utils/util.js
index fad4d1e0f5d..8773f07446e 100644
--- a/ui/src/utils/util.js
+++ b/ui/src/utils/util.js
@@ -15,6 +15,8 @@
// specific language governing permissions and limitations
// under the License.
+import semver from 'semver'
+
export function timeFix () {
const time = new Date()
const hour = time.getHours()
@@ -69,6 +71,14 @@ export function sanitizeReverse (value) {
.replace(/>/g, '>')
}
+export function getParsedVersion (version) {
+ version = version.split('-')[0]
+ if (semver.valid(version) === null) {
+ version = version.split('.').slice(1).join('.')
+ }
+ return version
+}
+
export function toCsv ({ keys = null, data = null, columnDelimiter = ',', lineDelimiter = '\n' }) {
if (data === null || !data.length) {
return null
diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue
index 13bb35cbf1e..bfdde77191e 100644
--- a/ui/src/views/AutogenView.vue
+++ b/ui/src/views/AutogenView.vue
@@ -51,7 +51,7 @@
@@ -67,7 +67,7 @@
+
+
+ {{ $toLocaleDate(text) }}
+
+