diff options
-rw-r--r-- | src/components/AppNavigation/AppNavigationMixin.js | 5 | ||||
-rw-r--r-- | src/locales/en-US.json | 28 | ||||
-rw-r--r-- | src/locales/ru-RU.json | 28 | ||||
-rw-r--r-- | src/router/routes.js | 9 | ||||
-rw-r--r-- | src/store/index.js | 2 | ||||
-rw-r--r-- | src/store/modules/Settings/SnmpAlertsStore.js | 121 | ||||
-rw-r--r-- | src/views/Settings/SnmpAlerts/ModalAddDestination.vue | 145 | ||||
-rw-r--r-- | src/views/Settings/SnmpAlerts/SnmpAlerts.vue | 274 | ||||
-rw-r--r-- | src/views/Settings/SnmpAlerts/index.js | 2 |
9 files changed, 614 insertions, 0 deletions
diff --git a/src/components/AppNavigation/AppNavigationMixin.js b/src/components/AppNavigation/AppNavigationMixin.js index 61230988..b33b24ba 100644 --- a/src/components/AppNavigation/AppNavigationMixin.js +++ b/src/components/AppNavigation/AppNavigationMixin.js @@ -136,6 +136,11 @@ const AppNavigationMixin = { label: this.$t('appNavigation.powerRestorePolicy'), route: '/settings/power-restore-policy', }, + { + id: 'snmp-alerts', + label: this.$t('appNavigation.snmpAlerts'), + route: '/settings/snmp-alerts', + }, ], }, { diff --git a/src/locales/en-US.json b/src/locales/en-US.json index ff107690..18bb7c6a 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -130,6 +130,7 @@ "serverPowerOperations": "@:appPageTitle.serverPowerOperations", "certificates": "@:appPageTitle.certificates", "virtualMedia": "@:appPageTitle.virtualMedia", + "snmpAlerts": "@:appPageTitle.snmpAlerts", "power": "@:appPageTitle.power", "keyClear": "@:appPageTitle.keyClear" }, @@ -159,6 +160,7 @@ "serialOverLan": "Serial over LAN (SOL) console", "serverPowerOperations": "Server power operations", "certificates": "Certificates", + "snmpAlerts": "SNMP Alerts", "virtualMedia": "Virtual media", "keyClear": "Key clear" }, @@ -895,6 +897,32 @@ "successSaveSettings": "Successfully saved settings." } }, + "pageSnmpAlerts": { + "addDestination": "Add destination", + "deleteDestination": "Delete destination | Delete destinations", + "pageDescription": "Set the Simple Network Management Protocol (SNMP) traps with an IP address and a port.", + "modal": { + "addSnmpDestinationTitle": "Add SNMP alert destination", + "batchDeleteConfirmMessage": "Are you sure you want to delete the SNMP alert destination? This action cannot be undone. | Are you sure you want to delete %{count} SNMP alert destinations? This action cannot be undone.", + "deleteConfirmMessage": "Are you sure you want to delete the SNMP alert destination? This action cannot be undone.", + "deleteSnmpDestinationTitle": "Delete SNMP alert destination | Delete SNMP alert destinations", + "ipaddress": "IP Address", + "port": "Port" + }, + "table": { + "ipaddress": "IP Address", + "port": "Port" + }, + "toast": { + "errorAddDestination": "Error in adding SNMP alert destination", + "errorBatchDelete": "Error in deleting SNMP alert destination. | Error in deleting SNMP alert destinations.", + "errorDeleteDestination": "Error deleting SNMP alert destination.", + "errorLoadSnmpDetails": "Error loading SNMP alert details.", + "successAddDestination": "Successfully added SNMP alert destination.", + "successBatchDelete": "Successfully deleted SNMP alert destination. | Successfully deleted %{count} SNMP alert destinations.", + "successDeleteDestination": "Successfully deleted SNMP alert destination." + } + }, "pageCertificates": { "addNewCertificate": "Add new certificate", "caCertificate": "CA Certificate", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index d6c8275c..f01bac74 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -130,6 +130,7 @@ "serverPowerOperations": "@:appPageTitle.serverPowerOperations", "certificates": "@:appPageTitle.certificates", "virtualMedia": "@:appPageTitle.virtualMedia", + "snmpAlerts": "@:appPageTitle.snmpAlerts", "power": "@:appPageTitle.power", "keyClear": "@:appPageTitle.keyClear" }, @@ -159,6 +160,7 @@ "serialOverLan": "Консоль Serial over LAN (SOL)", "serverPowerOperations": "Управление питанием сервера", "certificates": "Сертификаты", + "snmpAlerts": "SNMP оповещения", "virtualMedia": "Виртуальные носители", "keyClear": "Удаление ключей" }, @@ -895,6 +897,32 @@ "successSaveSettings": "Успешное сохранение настроек." } }, + "pageSnmpAlerts": { + "addDestination": "Добавить получателя", + "deleteDestination": "Удалить получателя | Удалить получателей", + "pageDescription": "Настройка IP адреса и порта для Simple Network Management Protocol (SNMP) ловушек.", + "modal": { + "addSnmpDestinationTitle": "Добавить получателя SNMP оповещения", + "batchDeleteConfirmMessage": "Вы уверены, что хотите удалить получателя SNMP оповещения? Действие не может быть отменено. | Вы уверены, что хотите удалить %{count} получателей SNMP оповещений? Действие не может быть отменено.", + "deleteConfirmMessage": "Вы уверены, что хотите удалить получателя SNMP оповещения? Действие не может быть отменено.", + "deleteSnmpDestinationTitle": "Удаление получателя SNMP оповещения | Удаление получателей SNMP оповещений", + "ipaddress": "IP адрес", + "port": "Порт" + }, + "table": { + "ipaddress": "IP адрес", + "port": "Порт" + }, + "toast": { + "errorAddDestination": "Ошибка добавления получателя SNMP оповещения", + "errorBatchDelete": "Ошибка удаления получателя SNMP оповещения. | Ошибка удаления получателей SNMP оповещений.", + "errorDeleteDestination": "Ошибка удаления получателя SNMP оповещения.", + "errorLoadSnmpDetails": "Ошибка загрузки информации о получателе SNMP оповещения.", + "successAddDestination": "Успешное добавление получателя SNMP оповещения.", + "successBatchDelete": "Успешное удаление получателя SNMP оповещения. | Успешное удаление %{count} получателей SNMP оповещений.", + "successDeleteDestination": "Успешное удаление получателя SNMP оповещения." + } + }, "pageCertificates": { "addNewCertificate": "Добавить новый сертификат", "caCertificate": "Сертификат CA", diff --git a/src/router/routes.js b/src/router/routes.js index 4cf8d72d..eb376aad 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -29,6 +29,7 @@ import ServerPowerOperations from '@/views/Operations/ServerPowerOperations'; import Certificates from '@/views/SecurityAndAccess/Certificates'; import VirtualMedia from '@/views/Operations/VirtualMedia'; import Power from '@/views/ResourceManagement/Power'; +import SnmpAlerts from '@/views/Settings/SnmpAlerts'; import i18n from '@/i18n'; const roles = { @@ -191,6 +192,14 @@ const routes = [ }, }, { + path: '/settings/snmp-alerts', + name: 'snmp-alerts', + component: SnmpAlerts, + meta: { + title: i18n.t('appPageTitle.snmpAlerts'), + }, + }, + { path: '/operations/factory-reset', name: 'factory-reset', component: FactoryReset, diff --git a/src/store/index.js b/src/store/index.js index c0b78945..8b1ed078 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -28,6 +28,7 @@ import AssemblyStore from './modules/HardwareStatus/AssemblyStore'; import PostCodeLogsStore from './modules/Logs/PostCodeLogsStore'; import PoliciesStore from './modules/SecurityAndAccess/PoliciesStore'; import FactoryResetStore from './modules/Operations/FactoryResetStore'; +import SnmpAlertsStore from './modules/Settings/SnmpAlertsStore'; import KeyClearStore from './modules/Operations/KeyClearStore'; import WebSocketPlugin from './plugins/WebSocketPlugin'; @@ -58,6 +59,7 @@ export default new Vuex.Store({ dumps: DumpsStore, sensors: SensorsStore, serverLed: ServerLedStore, + snmpAlerts: SnmpAlertsStore, certificates: CertificatesStore, system: SystemStore, memory: MemoryStore, diff --git a/src/store/modules/Settings/SnmpAlertsStore.js b/src/store/modules/Settings/SnmpAlertsStore.js new file mode 100644 index 00000000..f945ee3a --- /dev/null +++ b/src/store/modules/Settings/SnmpAlertsStore.js @@ -0,0 +1,121 @@ +import api, { getResponseCount } from '@/store/api'; +import i18n from '@/i18n'; + +const SnmpAlertsStore = { + namespaced: true, + state: { + allSnmpDetails: [], + }, + getters: { + allSnmpDetails(state) { + return state.allSnmpDetails; + }, + }, + mutations: { + setSnmpDetails(state, allSnmpDetails) { + state.allSnmpDetails = allSnmpDetails; + }, + }, + actions: { + async getSnmpAlertUrl() { + return await api + .get('/redfish/v1/') + .then((response) => api.get(response.data.EventService['@odata.id'])) + .then((response) => api.get(response.data.Subscriptions['@odata.id'])) + .then((response) => response.data['@odata.id']) + .catch((error) => console.log('Error', error)); + }, + async getSnmpDetails({ commit, dispatch }) { + const snmpAlertUrl = await dispatch('getSnmpAlertUrl'); + return await api + .get(snmpAlertUrl) + .then((response) => + response.data.Members.map((user) => user['@odata.id']) + ) + .then((userIds) => api.all(userIds.map((user) => api.get(user)))) + .then((users) => { + const snmpDetailsData = users.map((user) => user.data); + commit('setSnmpDetails', snmpDetailsData); + }) + .catch((error) => { + console.log(error); + const message = i18n.t('pageSnmpAlerts.toast.errorLoadSnmpDetails'); + throw new Error(message); + }); + }, + async deleteDestination({ dispatch }, id) { + const snmpAlertUrl = await dispatch('getSnmpAlertUrl'); + return await api + .delete(`${snmpAlertUrl}/${id}`) + .then(() => dispatch('getSnmpDetails')) + .then(() => + i18n.t('pageSnmpAlerts.toast.successDeleteDestination', { + id, + }) + ) + .catch((error) => { + console.log(error); + const message = i18n.t( + 'pageSnmpAlerts.toast.errorDeleteDestination', + { + id, + } + ); + throw new Error(message); + }); + }, + async deleteMultipleDestinations({ dispatch }, destination) { + const snmpAlertUrl = await dispatch('getSnmpAlertUrl'); + const promises = destination.map(({ id }) => { + return api.delete(`${snmpAlertUrl}/${id}`).catch((error) => { + console.log(error); + return error; + }); + }); + return await api + .all(promises) + .then((response) => { + dispatch('getSnmpDetails'); + return response; + }) + .then( + api.spread((...responses) => { + const { successCount, errorCount } = getResponseCount(responses); + let toastMessages = []; + + if (successCount) { + const message = i18n.tc( + 'pageSnmpAlerts.toast.successBatchDelete', + successCount + ); + toastMessages.push({ type: 'success', message }); + } + + if (errorCount) { + const message = i18n.tc( + 'pageSnmpAlerts.toast.errorBatchDelete', + errorCount + ); + toastMessages.push({ type: 'error', message }); + } + + return toastMessages; + }) + ); + }, + async addDestination({ dispatch }, { data }) { + const snmpAlertUrl = await dispatch('getSnmpAlertUrl'); + return await api + .post(snmpAlertUrl, data) + .then(() => dispatch('getSnmpDetails')) + .then(() => i18n.t('pageSnmpAlerts.toast.successAddDestination')) + .catch((error) => { + console.log(error); + const message = i18n.t('pageSnmpAlerts.toast.errorAddDestination'); + throw new Error(message); + }); + }, + }, +}; + +export default SnmpAlertsStore; diff --git a/src/views/Settings/SnmpAlerts/ModalAddDestination.vue b/src/views/Settings/SnmpAlerts/ModalAddDestination.vue new file mode 100644 index 00000000..9637652b --- /dev/null +++ b/src/views/Settings/SnmpAlerts/ModalAddDestination.vue @@ -0,0 +1,145 @@ +<template> + <b-modal id="add-destination" ref="modal" @ok="onOk" @hidden="resetForm"> + <template #modal-title> + {{ $t('pageSnmpAlerts.modal.addSnmpDestinationTitle') }} + </template> + <b-form id="form-destination"> + <b-container> + <b-row> + <b-col sm="6"> + <!-- Add new SNMP alert destination type --> + <b-form-group + :label="$t('pageSnmpAlerts.modal.ipaddress')" + label-for="ip-address" + > + <b-form-input + id="ip-Address" + v-model="form.ipAddress" + :state="getValidationState($v.form.ipAddress)" + data-test-id="snmpAlerts-input-ipAddress" + type="text" + @blur="$v.form.ipAddress.$touch()" + /> + + <b-form-invalid-feedback role="alert"> + <template v-if="!$v.form.ipAddress.required"> + {{ $t('global.form.fieldRequired') }} + </template> + <template v-if="!$v.form.ipAddress.ipAddress"> + {{ $t('global.form.invalidFormat') }} + </template> + </b-form-invalid-feedback> + </b-form-group> + </b-col> + <b-col> + <b-form-group label-for="port"> + <template #label> + {{ $t('pageSnmpAlerts.modal.port') }} - + <span class="form-text d-inline"> + {{ $t('global.form.optional') }} + </span> + </template> + <b-form-input + id="port" + v-model="form.port" + type="text" + :state="getValidationState($v.form.port)" + data-test-id="snmpAlerts-input-port" + @blur="$v.form.port.$touch()" + /> + <b-form-invalid-feedback role="alert"> + <template + v-if="!$v.form.port.minLength || !$v.form.port.maxLength" + > + {{ + $t('global.form.valueMustBeBetween', { + min: 0, + max: 65535, + }) + }} + </template> + </b-form-invalid-feedback> + </b-form-group> + </b-col> + </b-row> + </b-container> + </b-form> + <template #modal-footer="{ cancel }"> + <b-button variant="secondary" @click="cancel()"> + {{ $t('global.action.cancel') }} + </b-button> + <b-button + form="form-user" + type="submit" + variant="primary" + data-test-id="snmpAlerts-button-ok" + @click="onOk" + > + {{ $t('pageSnmpAlerts.addDestination') }} + </b-button> + </template> + </b-modal> +</template> + +<script> +import { + required, + ipAddress, + minValue, + maxValue, +} from 'vuelidate/lib/validators'; +import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js'; + +export default { + mixins: [VuelidateMixin], + data() { + return { + form: { + ipaddress: null, + port: null, + }, + }; + }, + validations() { + return { + form: { + ipAddress: { + required, + ipAddress, + }, + port: { + minValue: minValue(0), + maxValue: maxValue(65535), + }, + }, + }; + }, + methods: { + handleSubmit() { + this.$v.$touch(); + if (this.$v.$invalid) return; + this.$emit('ok', { + ipAddress: this.form.ipAddress, + port: this.form.port, + }); + this.closeModal(); + }, + closeModal() { + this.$nextTick(() => { + this.$refs.modal.hide(); + }); + }, + resetForm() { + this.form.ipAddress = ''; + this.form.port = ''; + this.$v.$reset(); + this.$emit('hidden'); + }, + onOk(bvModalEvt) { + // prevent modal close + bvModalEvt.preventDefault(); + this.handleSubmit(); + }, + }, +}; +</script> diff --git a/src/views/Settings/SnmpAlerts/SnmpAlerts.vue b/src/views/Settings/SnmpAlerts/SnmpAlerts.vue new file mode 100644 index 00000000..8a9b8e73 --- /dev/null +++ b/src/views/Settings/SnmpAlerts/SnmpAlerts.vue @@ -0,0 +1,274 @@ +<template> + <b-container fluid="xl"> + <page-title :description="$t('pageSnmpAlerts.pageDescription')" /> + <b-row> + <b-col xl="9" class="text-right"> + <b-button variant="primary" @click="initModalAddDestination"> + <icon-add /> + {{ $t('pageSnmpAlerts.addDestination') }} + </b-button> + </b-col> + </b-row> + <b-row> + <b-col xl="9"> + <table-toolbar + ref="toolbar" + :selected-items-count="selectedRows.length" + :actions="tableToolbarActions" + @clear-selected="clearSelectedRows($refs.table)" + @batch-action="onBatchAction" + /> + <b-table + ref="table" + responsive="md" + selectable + show-empty + no-select-on-click + hover + :fields="fields" + :items="tableItems" + :empty-text="$t('global.table.emptyMessage')" + @row-selected="onRowSelected($event, tableItems.length)" + > + <!-- Checkbox column --> + <template #head(checkbox)> + <b-form-checkbox + v-model="tableHeaderCheckboxModel" + data-test-id="snmpAlerts-checkbox-selectAll" + :indeterminate="tableHeaderCheckboxIndeterminate" + @change="onChangeHeaderCheckbox($refs.table)" + > + <span class="sr-only">{{ $t('global.table.selectAll') }}</span> + </b-form-checkbox> + </template> + <template #cell(checkbox)="row"> + <b-form-checkbox + v-model="row.rowSelected" + :data-test-id="`snmpAlerts-checkbox-selectRow-${row.index}`" + @change="toggleSelectRow($refs.table, row.index)" + > + <span class="sr-only">{{ $t('global.table.selectItem') }}</span> + </b-form-checkbox> + </template> + + <!-- table actions column --> + <template #cell(actions)="{ item }"> + <table-row-action + v-for="(action, index) in item.actions" + :key="index" + :value="action.value" + :enabled="action.enabled" + :title="action.title" + :data-test-id="`snmpAlerts-button-deleteRow-${item.index}`" + @click-table-action="onTableRowAction($event, item)" + > + <template #icon> + <icon-trashcan v-if="action.value === 'delete'" /> + </template> + </table-row-action> + </template> + </b-table> + </b-col> + </b-row> + <!-- Modals --> + <modal-add-destination @ok="onModalOk" /> + </b-container> +</template> + +<script> +import IconTrashcan from '@carbon/icons-vue/es/trash-can/20'; +import ModalAddDestination from './ModalAddDestination'; +import PageTitle from '@/components/Global/PageTitle'; +import IconAdd from '@carbon/icons-vue/es/add--alt/20'; +import TableToolbar from '@/components/Global/TableToolbar'; +import TableRowAction from '@/components/Global/TableRowAction'; +import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin'; +import BVToastMixin from '@/components/Mixins/BVToastMixin'; + +import BVTableSelectableMixin, { + selectedRows, + tableHeaderCheckboxModel, + tableHeaderCheckboxIndeterminate, +} from '@/components/Mixins/BVTableSelectableMixin'; +export default { + name: 'SnmpAlerts', + components: { + PageTitle, + IconAdd, + TableToolbar, + IconTrashcan, + ModalAddDestination, + TableRowAction, + }, + mixins: [BVTableSelectableMixin, BVToastMixin, LoadingBarMixin], + beforeRouteLeave(to, from, next) { + this.hideLoader(); + next(); + }, + data() { + return { + fields: [ + { + key: 'checkbox', + }, + { + key: 'IP', + label: this.$t('pageSnmpAlerts.table.ipaddress'), + }, + { + key: 'Port', + label: this.$t('pageSnmpAlerts.table.port'), + }, + { + key: 'actions', + label: '', + tdClass: 'text-right text-nowrap', + }, + ], + tableToolbarActions: [ + { + value: 'delete', + label: this.$t('global.action.delete'), + }, + ], + selectedRows: selectedRows, + tableHeaderCheckboxModel: tableHeaderCheckboxModel, + tableHeaderCheckboxIndeterminate: tableHeaderCheckboxIndeterminate, + }; + }, + computed: { + allSnmpDetails() { + return this.$store.getters['snmpAlerts/allSnmpDetails']; + }, + tableItems() { + // transform destination data to table data + return this.allSnmpDetails.map((subscriptions) => { + const [destination, dataWithProtocol, dataWithoutProtocol] = [ + subscriptions.Destination, + subscriptions.Destination.split('/')[2].split(':'), + subscriptions.Destination.split(':'), + ]; + //condition to check if destination comes with protocol or not + const conditionForProtocolCheck = destination.includes('://'); + const ip = conditionForProtocolCheck + ? dataWithProtocol[0] + : dataWithoutProtocol[0]; + const port = conditionForProtocolCheck + ? dataWithProtocol[1] + : dataWithoutProtocol[1]; + return { + IP: ip, + Port: port, + id: subscriptions.Id, + actions: [ + { + value: 'delete', + enabled: true, + title: this.$tc('pageSnmpAlerts.deleteDestination'), + }, + ], + ...subscriptions, + }; + }); + }, + }, + created() { + this.startLoader(); + this.$store + .dispatch('snmpAlerts/getSnmpDetails') + .finally(() => this.endLoader()); + }, + methods: { + onModalOk({ ipAddress, port }) { + const protocolIpAddress = 'snmp://' + ipAddress; + const destination = port + ? protocolIpAddress + ':' + port + : protocolIpAddress; + const data = { + Destination: destination, + SubscriptionType: 'SNMPTrap', + Protocol: 'SNMPv2c', + }; + this.startLoader(); + this.$store + .dispatch('snmpAlerts/addDestination', { data }) + .then((success) => this.successToast(success)) + .catch(({ message }) => this.errorToast(message)) + .finally(() => this.endLoader()); + }, + initModalAddDestination() { + this.$bvModal.show('add-destination'); + }, + initModalDeleteDestination(destination) { + this.$bvModal + .msgBoxConfirm( + this.$t('pageSnmpAlerts.modal.deleteConfirmMessage', { + destination: destination.id, + }), + { + title: this.$tc('pageSnmpAlerts.modal.deleteSnmpDestinationTitle'), + okTitle: this.$tc('pageSnmpAlerts.deleteDestination'), + cancelTitle: this.$t('global.action.cancel'), + } + ) + .then((deleteConfirmed) => { + if (deleteConfirmed) { + this.deleteDestination(destination); + } + }); + }, + deleteDestination({ id }) { + this.startLoader(); + this.$store + .dispatch('snmpAlerts/deleteDestination', id) + .then((success) => this.successToast(success)) + .catch(({ message }) => this.errorToast(message)) + .finally(() => this.endLoader()); + }, + onBatchAction(action) { + if (action === 'delete') { + this.$bvModal + .msgBoxConfirm( + this.$tc( + 'pageSnmpAlerts.modal.batchDeleteConfirmMessage', + this.selectedRows.length + ), + { + title: this.$tc( + 'pageSnmpAlerts.modal.deleteSnmpDestinationTitle', + this.selectedRows.length + ), + okTitle: this.$tc( + 'pageSnmpAlerts.deleteDestination', + this.selectedRows.length + ), + cancelTitle: this.$t('global.action.cancel'), + } + ) + .then((deleteConfirmed) => { + if (deleteConfirmed) { + this.startLoader(); + this.$store + .dispatch( + 'snmpAlerts/deleteMultipleDestinations', + this.selectedRows + ) + .then((messages) => { + messages.forEach(({ type, message }) => { + if (type === 'success') this.successToast(message); + if (type === 'error') this.errorToast(message); + }); + }) + .finally(() => this.endLoader()); + } + }); + } + }, + onTableRowAction(action, row) { + if (action === 'delete') { + this.initModalDeleteDestination(row); + } + }, + }, +}; +</script> diff --git a/src/views/Settings/SnmpAlerts/index.js b/src/views/Settings/SnmpAlerts/index.js new file mode 100644 index 00000000..f27ed4aa --- /dev/null +++ b/src/views/Settings/SnmpAlerts/index.js @@ -0,0 +1,2 @@ +import SnmpAlerts from './SnmpAlerts.vue'; +export default SnmpAlerts; |