+
{{ dataResource[item].join(', ') }}
{{ dataResource[item] }}
@@ -263,6 +266,7 @@ import ObjectListTable from '@/components/view/ObjectListTable'
import ExternalConfigurationDetails from '@/views/extension/ExternalConfigurationDetails'
import TooltipButton from '@/components/widgets/TooltipButton'
import { genericCompare } from '@/utils/sort'
+import { resourceAlertLabel } from '@/views/resourcealert/resourceAlertOptions'
import CodeHighlight from 'vue-code-highlight/src/CodeHighlight.vue'
import 'vue-code-highlight/themes/prism-okaidia.css'
@@ -466,6 +470,7 @@ export default {
}
},
methods: {
+ resourceAlertLabel,
decodeUserData (userdata) {
const decodedData = Buffer.from(userdata, 'base64')
return decodedData.toString('utf-8')
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue
index 9a7d874fef3e..a928fc492a6a 100644
--- a/ui/src/components/view/ListView.vue
+++ b/ui/src/components/view/ListView.vue
@@ -1222,7 +1222,7 @@ export default {
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering',
'/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', 'webhookfilters', '/quotatariff', '/sharedfs',
'/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule',
- '/kmskey', '/hsmprofile', '/dnsserver', '/dnszone'].join('|'))
+ '/kmskey', '/hsmprofile', '/dnsserver', '/dnszone', '/resourcealerts'].join('|'))
.test(this.$route.path)
},
enableGroupAction () {
@@ -1231,7 +1231,8 @@ export default {
'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering',
'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment', 'buckets',
'webhook', 'webhookdeliveries', 'sharedfs', 'ipv4subnets', 'asnumbers', 'guestos', 'gpucard', 'gpudevices', 'vgpuprofile',
- 'quotatariff'].includes(this.$route.name)
+ 'quotatariff', 'resourcealerts'
+ ].includes(this.$route.name)
},
getDateAtTimeZone (date, timezone) {
return date ? moment(date).tz(timezone).format('YYYY-MM-DD HH:mm:ss') : null
diff --git a/ui/src/components/view/ResourceAlertsTab.vue b/ui/src/components/view/ResourceAlertsTab.vue
new file mode 100644
index 000000000000..e35351e853ca
--- /dev/null
+++ b/ui/src/components/view/ResourceAlertsTab.vue
@@ -0,0 +1,159 @@
+// 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.
+
+
+
+
+ {{ $t('label.severity') }}: {{ $t('label.all') }}
+ {{ s }}
+
+
+
+
+ {{ $toLocaleDate(text) }}
+
+
+ {{ Number(text).toFixed(2) }}
+
+
+ {{ resourceAlertLabel('metrictype', text) }}
+
+
+ {{ text }}
+
+
+
+
+
+
+
diff --git a/ui/src/components/view/SearchView.vue b/ui/src/components/view/SearchView.vue
index 7cd5db96f483..b58c2b54b190 100644
--- a/ui/src/components/view/SearchView.vue
+++ b/ui/src/components/view/SearchView.vue
@@ -172,6 +172,7 @@ import TooltipButton from '@/components/widgets/TooltipButton'
import ResourceIcon from '@/components/view/ResourceIcon'
import Status from '@/components/widgets/Status'
import { i18n } from '@/locales'
+import { RESOURCE_TYPE_LABELS } from '@/views/resourcealert/resourceAlertOptions'
export default {
name: 'SearchView',
@@ -342,6 +343,8 @@ export default {
type = 'list'
} else if (item === 'tags') {
type = 'tag'
+ } else if (item === 'resourcetype' && this.isResourceAlertsView()) {
+ type = 'list'
} else if (['resourcetype', 'apikeyaccess'].includes(item)) {
type = 'autocomplete'
} else if (item === 'isencrypted') {
@@ -469,6 +472,9 @@ export default {
{ value: 'Volume' },
{ value: 'QuotaTariff' }
]
+ if (this.isResourceAlertsView()) {
+ this.fields[resourceTypeIndex].opts = Object.entries(RESOURCE_TYPE_LABELS).map(([id, name]) => ({ id, name }))
+ }
this.fields[resourceTypeIndex].loading = false
}
@@ -929,6 +935,9 @@ export default {
}
})
},
+ isResourceAlertsView () {
+ return this.$route.path.startsWith('/resourcealerts')
+ },
initFormFieldData () {
const arrayField = this.initFields()
diff --git a/ui/src/config/router.js b/ui/src/config/router.js
index b9c60bcd0c21..30b468764b69 100644
--- a/ui/src/config/router.js
+++ b/ui/src/config/router.js
@@ -43,6 +43,7 @@ import config from '@/config/section/config'
import extension from '@/config/section/extension'
import customaction from '@/config/section/extension/customaction'
import tools from '@/config/section/tools'
+import monitoring from '@/config/section/monitoring'
import quota from '@/config/section/plugin/quota'
import cloudian from '@/config/section/plugin/cloudian'
@@ -229,6 +230,7 @@ export function asyncRouterMap () {
generateRouterMap(account),
generateRouterMap(domain),
generateRouterMap(infra),
+ generateRouterMap(monitoring),
generateRouterMap(zone),
generateRouterMap(offering),
generateRouterMap(config),
diff --git a/ui/src/config/section/infra/hosts.js b/ui/src/config/section/infra/hosts.js
index 046f120dd37a..ce2e356dba3c 100644
--- a/ui/src/config/section/infra/hosts.js
+++ b/ui/src/config/section/infra/hosts.js
@@ -53,6 +53,11 @@ export default {
name: 'gpu',
resourceType: 'Host',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/GPUTab.vue')))
+ }, {
+ name: 'resourcealerts',
+ resourceType: 'Host',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceAlertsTab.vue'))),
+ show: () => { return 'listResourceAlerts' in store.getters.apis }
}, {
name: 'events',
resourceType: 'Host',
diff --git a/ui/src/config/section/infra/primaryStorages.js b/ui/src/config/section/infra/primaryStorages.js
index f127a0853b9e..bb7bd3fde703 100644
--- a/ui/src/config/section/infra/primaryStorages.js
+++ b/ui/src/config/section/infra/primaryStorages.js
@@ -66,6 +66,11 @@ export default {
name: 'browser',
resourceType: 'PrimaryStorage',
component: shallowRef(defineAsyncComponent(() => import('@/views/infra/StorageBrowser.vue')))
+ }, {
+ name: 'resourcealerts',
+ resourceType: 'StoragePool',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceAlertsTab.vue'))),
+ show: () => { return 'listResourceAlerts' in store.getters.apis }
}, {
name: 'events',
resourceType: 'StoragePool',
diff --git a/ui/src/config/section/infra/resourceAlertRules.js b/ui/src/config/section/infra/resourceAlertRules.js
new file mode 100644
index 000000000000..77a810ed647b
--- /dev/null
+++ b/ui/src/config/section/infra/resourceAlertRules.js
@@ -0,0 +1,98 @@
+// 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.
+
+import { shallowRef, defineAsyncComponent } from 'vue'
+import store from '@/store'
+import { i18n } from '@/locales'
+import { resourceAlertLabel } from '@/views/resourcealert/resourceAlertOptions'
+
+const label = (field) => ({ [field]: (record) => resourceAlertLabel(field, record[field]) })
+
+export default {
+ name: 'resourcealerts',
+ title: 'label.resource.alert.rules',
+ icon: 'BellOutlined',
+ permission: ['listResourceAlertRules'],
+ columns: () => {
+ const cols = ['name', label('resourcetype'), {
+ resourcename: (record) => record.resourceid ? record.resourcename : i18n.global.t('label.resource.alert.all.resources')
+ }, label('metric'), label('condition'), 'threshold', label('severity'), 'state']
+ if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) cols.push('account')
+ return cols
+ },
+ details: ['name', 'id', 'resourcetype', 'resourcename', 'resourceid', 'metric', 'condition', 'threshold', 'severity', 'state', 'message', 'email', 'resetinterval', 'webhooknames', 'account', 'project', 'domain', 'created'],
+ searchFilters: ['name', 'resourcetype'],
+ tabs: [{
+ name: 'details',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
+ }, {
+ name: 'firedalerts',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceAlertsTab.vue'))),
+ show: () => { return 'listResourceAlerts' in store.getters.apis }
+ }],
+ actions: [
+ {
+ api: 'createResourceAlertRule',
+ icon: 'plus-outlined',
+ label: 'label.create.resource.alert.rule',
+ listView: true,
+ popup: true,
+ component: shallowRef(defineAsyncComponent(() => import('@/views/resourcealert/CreateResourceAlertRule.vue')))
+ },
+ {
+ api: 'updateResourceAlertRule',
+ icon: 'edit-outlined',
+ label: 'label.edit',
+ dataView: true,
+ popup: true,
+ component: shallowRef(defineAsyncComponent(() => import('@/views/resourcealert/EditResourceAlertRule.vue')))
+ },
+ {
+ api: 'updateResourceAlertRule',
+ icon: 'play-circle-outlined',
+ label: 'label.enable.resource.alert.rule',
+ message: 'message.confirm.enable.resource.alert.rule',
+ dataView: true,
+ groupAction: true,
+ popup: true,
+ defaultArgs: { state: 'Enabled' },
+ groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Enabled' } }) },
+ show: (record) => { return record.state === 'Disabled' }
+ },
+ {
+ api: 'updateResourceAlertRule',
+ icon: 'pause-circle-outlined',
+ label: 'label.disable.resource.alert.rule',
+ message: 'message.confirm.disable.resource.alert.rule',
+ dataView: true,
+ groupAction: true,
+ popup: true,
+ defaultArgs: { state: 'Disabled' },
+ groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Disabled' } }) },
+ show: (record) => { return record.state !== 'Disabled' }
+ },
+ {
+ api: 'deleteResourceAlertRule',
+ icon: 'delete-outlined',
+ label: 'label.delete',
+ message: 'message.confirm.delete.resource.alert.rule',
+ dataView: true,
+ groupAction: true,
+ groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
+ }
+ ]
+}
diff --git a/ui/src/config/section/monitoring.js b/ui/src/config/section/monitoring.js
new file mode 100644
index 000000000000..1bade98aa104
--- /dev/null
+++ b/ui/src/config/section/monitoring.js
@@ -0,0 +1,28 @@
+// 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.
+
+import resourceAlerts from '@/config/section/infra/resourceAlertRules'
+
+export default {
+ name: 'monitoring',
+ title: 'label.monitoring',
+ icon: 'BarChartOutlined',
+ permission: ['listResourceAlertRules'],
+ children: [
+ resourceAlerts
+ ]
+}
diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js
index 75bdfd4d5fa6..d0ea953569e7 100644
--- a/ui/src/config/section/storage.js
+++ b/ui/src/config/section/storage.js
@@ -81,6 +81,12 @@ export default {
component: shallowRef(defineAsyncComponent(() => import('@/components/view/StatsTab.vue'))),
show: (record) => { return store.getters.features.instancesdisksstatsretentionenabled }
},
+ {
+ name: 'resourcealerts',
+ resourceType: 'Volume',
+ component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceAlertsTab.vue'))),
+ show: () => { return 'listResourceAlerts' in store.getters.apis }
+ },
{
name: 'events',
resourceType: 'Volume',
diff --git a/ui/src/views/compute/InstanceTab.vue b/ui/src/views/compute/InstanceTab.vue
index d125995e3e1c..c4c53dc91993 100644
--- a/ui/src/views/compute/InstanceTab.vue
+++ b/ui/src/views/compute/InstanceTab.vue
@@ -107,6 +107,9 @@
+
+
+
@@ -163,6 +166,7 @@ import TooltipButton from '@/components/widgets/TooltipButton'
import ResourceIcon from '@/components/view/ResourceIcon'
import AnnotationsTab from '@/components/view/AnnotationsTab'
import VolumesTab from '@/components/view/VolumesTab.vue'
+import ResourceAlertsTab from '@/components/view/ResourceAlertsTab.vue'
import SecurityGroupSelection from '@views/compute/wizard/SecurityGroupSelection'
import GPUTab from '@/components/view/GPUTab.vue'
@@ -180,6 +184,7 @@ export default {
ResourceSchedules,
ListResourceTable,
SecurityGroupSelection,
+ ResourceAlertsTab,
TooltipButton,
ResourceIcon,
AnnotationsTab,
diff --git a/ui/src/views/resourcealert/CreateResourceAlertRule.vue b/ui/src/views/resourcealert/CreateResourceAlertRule.vue
new file mode 100644
index 000000000000..8d9b072959aa
--- /dev/null
+++ b/ui/src/views/resourcealert/CreateResourceAlertRule.vue
@@ -0,0 +1,310 @@
+// 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.
+
+
+
+
+
+
+ {{ $t('label.name') }}
+
+
+
+
+ {{ $t('label.domain') }}
+
+ {{ d.path || d.name }}
+
+
+
+
+ {{ $t('label.account') }}
+
+ {{ a.name }}
+
+
+
+
+ {{ $t('label.resourcetype') }}
+
+ {{ resourceTypeLabels[rt] || rt }}
+
+
+
+
+ {{ $t('label.resource') }}
+
+ {{ $t('label.resource.alert.all.resources') }}
+ {{ r.displayname || r.name }}
+
+
+
+
+ {{ $t('label.metric') }}
+
+ {{ metricLabels[m] || m }}
+
+
+
+
+ {{ $t('label.condition') }}
+
+ {{ conditionLabels[c] || c }}
+
+
+
+
+ {{ $t('label.threshold') }}
+
+
+
+
+ {{ $t('label.severity') }}
+
+ {{ severityLabels[s] || s }}
+
+
+
+
+ {{ $t('label.message') }}
+
+
+
+
+ {{ $t('label.email') }}
+
+
+
+
+ {{ $t('label.resetinterval') }}
+
+
+
+
+ {{ $t('label.webhooks') }}
+
+ {{ wh.name }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/views/resourcealert/EditResourceAlertRule.vue b/ui/src/views/resourcealert/EditResourceAlertRule.vue
new file mode 100644
index 000000000000..6e3b57638757
--- /dev/null
+++ b/ui/src/views/resourcealert/EditResourceAlertRule.vue
@@ -0,0 +1,192 @@
+// 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.
+
+
+
+
+
+
+ {{ $t('label.name') }}
+
+
+
+
+ {{ $t('label.condition') }}
+
+ {{ conditionLabels[c] || c }}
+
+
+
+
+ {{ $t('label.threshold') }}
+
+
+
+
+ {{ $t('label.severity') }}
+
+ {{ severityLabels[s] || s }}
+
+
+
+
+ {{ $t('label.message') }}
+
+
+
+
+ {{ $t('label.email') }}
+
+
+
+
+ {{ $t('label.resetinterval') }}
+
+
+
+
+ {{ $t('label.webhooks') }}
+
+ {{ wh.name }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui/src/views/resourcealert/resourceAlertOptions.js b/ui/src/views/resourcealert/resourceAlertOptions.js
new file mode 100644
index 000000000000..82c991714193
--- /dev/null
+++ b/ui/src/views/resourcealert/resourceAlertOptions.js
@@ -0,0 +1,78 @@
+// 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.
+
+export const METRICS_BY_TYPE = {
+ VirtualMachine: ['CPU_UTILIZATION', 'MEMORY_UTILIZATION', 'DISK_READ_IOPS', 'DISK_WRITE_IOPS', 'DISK_READ_KBPS', 'DISK_WRITE_KBPS', 'NETWORK_READ_KBPS', 'NETWORK_WRITE_KBPS'],
+ Host: ['CPU_UTILIZATION', 'MEMORY_UTILIZATION', 'LOAD_AVERAGE', 'NETWORK_READ_KBPS', 'NETWORK_WRITE_KBPS'],
+ Volume: ['VOLUME_USED_GB', 'VOLUME_UTILIZATION'],
+ StoragePool: ['STORAGE_UTILIZATION', 'STORAGE_USED_IOPS']
+}
+
+export const CONDITIONS = ['GT', 'GTE', 'LT', 'LTE', 'EQ']
+
+export const SEVERITIES = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
+
+export const RESOURCE_TYPE_LABELS = {
+ VirtualMachine: 'Virtual Machine',
+ Host: 'Host',
+ Volume: 'Volume',
+ StoragePool: 'Storage Pool'
+}
+
+export const METRIC_LABELS = {
+ CPU_UTILIZATION: 'CPU Utilization %',
+ MEMORY_UTILIZATION: 'Memory Utilization %',
+ DISK_READ_IOPS: 'Disk Read IOPS',
+ DISK_WRITE_IOPS: 'Disk Write IOPS',
+ DISK_READ_KBPS: 'Disk Read KB/s',
+ DISK_WRITE_KBPS: 'Disk Write KB/s',
+ NETWORK_READ_KBPS: 'Network In KB/s',
+ NETWORK_WRITE_KBPS: 'Network Out KB/s',
+ STORAGE_UTILIZATION: 'Storage Utilization %',
+ LOAD_AVERAGE: 'Load Average',
+ VOLUME_USED_GB: 'Volume Used (GB)',
+ VOLUME_UTILIZATION: 'Volume Used %',
+ STORAGE_USED_IOPS: 'Storage Used IOPS'
+}
+
+export const CONDITION_LABELS = {
+ GT: 'Is above',
+ GTE: 'Is above or equal to',
+ LT: 'Is below',
+ LTE: 'Is below or equal to',
+ EQ: 'Equals'
+}
+
+export const SEVERITY_LABELS = {
+ CRITICAL: 'Critical',
+ HIGH: 'High',
+ MEDIUM: 'Medium',
+ LOW: 'Low'
+}
+
+const LABELS_BY_FIELD = {
+ resourcetype: RESOURCE_TYPE_LABELS,
+ metric: METRIC_LABELS,
+ metrictype: METRIC_LABELS,
+ condition: CONDITION_LABELS,
+ severity: SEVERITY_LABELS
+}
+
+export function resourceAlertLabel (field, value) {
+ const labels = LABELS_BY_FIELD[field]
+ return (labels && labels[value]) || value
+}