Device monitoring/v2
This commit is contained in:
@@ -39,6 +39,7 @@ $jsFiles = [
|
||||
"plugins/vue/tt-components/tt-select.js",
|
||||
"plugins/vue/tt-components/tt-datepicker.js",
|
||||
"plugins/vue/tt-components/tt-input.js",
|
||||
"plugins/vue/tt-components/tt-switch.js",
|
||||
"plugins/vue/tt-components/tt-input-article.js",
|
||||
"plugins/vue/tt-components/tt-button.js",
|
||||
"plugins/vue/tt-components/tt-modal.js",
|
||||
|
||||
@@ -44,6 +44,7 @@ $cssFiles = [
|
||||
'plugins/vue/tt-components/css/tt-tooltip.css',
|
||||
'plugins/vue/tt-components/css/tt-loader.css',
|
||||
'plugins/vue/tt-components/css/tt-select.css',
|
||||
'plugins/vue/tt-components/css/tt-switch.css',
|
||||
'plugins/vue/tt-components/css/tt-file-gallery.css',
|
||||
'plugins/vue/tt-components/css/tt-position-manager.css',
|
||||
];
|
||||
|
||||
@@ -1,29 +1,207 @@
|
||||
.monitoring-tabs { display: flex; border-bottom: 1px solid #dee2e6; }
|
||||
.monitoring-tabs button { background: none; border: none; padding: 10px 15px; cursor: pointer; border-bottom: 3px solid transparent; font-size: 0.9rem; color: #495057; }
|
||||
.monitoring-tabs button:hover { color: #0056b3; }
|
||||
.monitoring-tabs button.active { border-bottom-color: #007bff; color: #007bff; font-weight: bold; }
|
||||
.monitoring-content { padding: 15px; min-height: 400px; }
|
||||
.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; }
|
||||
.chart-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 15px; }
|
||||
.chart-title { font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.problems-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.problem-card { display: flex; align-items: center; padding: 10px; border-radius: 5px; border-left-width: 5px; border-left-style: solid; background-color: #f8f9fa; }
|
||||
.problem-icon { font-size: 1.5rem; margin-right: 15px; width: 30px; text-align: center; }
|
||||
.problem-details { flex-grow: 1; }
|
||||
.problem-header { display: flex; justify-content: space-between; align-items: baseline; }
|
||||
.problem-name { font-weight: 500; }
|
||||
.problem-time { font-size: 0.8rem; color: #6c757d; }
|
||||
.problem-opdata { font-size: 0.85rem; color: #495057; margin-top: 4px; }
|
||||
.sev-info { border-left-color: #17a2b8; } .sev-info .problem-icon { color: #17a2b8; }
|
||||
.sev-warning { border-left-color: #ffc107; } .sev-warning .problem-icon { color: #ffc107; }
|
||||
.sev-average { border-left-color: #fd7e14; } .sev-average .problem-icon { color: #fd7e14; }
|
||||
.sev-high { border-left-color: #dc3545; } .sev-high .problem-icon { color: #dc3545; }
|
||||
.sev-disaster { border-left-color: #7B014C; } .sev-disaster .problem-icon { color: #7B014C; }
|
||||
.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; }
|
||||
.problem-counts { display: flex; justify-content: space-around; text-align: center; padding: 1rem 0; }
|
||||
.problem-counts .count { font-size: 1.5rem; font-weight: bold; display: block; }
|
||||
.problem-counts .period { font-size: 0.8rem; color: #6c757d; }
|
||||
.problems-list.resolved .problem-card { opacity: 0.8; }
|
||||
.sev-resolved { border-left: 5px solid #28a745; }
|
||||
.sev-resolved .problem-icon { color: #28a745; }
|
||||
.c-pointer { cursor: pointer; }
|
||||
.monitoring-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.monitoring-tabs button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
font-size: 0.9rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.monitoring-tabs button:hover {
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
.monitoring-tabs button.active {
|
||||
border-bottom-color: #007bff;
|
||||
color: #007bff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.monitoring-content {
|
||||
padding: 15px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.problems-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.problem-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border-left-width: 5px;
|
||||
border-left-style: solid;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.problem-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-right: 15px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.problem-details {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.problem-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.problem-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.problem-time {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.problem-opdata {
|
||||
font-size: 0.85rem;
|
||||
color: #495057;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sev-info {
|
||||
border-left-color: #17a2b8;
|
||||
}
|
||||
|
||||
.sev-info .problem-icon {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.sev-warning {
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
.sev-warning .problem-icon {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.sev-average {
|
||||
border-left-color: #fd7e14;
|
||||
}
|
||||
|
||||
.sev-average .problem-icon {
|
||||
color: #fd7e14;
|
||||
}
|
||||
|
||||
.sev-high {
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
|
||||
.sev-high .problem-icon {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.sev-disaster {
|
||||
border-left-color: #7B014C;
|
||||
}
|
||||
|
||||
.sev-disaster .problem-icon {
|
||||
color: #7B014C;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.problem-counts {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.problem-counts .count {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.problem-counts .period {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.problems-list.resolved .problem-card {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.sev-resolved {
|
||||
border-left: 5px solid #28a745;
|
||||
}
|
||||
|
||||
.sev-resolved .problem-icon {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.c-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Styles for Interface Alarm List */
|
||||
.interface-alarm-list {
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
.interface-alarm-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: .75rem 1rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.interface-alarm-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.interface-alarm-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.interface-name {
|
||||
font-weight: 500;
|
||||
flex-grow: 1;
|
||||
margin-right: 1rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -200,18 +200,25 @@ Vue.component('device-monitoring-modal', {
|
||||
</div>
|
||||
</tt-card>
|
||||
<tt-card>
|
||||
<template v-slot:header><h6><i class="fas fa-bell"></i> Schnittstellen-Alarmierung (Link-Status)</h6></template>
|
||||
<div class="table-responsive" style="max-height: 500px;">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead><tr><th>Schnittstelle</th><th class="text-center">Alarmierung aktiv</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="iface in configData.interfaces" :key="iface.itemid">
|
||||
<td>{{ iface.name }}</td>
|
||||
<td class="text-center"><tt-switch v-model="iface.isAlarmed" @input="toggleInterfaceAlarm(iface)"></tt-switch></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<template v-slot:header>
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap">
|
||||
<h6 class="mb-0"><i class="fas fa-bell"></i> Schnittstellen-Alarmierung (Link-Status)</h6>
|
||||
<div style="min-width: 250px; margin-left: 1rem;">
|
||||
<tt-input sm no-form-group v-model="interfaceSearch" placeholder="Schnittstelle suchen..." type="search" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="interface-alarm-list">
|
||||
<div v-if="!filteredInterfaces || filteredInterfaces.length === 0" class="text-center text-muted p-3">
|
||||
Keine Schnittstellen gefunden.
|
||||
</div>
|
||||
<div v-for="iface in filteredInterfaces" :key="iface.itemid" class="interface-alarm-item">
|
||||
<div class="interface-name">{{ iface.name }}</div>
|
||||
<div class="interface-switch">
|
||||
<tt-switch v-model="iface.isAlarmed" :loading="iface.loading" @input="toggleInterfaceAlarm(iface)"></tt-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</tt-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,6 +249,7 @@ Vue.component('device-monitoring-modal', {
|
||||
dataNormalizationMode: 'avg',
|
||||
downsampleThreshold: 500,
|
||||
configData: { snmp: null, interfaces: [] },
|
||||
interfaceSearch: '',
|
||||
snmpV3Levels: [{text: 'noAuthNoPriv', value: '0'}, {text: 'authNoPriv', value: '1'}, {text: 'authPriv', value: '2'}],
|
||||
snmpV3Auth: [{text: 'MD5', value: '0'}, {text: 'SHA-1', value: '1'}],
|
||||
snmpV3Priv: [{text: 'DES', value: '0'}, {text: 'AES-128', value: '1'}],
|
||||
@@ -257,6 +265,16 @@ Vue.component('device-monitoring-modal', {
|
||||
computed: {
|
||||
interfaceOptions() { return this.allInterfaces.map(iface => ({ text: iface.name, value: iface.name })); },
|
||||
selectedInterfacesData() { return this.allInterfaces.filter(iface => this.selectedInterfaces.includes(iface.name)); },
|
||||
filteredInterfaces() {
|
||||
if (!this.configData.interfaces || !Array.isArray(this.configData.interfaces)) return [];
|
||||
if (!this.interfaceSearch) {
|
||||
return this.configData.interfaces;
|
||||
}
|
||||
const search = this.interfaceSearch.toLowerCase();
|
||||
return this.configData.interfaces.filter(iface =>
|
||||
iface.name.toLowerCase().includes(search)
|
||||
);
|
||||
},
|
||||
statistics() {
|
||||
if (this.selectedInterfaces.length === 0 || Object.keys(this.interfaceChartData).length === 0) return {};
|
||||
const stats = {};
|
||||
@@ -315,6 +333,9 @@ Vue.component('device-monitoring-modal', {
|
||||
this.problemData = (await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/getProblems`, { params: { hostId: this.hostId } })).data;
|
||||
} else if (tab === 'configuration') {
|
||||
this.configData = (await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/getConfigurationData`, { params: { hostId: this.hostId } })).data;
|
||||
if (this.configData.interfaces && Array.isArray(this.configData.interfaces)) {
|
||||
this.configData.interfaces.forEach(iface => this.$set(iface, 'loading', false));
|
||||
}
|
||||
} else if (tab === 'reports') {
|
||||
await this.fetchReportData();
|
||||
}
|
||||
@@ -339,10 +360,16 @@ Vue.component('device-monitoring-modal', {
|
||||
} catch(e) { window.notify('error', 'Fehler beim Speichern der SNMP-Konfiguration.'); }
|
||||
},
|
||||
async toggleInterfaceAlarm(iface) {
|
||||
this.$set(iface, 'loading', true);
|
||||
try {
|
||||
await axios.post(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/updateInterfaceAlarm`, { hostId: this.hostId, item: iface, enabled: iface.isAlarmed });
|
||||
window.notify('success', `Alarm für ${iface.name} ${iface.isAlarmed ? 'aktiviert' : 'deaktiviert'}.`);
|
||||
} catch(e) { window.notify('error', 'Fehler beim Ändern des Alarms.'); iface.isAlarmed = !iface.isAlarmed; }
|
||||
} catch(e) {
|
||||
window.notify('error', 'Fehler beim Ändern des Alarms.');
|
||||
iface.isAlarmed = !iface.isAlarmed;
|
||||
} finally {
|
||||
this.$set(iface, 'loading', false);
|
||||
}
|
||||
},
|
||||
async fetchReportData() {
|
||||
this.loading.reports = true;
|
||||
@@ -413,4 +440,4 @@ Vue.component('device-monitoring-modal', {
|
||||
dataNormalizationMode() { this.handleInterfaceSelectionChange(this.selectedInterfaces, this.selectedInterfaces); },
|
||||
reportTimeRange: 'fetchReportData'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
103
public/plugins/vue/tt-components/css/tt-switch.css
Normal file
103
public/plugins/vue/tt-components/css/tt-switch.css
Normal file
@@ -0,0 +1,103 @@
|
||||
.tt-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.tt-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: background-color .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
transition: transform .4s, opacity .4s; /* Added opacity transition */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #28a745;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* --- Loading State --- */
|
||||
|
||||
input:disabled + .slider {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Fade out the handle when loading/disabled */
|
||||
input[type="checkbox"]:disabled + .slider:before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Wrapper for the spinner to handle translation */
|
||||
.spinner-wrapper {
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
transition: transform .4s;
|
||||
}
|
||||
|
||||
/* Move wrapper when switch is checked */
|
||||
input:checked + .slider .spinner-wrapper {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
input:checked:disabled + .slider .spinner-wrapper {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* The actual spinner for rotation */
|
||||
.spinner {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
14
public/plugins/vue/tt-components/tt-switch.js
Normal file
14
public/plugins/vue/tt-components/tt-switch.js
Normal file
@@ -0,0 +1,14 @@
|
||||
Vue.component('tt-switch', {
|
||||
template: `
|
||||
<label class="tt-switch">
|
||||
<input type="checkbox" :checked="value" @change="$emit('input', $event.target.checked)" :disabled="loading">
|
||||
<span class="slider round">
|
||||
<span v-if="loading" class="spinner-wrapper"><span class="spinner"></span></span>
|
||||
</span>
|
||||
</label>
|
||||
`,
|
||||
props: {
|
||||
value: { type: Boolean, default: false },
|
||||
loading: { type: Boolean, default: false }
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user