diff --git a/public/bundler.php b/public/bundler.php index 081e7c041..6b165e718 100644 --- a/public/bundler.php +++ b/public/bundler.php @@ -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", diff --git a/public/cssbundler.php b/public/cssbundler.php index ae1eee5a6..79bdf2724 100644 --- a/public/cssbundler.php +++ b/public/cssbundler.php @@ -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', ]; diff --git a/public/js/pages/Device/Device.css b/public/js/pages/Device/Device.css index 2548161e7..11e78d3d9 100644 --- a/public/js/pages/Device/Device.css +++ b/public/js/pages/Device/Device.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; } \ No newline at end of file +.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; +} \ No newline at end of file diff --git a/public/js/pages/Device/DeviceMonitoring.js b/public/js/pages/Device/DeviceMonitoring.js index 96266313b..6020bb2a9 100644 --- a/public/js/pages/Device/DeviceMonitoring.js +++ b/public/js/pages/Device/DeviceMonitoring.js @@ -200,18 +200,25 @@ Vue.component('device-monitoring-modal', { - Schnittstellen-Alarmierung (Link-Status) - - - SchnittstelleAlarmierung aktiv - - - {{ iface.name }} - - - - - + + + Schnittstellen-Alarmierung (Link-Status) + + + + + + + + Keine Schnittstellen gefunden. + + + {{ iface.name }} + + + + + @@ -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' } -}); \ No newline at end of file +}); diff --git a/public/plugins/vue/tt-components/css/tt-switch.css b/public/plugins/vue/tt-components/css/tt-switch.css new file mode 100644 index 000000000..03647d5c2 --- /dev/null +++ b/public/plugins/vue/tt-components/css/tt-switch.css @@ -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); + } +} \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-switch.js b/public/plugins/vue/tt-components/tt-switch.js new file mode 100644 index 000000000..8f25860dc --- /dev/null +++ b/public/plugins/vue/tt-components/tt-switch.js @@ -0,0 +1,14 @@ +Vue.component('tt-switch', { + template: ` + + + + + + + `, + props: { + value: { type: Boolean, default: false }, + loading: { type: Boolean, default: false } + } +}); \ No newline at end of file