import { useEffect, useMemo, useState } from 'react' import ViewerShortcutButton from '../components/ViewerShortcutButton' import { api, apiPost } from '../lib/visuApi' const RANGE_OPTIONS = [ { value: '15m', label: '15 Min' }, { value: '1h', label: '1 h' }, { value: '6h', label: '6 h' }, { value: '12h', label: '12 h' }, { value: '24h', label: '24 h' }, { value: '7d', label: '7 Tage' }, { value: '30d', label: '30 Tage' }, { value: '90d', label: '90 Tage' }, { value: '180d', label: '6 Monate' }, ] const INTERVAL_OPTIONS = ['auto', '10s', '30s', '1m', '5m', '10m', '15m', '30m', '1h', '6h', '12h', '1d'] const AGGREGATES = [ { value: 'mean', label: 'Mittelwert' }, { value: 'last', label: 'Letzter Wert' }, { value: 'min', label: 'Minimum' }, { value: 'max', label: 'Maximum' }, ] function pointKey(point) { return ['influx', point.database || '', point.measurement || '', point.tag_key || '', point.tag_value || ''].join('|') } function pointMeasurement(point) { return String(point?.measurement || '').trim() } function csvEscape(value, delimiter = ',') { const text = String(value ?? '') return /["\r\n]/.test(text) || text.includes(delimiter) ? `"${text.replace(/"/g, '""')}"` : text } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename a.click() setTimeout(() => URL.revokeObjectURL(url), 1000) } function chunks(items, size = 8) { const out = [] for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size)) return out } function lastNonNull(series) { const row = [...(series?.values || [])].reverse().find((item) => item.value !== null && item.value !== undefined && item.value !== '') return row ? { value: row.value, time: row.time } : { value: '', time: '' } } export default function MonitoringData() { const [databases, setDatabases] = useState([]) const [database, setDatabase] = useState('') const [points, setPoints] = useState([]) const [selection, setSelection] = useState([]) const [savedSelections, setSavedSelections] = useState([]) const [activeSelectionId, setActiveSelectionId] = useState(0) const [selectionName, setSelectionName] = useState('Neue Selection') const [q, setQ] = useState('') const [measurementFilter, setMeasurementFilter] = useState('') const [range, setRange] = useState('24h') const [interval, setIntervalValue] = useState('auto') const [aggregate, setAggregate] = useState('last') const [delimiter, setDelimiter] = useState(';') const [lastValues, setLastValues] = useState({}) const [loading, setLoading] = useState(false) const [savingSelection, setSavingSelection] = useState(false) const [error, setError] = useState('') async function loadSavedSelections() { const data = await api('monitoring_selections') setSavedSelections(data.selections || []) } useEffect(() => { loadSavedSelections().catch((err) => setError(err.message)) }, []) useEffect(() => { let alive = true setLoading(true) setError('') api('trend_sources', { database }) .then((data) => { if (!alive) return setDatabases(data.databases || []) setDatabase((current) => current || data.selected_database || data.databases?.[0] || '') setPoints(data.points || []) }) .catch((err) => { if (alive) setError(err.message) }) .finally(() => { if (alive) setLoading(false) }) return () => { alive = false } }, [database]) useEffect(() => { if (!measurementFilter) return if (!points.some((point) => pointMeasurement(point) === measurementFilter)) { setMeasurementFilter('') } }, [measurementFilter, points]) useEffect(() => { let alive = true async function loadLastValues() { if (!selection.length) { setLastValues({}) return } const next = {} for (const group of chunks(selection, 8)) { const res = await apiPost('trend_data', { series_json: JSON.stringify(group.map((point, index) => ({ ...point, id: pointKey(point) || `selection-${index}`, label: point.tag_value || point.label, }))), range, interval: 'auto', aggregate: 'last', }) ;(res.data?.series || []).forEach((serie) => { next[pointKey(serie)] = lastNonNull(serie) }) } if (alive) setLastValues(next) } loadLastValues().catch((err) => { if (alive) setError(err.message) }) return () => { alive = false } }, [selection, range]) const measurements = useMemo(() => ( Array.from(new Set(points.map(pointMeasurement).filter(Boolean))).sort((a, b) => ( a.localeCompare(b, 'de', { numeric: true, sensitivity: 'base' }) )) ), [points]) const filteredPoints = useMemo(() => { const needle = q.trim().toLowerCase() return points.filter((point) => { const hay = `${point.measurement} ${point.tag_key} ${point.tag_value} ${point.label}`.toLowerCase() const matchesSearch = !needle || hay.includes(needle) const matchesMeasurement = !measurementFilter || pointMeasurement(point) === measurementFilter return matchesSearch && matchesMeasurement }) }, [points, q, measurementFilter]) const selectionKeys = new Set(selection.map(pointKey)) function addPoint(point) { if (selectionKeys.has(pointKey(point))) return setSelection((old) => [...old, point]) } function removePoint(key) { setSelection((old) => old.filter((point) => pointKey(point) !== key)) } async function saveCurrentSelection() { const name = selectionName.trim() || `Selection ${savedSelections.length + 1}` setError('') setSavingSelection(true) try { const data = await apiPost('monitoring_selection_save', { selection_id: activeSelectionId || '', name, database_name: database, range_value: range, interval_value: interval, aggregate_value: aggregate, csv_delimiter: delimiter, points_json: JSON.stringify(selection), }) setActiveSelectionId(data.selection_id || activeSelectionId) setSelectionName(name) await loadSavedSelections() } catch (err) { setError(err.message) } finally { setSavingSelection(false) } } function loadSelection(item) { setSelection(item.points || []) setSelectionName(item.name || 'Selection') setActiveSelectionId(Number(item.id || 0)) setDatabase(item.database_name || database) setRange(item.range_value || '24h') setIntervalValue(item.interval_value || 'auto') setAggregate(item.aggregate_value || 'last') setDelimiter(item.csv_delimiter || item.delimiter_value || ';') } async function deleteSelection(item) { if (!item?.id) return setError('') try { await apiPost('monitoring_selection_delete', { selection_id: item.id }) if (Number(item.id) === Number(activeSelectionId)) setActiveSelectionId(0) await loadSavedSelections() } catch (err) { setError(err.message) } } function exportLastValuesCsv() { const header = ['Datenpunkt', 'Measurement', 'Tag', 'Wert', 'Zeit', 'Datenbank'] const lines = [ header.map((cell) => csvEscape(cell, delimiter)).join(delimiter), ...selection.map((point) => { const last = lastValues[pointKey(point)] || {} return [point.tag_value, point.measurement, point.tag_key, last.value ?? '', last.time ?? '', point.database] .map((cell) => csvEscape(cell, delimiter)).join(delimiter) }), ] downloadBlob(new Blob([`\uFEFF${lines.join('\r\n')}`], { type: 'text/csv;charset=utf-8' }), `${selectionName || 'monitoring_selection'}_lastvalues.csv`) } async function exportTrendData(kind = 'csv') { const rows = new Map() const headers = ['Zeit', ...selection.map((point) => point.tag_value || point.label)] for (const group of chunks(selection, 8)) { const res = await apiPost('trend_data', { series_json: JSON.stringify(group.map((point) => ({ ...point, id: pointKey(point), label: point.tag_value || point.label, }))), range, interval, aggregate, }) ;(res.data?.series || []).forEach((serie) => { ;(serie.values || []).forEach((row) => { if (!rows.has(row.time)) rows.set(row.time, { Zeit: row.time }) rows.get(row.time)[serie.label] = row.value ?? '' }) }) } const sortedRows = Array.from(rows.values()).sort((a, b) => String(a.Zeit).localeCompare(String(b.Zeit))) if (kind === 'xls') { const html = [ '', `${headers.map((cell) => ``).join('')}`, ...sortedRows.map((row) => `${headers.map((cell) => ``).join('')}`), '
${String(cell).replace(/[<>&]/g, '')}
${String(row[cell] ?? '').replace(/[<>&]/g, '')}
', ].join('') downloadBlob(new Blob([html], { type: 'application/vnd.ms-excel;charset=utf-8' }), `${selectionName || 'monitoring_selection'}_trend.xls`) return } const lines = [ headers.map((cell) => csvEscape(cell, delimiter)).join(delimiter), ...sortedRows.map((row) => headers.map((cell) => csvEscape(row[cell] ?? '', delimiter)).join(delimiter)), ] downloadBlob(new Blob([`\uFEFF${lines.join('\r\n')}`], { type: 'text/csv;charset=utf-8' }), `${selectionName || 'monitoring_selection'}_trend.csv`) } return (
Monitoring

Datenpunkt-Uebersicht

Datenpunkte auswaehlen, Selections speichern und manuelle CSV- oder Excel-Exporte ziehen.

Dashboard Verwaltung Templates Datenpunkte Virtuelle Datenpunkte CSV Exporte
{error &&
{error}
}
setQ(e.target.value)} placeholder="Measurement, Tag, Datenpunkt..." />
{loading &&
Datenpunkte werden geladen...
}
{filteredPoints.map((point) => ( ))}
setSelectionName(e.target.value)} />
Zyklische Exporte
{selection.map((point) => { const last = lastValues[pointKey(point)] || {} return (
{point.tag_value} {last.value !== '' && last.value !== undefined ? last.value : '...'} {last.time ? `- ${new Date(last.time).toLocaleString('de-DE')}` : ''}
) })}
Gespeicherte Selections
{savedSelections.map((item) => (
))}
) }