Overhaul of the highlight system
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import * as statusColors from '../../lib/statusColors'
|
||||
|
||||
interface AlertItem {
|
||||
id: number
|
||||
@@ -31,10 +32,14 @@ const AlertsList: React.FC<AlertsListProps> = ({ alerts, onAcknowledgeToggle, in
|
||||
|
||||
const getStatusColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'critical': return '#b3261e'
|
||||
case 'warning': return '#fd7c22'
|
||||
case 'info': return '#00ff00'
|
||||
default: return '#666'
|
||||
case 'critical':
|
||||
return statusColors.STATUS_COLOR_CRITICAL
|
||||
case 'warning':
|
||||
return statusColors.STATUS_COLOR_WARNING
|
||||
case 'info':
|
||||
return statusColors.STATUS_COLOR_NORMAL
|
||||
default:
|
||||
return statusColors.STATUS_COLOR_UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +109,14 @@ const AlertsList: React.FC<AlertsListProps> = ({ alerts, onAcknowledgeToggle, in
|
||||
<td className="py-4">
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium text-white"
|
||||
style={{ backgroundColor: item.priority === 'high' ? '#b3261e' : item.priority === 'medium' ? '#fd7c22' : '#00ff00' }}
|
||||
style={{
|
||||
backgroundColor:
|
||||
item.priority === 'high'
|
||||
? statusColors.STATUS_COLOR_CRITICAL
|
||||
: item.priority === 'medium'
|
||||
? statusColors.STATUS_COLOR_WARNING
|
||||
: statusColors.STATUS_COLOR_NORMAL,
|
||||
}}
|
||||
>
|
||||
{item.priority === 'high' ? 'Высокий' : item.priority === 'medium' ? 'Средний' : 'Низкий'}
|
||||
</span>
|
||||
@@ -142,4 +154,4 @@ const AlertsList: React.FC<AlertsListProps> = ({ alerts, onAcknowledgeToggle, in
|
||||
)
|
||||
}
|
||||
|
||||
export default AlertsList
|
||||
export default AlertsList
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import * as statusColors from '../../lib/statusColors'
|
||||
|
||||
interface Detector {
|
||||
detector_id: number
|
||||
name: string
|
||||
@@ -30,14 +31,14 @@ interface RawDetector {
|
||||
priority: string
|
||||
}>
|
||||
}
|
||||
|
||||
|
||||
interface DetectorListProps {
|
||||
objectId?: string
|
||||
selectedDetectors: number[]
|
||||
onDetectorSelect: (detectorId: number, selected: boolean) => void
|
||||
initialSearchTerm?: string
|
||||
}
|
||||
|
||||
|
||||
const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors, onDetectorSelect, initialSearchTerm = '' }) => {
|
||||
const [detectors, setDetectors] = useState<Detector[]>([])
|
||||
const [searchTerm, setSearchTerm] = useState<string>(initialSearchTerm)
|
||||
@@ -150,12 +151,15 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
|
||||
<td className="py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full`}
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: detector.status }}
|
||||
></div>
|
||||
<span className="text-sm text-gray-300">
|
||||
{detector.status === '#b3261e' ? 'Критическое' :
|
||||
detector.status === '#fd7c22' ? 'Предупреждение' : 'Норма'}
|
||||
{detector.status === statusColors.STATUS_COLOR_CRITICAL
|
||||
? 'Критическое'
|
||||
: detector.status === statusColors.STATUS_COLOR_WARNING
|
||||
? 'Предупреждение'
|
||||
: 'Норма'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -187,15 +191,15 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
|
||||
<div className="text-sm text-gray-400">Всего</div>
|
||||
</div>
|
||||
<div className="bg-[#161824] p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-500">{filteredDetectors.filter(d => d.status === '#00ff00').length}</div>
|
||||
<div className="text-2xl font-bold text-green-500">{filteredDetectors.filter(d => d.status === statusColors.STATUS_COLOR_NORMAL).length}</div>
|
||||
<div className="text-sm text-gray-400">Норма</div>
|
||||
</div>
|
||||
<div className="bg-[#161824] p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-orange-500">{filteredDetectors.filter(d => d.status === '#fd7c22').length}</div>
|
||||
<div className="text-2xl font-bold text-orange-500">{filteredDetectors.filter(d => d.status === statusColors.STATUS_COLOR_WARNING).length}</div>
|
||||
<div className="text-sm text-gray-400">Предупреждения</div>
|
||||
</div>
|
||||
<div className="bg-[#161824] p-4 rounded-lg">
|
||||
<div className="text-2xl font-bold text-red-500">{filteredDetectors.filter(d => d.status === '#b3261e').length}</div>
|
||||
<div className="text-2xl font-bold text-red-500">{filteredDetectors.filter(d => d.status === statusColors.STATUS_COLOR_CRITICAL).length}</div>
|
||||
<div className="text-sm text-gray-400">Критические</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,4 +213,4 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
|
||||
)
|
||||
}
|
||||
|
||||
export default DetectorList
|
||||
export default DetectorList
|
||||
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
Color4,
|
||||
AbstractMesh,
|
||||
Nullable,
|
||||
HighlightLayer,
|
||||
Mesh,
|
||||
InstancedMesh,
|
||||
HighlightLayer,
|
||||
Animation,
|
||||
CubicEase,
|
||||
EasingFunction,
|
||||
@@ -24,9 +22,19 @@ import {
|
||||
Matrix,
|
||||
} from '@babylonjs/core'
|
||||
import '@babylonjs/loaders'
|
||||
|
||||
|
||||
import SceneToolbar from './SceneToolbar';
|
||||
import LoadingSpinner from '../ui/LoadingSpinner'
|
||||
import {
|
||||
getSensorIdFromMesh,
|
||||
collectSensorMeshes,
|
||||
applyHighlightToMeshes,
|
||||
statusToColor3,
|
||||
} from './sensorHighlight'
|
||||
import {
|
||||
computeSensorOverlayCircles,
|
||||
hexWithAlpha,
|
||||
} from './sensorHighlightOverlay'
|
||||
|
||||
export interface ModelViewerProps {
|
||||
modelPath: string
|
||||
@@ -45,6 +53,7 @@ export interface ModelViewerProps {
|
||||
isSensorSelectionEnabled?: boolean
|
||||
onSensorPick?: (sensorId: string | null) => void
|
||||
highlightAllSensors?: boolean
|
||||
sensorStatusMap?: Record<string, string>
|
||||
}
|
||||
|
||||
const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
@@ -57,6 +66,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
isSensorSelectionEnabled,
|
||||
onSensorPick,
|
||||
highlightAllSensors,
|
||||
sensorStatusMap,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const engineRef = useRef<Nullable<Engine>>(null)
|
||||
@@ -74,6 +84,10 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
const [overlayData, setOverlayData] = useState<{ name?: string; sensorId?: string } | null>(null)
|
||||
const [modelReady, setModelReady] = useState(false)
|
||||
const [panActive, setPanActive] = useState(false);
|
||||
const [webglError, setWebglError] = useState<string | null>(null)
|
||||
const [allSensorsOverlayCircles, setAllSensorsOverlayCircles] = useState<
|
||||
{ sensorId: string; left: number; top: number; colorHex: string }[]
|
||||
>([])
|
||||
|
||||
const handlePan = () => setPanActive(!panActive);
|
||||
|
||||
@@ -222,7 +236,41 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
if (!canvasRef.current || isInitializedRef.current) return
|
||||
|
||||
const canvas = canvasRef.current
|
||||
const engine = new Engine(canvas, true, { stencil: true })
|
||||
setWebglError(null)
|
||||
|
||||
let hasWebGL = false
|
||||
try {
|
||||
const testCanvas = document.createElement('canvas')
|
||||
const gl =
|
||||
testCanvas.getContext('webgl2') ||
|
||||
testCanvas.getContext('webgl') ||
|
||||
testCanvas.getContext('experimental-webgl')
|
||||
hasWebGL = !!gl
|
||||
} catch {
|
||||
hasWebGL = false
|
||||
}
|
||||
|
||||
if (!hasWebGL) {
|
||||
const message = 'WebGL не поддерживается в текущем окружении'
|
||||
setWebglError(message)
|
||||
onError?.(message)
|
||||
setIsLoading(false)
|
||||
setModelReady(false)
|
||||
return
|
||||
}
|
||||
|
||||
let engine: Engine
|
||||
try {
|
||||
engine = new Engine(canvas, true, { stencil: true })
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const message = `WebGL недоступен: ${errorMessage}`
|
||||
setWebglError(message)
|
||||
onError?.(message)
|
||||
setIsLoading(false)
|
||||
setModelReady(false)
|
||||
return
|
||||
}
|
||||
engineRef.current = engine
|
||||
|
||||
engine.runRenderLoop(() => {
|
||||
@@ -260,7 +308,14 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
fillLight.intensity = 0.3
|
||||
fillLight.diffuse = new Color3(0.8, 0.8, 1)
|
||||
|
||||
const hl = new HighlightLayer('highlight-layer', scene)
|
||||
const hl = new HighlightLayer('highlight-layer', scene, {
|
||||
mainTextureRatio: 1,
|
||||
blurTextureSizeRatio: 1,
|
||||
})
|
||||
hl.innerGlow = false
|
||||
hl.outerGlow = true
|
||||
hl.blurHorizontalSize = 2
|
||||
hl.blurVerticalSize = 2
|
||||
highlightLayerRef.current = hl
|
||||
|
||||
const handleResize = () => {
|
||||
@@ -285,7 +340,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
}
|
||||
sceneRef.current = null
|
||||
}
|
||||
}, [])
|
||||
}, [onError])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitializedRef.current || isDisposedRef.current) {
|
||||
@@ -427,19 +482,30 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
}, [modelPath, onError, onModelLoaded])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[ModelViewer] highlightAllSensors effect triggered:', { highlightAllSensors, modelReady, sceneReady: !!sceneRef.current })
|
||||
if (!sceneRef.current || isDisposedRef.current || !modelReady) return
|
||||
|
||||
// Если включено выделение всех сенсоров - выделяем их все
|
||||
if (highlightAllSensors) {
|
||||
console.log('[ModelViewer] Calling highlightAllSensorMeshes()')
|
||||
highlightAllSensorMeshes()
|
||||
const allMeshes = importedMeshesRef.current || []
|
||||
const sensorMeshes = collectSensorMeshes(allMeshes)
|
||||
applyHighlightToMeshes(
|
||||
highlightLayerRef.current,
|
||||
highlightedMeshesRef,
|
||||
sensorMeshes,
|
||||
mesh => {
|
||||
const sid = getSensorIdFromMesh(mesh)
|
||||
const status = sid ? sensorStatusMap?.[sid] : undefined
|
||||
return statusToColor3(status ?? null)
|
||||
},
|
||||
)
|
||||
chosenMeshRef.current = null
|
||||
setOverlayPos(null)
|
||||
setOverlayData(null)
|
||||
setAllSensorsOverlayCircles([])
|
||||
return
|
||||
}
|
||||
|
||||
const sensorId = (focusSensorId ?? '').trim()
|
||||
if (!sensorId) {
|
||||
console.log('[ModelViewer] Focus cleared (no Sensor_ID provided)')
|
||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
||||
highlightedMeshesRef.current = []
|
||||
highlightLayerRef.current?.removeAllMeshes()
|
||||
@@ -452,7 +518,6 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
const allMeshes = importedMeshesRef.current || []
|
||||
|
||||
if (allMeshes.length === 0) {
|
||||
console.warn('[ModelViewer] No meshes available for sensor matching')
|
||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
||||
highlightedMeshesRef.current = []
|
||||
highlightLayerRef.current?.removeAllMeshes()
|
||||
@@ -462,44 +527,8 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
const sensorMeshes = allMeshes.filter((m: any) => {
|
||||
try {
|
||||
return ((m.id ?? '').includes('IfcSensor') || (m.name ?? '').includes('IfcSensor'))
|
||||
} catch (error) {
|
||||
console.warn('[ModelViewer] Error filtering sensor mesh:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const chosen = sensorMeshes.find((m: any) => {
|
||||
try {
|
||||
const meta: any = (m as any)?.metadata
|
||||
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
||||
|
||||
const sid = extras?.Sensor_ID ?? extras?.sensor_id ?? extras?.SERIAL_NUMBER ?? extras?.serial_number ?? extras?.detector_id
|
||||
if (sid != null) {
|
||||
return String(sid).trim() === sensorId
|
||||
}
|
||||
|
||||
const monitoringSensorInstance = extras?.bonsaiPset_ARBM_PSet_MonitoringSensor_Instance
|
||||
if (monitoringSensorInstance && typeof monitoringSensorInstance === 'string') {
|
||||
try {
|
||||
const parsedInstance = JSON.parse(monitoringSensorInstance)
|
||||
const instanceSensorId = parsedInstance?.Sensor_ID
|
||||
if (instanceSensorId != null) {
|
||||
return String(instanceSensorId).trim() === sensorId
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('[ModelViewer] Error parsing MonitoringSensor_Instance JSON:', parseError)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
} catch (error) {
|
||||
console.warn('[ModelViewer] Error matching sensor mesh:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
const sensorMeshes = collectSensorMeshes(allMeshes)
|
||||
const chosen = sensorMeshes.find(m => getSensorIdFromMesh(m) === sensorId)
|
||||
|
||||
console.log('[ModelViewer] Sensor focus', {
|
||||
requested: sensorId,
|
||||
@@ -533,37 +562,19 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
Animation.CreateAndStartAnimation('camTarget', camera, 'target', frameRate, totalFrames, camera.target.clone(), center.clone(), Animation.ANIMATIONLOOPMODE_CONSTANT, ease)
|
||||
Animation.CreateAndStartAnimation('camRadius', camera, 'radius', frameRate, totalFrames, camera.radius, targetRadius, Animation.ANIMATIONLOOPMODE_CONSTANT, ease)
|
||||
|
||||
const hl = highlightLayerRef.current
|
||||
if (hl) {
|
||||
// Переключаем группу рендеринга для предыдущего выделенного меша
|
||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
||||
highlightedMeshesRef.current = []
|
||||
|
||||
hl.removeAllMeshes()
|
||||
if (chosen instanceof Mesh) {
|
||||
chosen.renderingGroupId = 1
|
||||
highlightedMeshesRef.current.push(chosen)
|
||||
hl.addMesh(chosen, new Color3(1, 1, 0))
|
||||
} else if (chosen instanceof InstancedMesh) {
|
||||
// Сохраняем исходный меш для инстанса
|
||||
chosen.sourceMesh.renderingGroupId = 1
|
||||
highlightedMeshesRef.current.push(chosen.sourceMesh)
|
||||
hl.addMesh(chosen.sourceMesh, new Color3(1, 1, 0))
|
||||
} else {
|
||||
const children = typeof (chosen as any)?.getChildMeshes === 'function' ? (chosen as any).getChildMeshes() : []
|
||||
for (const cm of children) {
|
||||
if (cm instanceof Mesh) {
|
||||
cm.renderingGroupId = 1
|
||||
highlightedMeshesRef.current.push(cm)
|
||||
hl.addMesh(cm, new Color3(1, 1, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
applyHighlightToMeshes(
|
||||
highlightLayerRef.current,
|
||||
highlightedMeshesRef,
|
||||
[chosen],
|
||||
mesh => {
|
||||
const sid = getSensorIdFromMesh(mesh)
|
||||
const status = sid ? sensorStatusMap?.[sid] : undefined
|
||||
return statusToColor3(status ?? null)
|
||||
},
|
||||
)
|
||||
chosenMeshRef.current = chosen
|
||||
setOverlayData({ name: chosen.name, sensorId })
|
||||
} catch (error) {
|
||||
console.error('[ModelViewer] Error focusing on sensor mesh:', error)
|
||||
} catch {
|
||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
||||
highlightedMeshesRef.current = []
|
||||
highlightLayerRef.current?.removeAllMeshes()
|
||||
@@ -582,99 +593,6 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [focusSensorId, modelReady, highlightAllSensors])
|
||||
|
||||
// Помощь: извлечь Sensor_ID из метаданных меша (совпадающая логика с фокусом)
|
||||
const getSensorIdFromMesh = React.useCallback((m: AbstractMesh | null): string | null => {
|
||||
if (!m) return null
|
||||
try {
|
||||
const meta: any = (m as any)?.metadata
|
||||
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
||||
const sid = extras?.Sensor_ID ?? extras?.sensor_id ?? extras?.SERIAL_NUMBER ?? extras?.serial_number ?? extras?.detector_id
|
||||
if (sid != null) return String(sid).trim()
|
||||
const monitoringSensorInstance = extras?.bonsaiPset_ARBM_PSet_MonitoringSensor_Instance
|
||||
if (monitoringSensorInstance && typeof monitoringSensorInstance === 'string') {
|
||||
try {
|
||||
const parsedInstance = JSON.parse(monitoringSensorInstance)
|
||||
const instanceSensorId = parsedInstance?.Sensor_ID
|
||||
if (instanceSensorId != null) return String(instanceSensorId).trim()
|
||||
} catch (parseError) {
|
||||
console.warn('[ModelViewer] Error parsing MonitoringSensor_Instance JSON in pick:', parseError)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return null
|
||||
}, [])
|
||||
|
||||
// Функция для выделения всех сенсоров на модели
|
||||
const highlightAllSensorMeshes = React.useCallback(() => {
|
||||
console.log('[ModelViewer] highlightAllSensorMeshes called')
|
||||
const scene = sceneRef.current
|
||||
if (!scene || !highlightLayerRef.current) {
|
||||
console.log('[ModelViewer] Cannot highlight - scene or highlightLayer not ready:', {
|
||||
sceneReady: !!scene,
|
||||
highlightLayerReady: !!highlightLayerRef.current
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const allMeshes = importedMeshesRef.current || []
|
||||
|
||||
// Use the same logic as getSensorIdFromMesh to identify sensor meshes
|
||||
const sensorMeshes = allMeshes.filter((m: any) => {
|
||||
try {
|
||||
const sensorId = getSensorIdFromMesh(m)
|
||||
if (sensorId) {
|
||||
console.log(`[ModelViewer] Found sensor mesh: ${m.name} (id: ${m.id}, sensorId: ${sensorId})`)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.warn('[ModelViewer] Error filtering sensor mesh:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`[ModelViewer] Found ${sensorMeshes.length} sensor meshes out of ${allMeshes.length} total meshes`)
|
||||
|
||||
if (sensorMeshes.length === 0) {
|
||||
console.log('[ModelViewer] No sensor meshes found to highlight')
|
||||
return
|
||||
}
|
||||
|
||||
// Clear previous highlights
|
||||
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
|
||||
highlightedMeshesRef.current = []
|
||||
highlightLayerRef.current?.removeAllMeshes()
|
||||
|
||||
// Highlight all sensor meshes
|
||||
sensorMeshes.forEach((mesh: any) => {
|
||||
try {
|
||||
if (mesh instanceof Mesh) {
|
||||
mesh.renderingGroupId = 1
|
||||
highlightedMeshesRef.current.push(mesh)
|
||||
highlightLayerRef.current?.addMesh(mesh, new Color3(1, 1, 0))
|
||||
} else if (mesh instanceof InstancedMesh) {
|
||||
mesh.sourceMesh.renderingGroupId = 1
|
||||
highlightedMeshesRef.current.push(mesh.sourceMesh)
|
||||
highlightLayerRef.current?.addMesh(mesh.sourceMesh, new Color3(1, 1, 0))
|
||||
} else {
|
||||
const children = typeof mesh.getChildMeshes === 'function' ? mesh.getChildMeshes() : []
|
||||
for (const cm of children) {
|
||||
if (cm instanceof Mesh) {
|
||||
cm.renderingGroupId = 1
|
||||
highlightedMeshesRef.current.push(cm)
|
||||
highlightLayerRef.current?.addMesh(cm, new Color3(1, 1, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[ModelViewer] Error highlighting sensor mesh:', error)
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`[ModelViewer] Successfully highlighted ${highlightedMeshesRef.current.length} sensor meshes`)
|
||||
}, [getSensorIdFromMesh])
|
||||
|
||||
// Включение выбора на основе взаимодействия с моделью только при готовности модели и включении выбора сенсоров
|
||||
useEffect(() => {
|
||||
const scene = sceneRef.current
|
||||
@@ -701,7 +619,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
return () => {
|
||||
scene.onPointerObservable.remove(pickObserver)
|
||||
}
|
||||
}, [modelReady, isSensorSelectionEnabled, onSensorPick, getSensorIdFromMesh])
|
||||
}, [modelReady, isSensorSelectionEnabled, onSensorPick])
|
||||
|
||||
// Расчет позиции оверлея
|
||||
const computeOverlayPosition = React.useCallback((mesh: AbstractMesh | null) => {
|
||||
@@ -733,6 +651,41 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
setOverlayPos(pos)
|
||||
}, [overlayData, computeOverlayPosition])
|
||||
|
||||
useEffect(() => {
|
||||
const scene = sceneRef.current
|
||||
const engine = engineRef.current
|
||||
if (!scene || !engine || !modelReady) {
|
||||
setAllSensorsOverlayCircles([])
|
||||
return
|
||||
}
|
||||
if (!highlightAllSensors || focusSensorId || !sensorStatusMap) {
|
||||
setAllSensorsOverlayCircles([])
|
||||
return
|
||||
}
|
||||
const allMeshes = importedMeshesRef.current || []
|
||||
const sensorMeshes = collectSensorMeshes(allMeshes)
|
||||
if (sensorMeshes.length === 0) {
|
||||
setAllSensorsOverlayCircles([])
|
||||
return
|
||||
}
|
||||
const engineTyped = engine as Engine
|
||||
const updateCircles = () => {
|
||||
const circles = computeSensorOverlayCircles({
|
||||
scene,
|
||||
engine: engineTyped,
|
||||
meshes: sensorMeshes,
|
||||
sensorStatusMap,
|
||||
})
|
||||
setAllSensorsOverlayCircles(circles)
|
||||
}
|
||||
updateCircles()
|
||||
const observer = scene.onBeforeRenderObservable.add(updateCircles)
|
||||
return () => {
|
||||
scene.onBeforeRenderObservable.remove(observer)
|
||||
setAllSensorsOverlayCircles([])
|
||||
}
|
||||
}, [highlightAllSensors, focusSensorId, modelReady, sensorStatusMap])
|
||||
|
||||
// Позиция оверлея при движущейся камере
|
||||
useEffect(() => {
|
||||
if (!sceneRef.current || !chosenMeshRef.current || !overlayData) return
|
||||
@@ -767,10 +720,24 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`w-full h-full outline-none block transition-opacity duration-500 ${
|
||||
showModel ? 'opacity-100' : 'opacity-0'
|
||||
showModel && !webglError ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
/>
|
||||
{isLoading && (
|
||||
{webglError ? (
|
||||
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-50">
|
||||
<div className="text-center p-8 bg-[#161824] rounded-lg border border-gray-700 max-w-md shadow-xl">
|
||||
<div className="text-red-400 text-lg font-semibold mb-2">
|
||||
3D просмотр недоступен
|
||||
</div>
|
||||
<div className="text-gray-300 mb-4">
|
||||
{webglError}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
Включите аппаратное ускорение в браузере или откройте страницу в другом браузере/устройстве
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-50">
|
||||
<LoadingSpinner
|
||||
progress={loadingProgress}
|
||||
@@ -778,8 +745,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
strokeWidth={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!modelReady && !isLoading && (
|
||||
) : !modelReady ? (
|
||||
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-40">
|
||||
<div className="text-center p-8 bg-[#161824] rounded-lg border border-gray-700 max-w-md">
|
||||
<div className="text-gray-400 text-lg font-semibold mb-4">
|
||||
@@ -790,7 +756,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
<SceneToolbar
|
||||
onZoomIn={handleZoomIn}
|
||||
onZoomOut={handleZoomOut}
|
||||
@@ -801,6 +767,27 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{allSensorsOverlayCircles.map(circle => {
|
||||
const size = 36
|
||||
const radius = size / 2
|
||||
const fill = hexWithAlpha(circle.colorHex, 0.2)
|
||||
return (
|
||||
<div
|
||||
key={`${circle.sensorId}-${Math.round(circle.left)}-${Math.round(circle.top)}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: circle.left - radius,
|
||||
top: circle.top - radius,
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '9999px',
|
||||
border: `2px solid ${circle.colorHex}`,
|
||||
backgroundColor: fill,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{renderOverlay && overlayPos && overlayData
|
||||
? renderOverlay({ anchor: overlayPos, info: overlayData })
|
||||
: null
|
||||
@@ -809,4 +796,4 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelViewer
|
||||
export default ModelViewer
|
||||
|
||||
96
frontend/components/model/sensorHighlight.ts
Normal file
96
frontend/components/model/sensorHighlight.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
AbstractMesh,
|
||||
HighlightLayer,
|
||||
Mesh,
|
||||
InstancedMesh,
|
||||
Color3,
|
||||
} from '@babylonjs/core'
|
||||
import * as statusColors from '../../lib/statusColors'
|
||||
|
||||
export const SENSOR_HIGHLIGHT_COLOR = new Color3(1, 1, 0)
|
||||
|
||||
const CRITICAL_COLOR3 = Color3.FromHexString(statusColors.STATUS_COLOR_CRITICAL)
|
||||
const WARNING_COLOR3 = Color3.FromHexString(statusColors.STATUS_COLOR_WARNING)
|
||||
const NORMAL_COLOR3 = Color3.FromHexString(statusColors.STATUS_COLOR_NORMAL)
|
||||
|
||||
export const statusToColor3 = (status?: string | null): Color3 => {
|
||||
if (!status) return SENSOR_HIGHLIGHT_COLOR
|
||||
const lower = status.toLowerCase()
|
||||
if (lower === 'critical') {
|
||||
return CRITICAL_COLOR3
|
||||
}
|
||||
if (lower === 'warning') {
|
||||
return WARNING_COLOR3
|
||||
}
|
||||
if (lower === 'info' || lower === 'normal' || lower === 'ok') {
|
||||
return NORMAL_COLOR3
|
||||
}
|
||||
if (status === statusColors.STATUS_COLOR_CRITICAL) return CRITICAL_COLOR3
|
||||
if (status === statusColors.STATUS_COLOR_WARNING) return WARNING_COLOR3
|
||||
if (status === statusColors.STATUS_COLOR_NORMAL) return NORMAL_COLOR3
|
||||
return SENSOR_HIGHLIGHT_COLOR
|
||||
}
|
||||
|
||||
export const getSensorIdFromMesh = (m: AbstractMesh | null): string | null => {
|
||||
if (!m) return null
|
||||
try {
|
||||
const meta: any = (m as any)?.metadata
|
||||
const extras: any = meta?.gltf?.extras ?? meta?.extras ?? (m as any)?.extras
|
||||
const sid =
|
||||
extras?.Sensor_ID ??
|
||||
extras?.sensor_id ??
|
||||
extras?.SERIAL_NUMBER ??
|
||||
extras?.serial_number ??
|
||||
extras?.detector_id
|
||||
if (sid != null) return String(sid).trim()
|
||||
const monitoringSensorInstance = extras?.bonsaiPset_ARBM_PSet_MonitoringSensor_Instance
|
||||
if (monitoringSensorInstance && typeof monitoringSensorInstance === 'string') {
|
||||
try {
|
||||
const parsedInstance = JSON.parse(monitoringSensorInstance)
|
||||
const instanceSensorId = parsedInstance?.Sensor_ID
|
||||
if (instanceSensorId != null) return String(instanceSensorId).trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const collectSensorMeshes = (meshes: AbstractMesh[]): AbstractMesh[] => {
|
||||
const result: AbstractMesh[] = []
|
||||
for (const m of meshes) {
|
||||
const sid = getSensorIdFromMesh(m)
|
||||
if (sid) result.push(m)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const applyHighlightToMeshes = (
|
||||
layer: HighlightLayer | null,
|
||||
highlightedRef: { current: AbstractMesh[] },
|
||||
meshesToHighlight: AbstractMesh[],
|
||||
getColor?: (mesh: AbstractMesh) => Color3 | null,
|
||||
) => {
|
||||
if (!layer) return
|
||||
for (const m of highlightedRef.current) {
|
||||
m.renderingGroupId = 0
|
||||
}
|
||||
highlightedRef.current = []
|
||||
layer.removeAllMeshes()
|
||||
|
||||
meshesToHighlight.forEach(mesh => {
|
||||
const color = getColor ? getColor(mesh) ?? SENSOR_HIGHLIGHT_COLOR : SENSOR_HIGHLIGHT_COLOR
|
||||
if (mesh instanceof Mesh) {
|
||||
mesh.renderingGroupId = 1
|
||||
highlightedRef.current.push(mesh)
|
||||
layer.addMesh(mesh, color)
|
||||
} else if (mesh instanceof InstancedMesh) {
|
||||
mesh.sourceMesh.renderingGroupId = 1
|
||||
highlightedRef.current.push(mesh.sourceMesh)
|
||||
layer.addMesh(mesh.sourceMesh, color)
|
||||
}
|
||||
})
|
||||
}
|
||||
72
frontend/components/model/sensorHighlightOverlay.ts
Normal file
72
frontend/components/model/sensorHighlightOverlay.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { AbstractMesh, Matrix, Vector3, Scene, Engine } from '@babylonjs/core'
|
||||
import * as statusColors from '../../lib/statusColors'
|
||||
import { getSensorIdFromMesh } from './sensorHighlight'
|
||||
|
||||
export interface SensorOverlayCircle {
|
||||
sensorId: string
|
||||
left: number
|
||||
top: number
|
||||
colorHex: string
|
||||
}
|
||||
|
||||
const parseHexColor = (hex: string) => {
|
||||
let clean = hex.trim().replace('#', '')
|
||||
if (clean.length === 3) {
|
||||
clean = clean
|
||||
.split('')
|
||||
.map(c => c + c)
|
||||
.join('')
|
||||
}
|
||||
const num = parseInt(clean, 16)
|
||||
const r = (num >> 16) & 255
|
||||
const g = (num >> 8) & 255
|
||||
const b = num & 255
|
||||
return { r, g, b }
|
||||
}
|
||||
|
||||
export const hexWithAlpha = (hex: string, alpha: number) => {
|
||||
const { r, g, b } = parseHexColor(hex)
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
||||
}
|
||||
|
||||
export const computeSensorOverlayCircles = (params: {
|
||||
scene: Scene
|
||||
engine: Engine
|
||||
meshes: AbstractMesh[]
|
||||
sensorStatusMap?: Record<string, string>
|
||||
}): SensorOverlayCircle[] => {
|
||||
const { scene, engine, meshes, sensorStatusMap } = params
|
||||
const camera = scene.activeCamera
|
||||
if (!camera) return []
|
||||
const viewport = camera.viewport.toGlobal(engine.getRenderWidth(), engine.getRenderHeight())
|
||||
const result: SensorOverlayCircle[] = []
|
||||
|
||||
for (const mesh of meshes) {
|
||||
const sensorId = getSensorIdFromMesh(mesh)
|
||||
if (!sensorId) continue
|
||||
|
||||
const bbox =
|
||||
typeof mesh.getHierarchyBoundingVectors === 'function'
|
||||
? mesh.getHierarchyBoundingVectors()
|
||||
: {
|
||||
min: mesh.getBoundingInfo().boundingBox.minimumWorld,
|
||||
max: mesh.getBoundingInfo().boundingBox.maximumWorld,
|
||||
}
|
||||
const center = bbox.min.add(bbox.max).scale(0.5)
|
||||
|
||||
const projected = Vector3.Project(center, Matrix.Identity(), scene.getTransformMatrix(), viewport)
|
||||
if (!projected) continue
|
||||
|
||||
const statusColor = sensorStatusMap?.[sensorId] || statusColors.STATUS_COLOR_NORMAL
|
||||
|
||||
result.push({
|
||||
sensorId,
|
||||
left: projected.x,
|
||||
top: projected.y,
|
||||
colorHex: statusColor,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import useNavigationStore from '@/app/store/navigationStore'
|
||||
import AreaChart from '../dashboard/AreaChart'
|
||||
import * as statusColors from '../../lib/statusColors'
|
||||
|
||||
interface AlertType {
|
||||
id: number
|
||||
@@ -18,7 +19,7 @@ interface AlertType {
|
||||
acknowledged: boolean
|
||||
priority: string
|
||||
}
|
||||
|
||||
|
||||
interface AlertMenuProps {
|
||||
alert: AlertType
|
||||
isOpen: boolean
|
||||
@@ -27,7 +28,7 @@ interface AlertMenuProps {
|
||||
compact?: boolean
|
||||
anchor?: { left: number; top: number } | null
|
||||
}
|
||||
|
||||
|
||||
const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatusText, compact = false, anchor = null }) => {
|
||||
const router = useRouter()
|
||||
const { setSelectedDetector, currentObject } = useNavigationStore()
|
||||
@@ -56,17 +57,19 @@ const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatus
|
||||
}
|
||||
|
||||
const getStatusColorCircle = (status: string) => {
|
||||
// Use hex colors from Alerts submenu system
|
||||
if (status === '#b3261e') return 'bg-red-500'
|
||||
if (status === '#fd7c22') return 'bg-orange-500'
|
||||
if (status === '#00ff00') return 'bg-green-500'
|
||||
if (status === statusColors.STATUS_COLOR_CRITICAL) return 'bg-red-500'
|
||||
if (status === statusColors.STATUS_COLOR_WARNING) return 'bg-orange-500'
|
||||
if (status === statusColors.STATUS_COLOR_NORMAL) return 'bg-green-500'
|
||||
|
||||
// Fallback for text-based status
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'critical': return 'bg-red-500'
|
||||
case 'warning': return 'bg-orange-500'
|
||||
case 'normal': return 'bg-green-500'
|
||||
default: return 'bg-gray-500'
|
||||
case 'critical':
|
||||
return 'bg-red-500'
|
||||
case 'warning':
|
||||
return 'bg-orange-500'
|
||||
case 'normal':
|
||||
return 'bg-green-500'
|
||||
default:
|
||||
return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,4 +342,4 @@ const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatus
|
||||
)
|
||||
}
|
||||
|
||||
export default AlertMenu
|
||||
export default AlertMenu
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import * as statusColors from '../../lib/statusColors'
|
||||
|
||||
interface DetectorsDataType {
|
||||
detectors: Record<string, DetectorType>
|
||||
}
|
||||
|
||||
|
||||
interface FloorNavigationProps {
|
||||
objectId?: string
|
||||
detectorsData: DetectorsDataType
|
||||
@@ -13,7 +14,7 @@ interface FloorNavigationProps {
|
||||
onClose?: () => void
|
||||
is3DReady?: boolean
|
||||
}
|
||||
|
||||
|
||||
interface DetectorType {
|
||||
detector_id: number
|
||||
name: string
|
||||
@@ -34,7 +35,7 @@ interface DetectorType {
|
||||
priority: string
|
||||
}>
|
||||
}
|
||||
|
||||
|
||||
const FloorNavigation: React.FC<FloorNavigationProps> = (props) => {
|
||||
const { objectId, detectorsData, onDetectorMenuClick, onClose, is3DReady = true } = props
|
||||
const [expandedFloors, setExpandedFloors] = useState<Set<number>>(new Set())
|
||||
@@ -81,19 +82,27 @@ const FloorNavigation: React.FC<FloorNavigationProps> = (props) => {
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case '#b3261e': return 'bg-red-500'
|
||||
case '#fd7c22': return 'bg-orange-500'
|
||||
case '#00ff00': return 'bg-green-500'
|
||||
default: return 'bg-gray-500'
|
||||
case statusColors.STATUS_COLOR_CRITICAL:
|
||||
return 'bg-red-500'
|
||||
case statusColors.STATUS_COLOR_WARNING:
|
||||
return 'bg-orange-500'
|
||||
case statusColors.STATUS_COLOR_NORMAL:
|
||||
return 'bg-green-500'
|
||||
default:
|
||||
return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case '#b3261e': return 'Критический'
|
||||
case '#fd7c22': return 'Предупреждение'
|
||||
case '#00ff00': return 'Норма'
|
||||
default: return 'Неизвестно'
|
||||
case statusColors.STATUS_COLOR_CRITICAL:
|
||||
return 'Критический'
|
||||
case statusColors.STATUS_COLOR_WARNING:
|
||||
return 'Предупреждение'
|
||||
case statusColors.STATUS_COLOR_NORMAL:
|
||||
return 'Норма'
|
||||
default:
|
||||
return 'Неизвестно'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,4 +232,4 @@ const FloorNavigation: React.FC<FloorNavigationProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default FloorNavigation
|
||||
export default FloorNavigation
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import * as statusColors from '../../lib/statusColors'
|
||||
|
||||
interface DetectorsDataType {
|
||||
detectors: Record<string, DetectorType>
|
||||
@@ -58,19 +59,27 @@ const ListOfDetectors: React.FC<ListOfDetectorsProps> = ({ objectId, detectorsDa
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case '#b3261e': return 'bg-red-500'
|
||||
case '#fd7c22': return 'bg-orange-500'
|
||||
case '#00ff00': return 'bg-green-500'
|
||||
default: return 'bg-gray-500'
|
||||
case statusColors.STATUS_COLOR_CRITICAL:
|
||||
return 'bg-red-500'
|
||||
case statusColors.STATUS_COLOR_WARNING:
|
||||
return 'bg-orange-500'
|
||||
case statusColors.STATUS_COLOR_NORMAL:
|
||||
return 'bg-green-500'
|
||||
default:
|
||||
return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case '#b3261e': return 'Критический'
|
||||
case '#fd7c22': return 'Предупреждение'
|
||||
case '#00ff00': return 'Норма'
|
||||
default: return 'Неизвестно'
|
||||
case statusColors.STATUS_COLOR_CRITICAL:
|
||||
return 'Критический'
|
||||
case statusColors.STATUS_COLOR_WARNING:
|
||||
return 'Предупреждение'
|
||||
case statusColors.STATUS_COLOR_NORMAL:
|
||||
return 'Норма'
|
||||
default:
|
||||
return 'Неизвестно'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,4 +177,4 @@ const ListOfDetectors: React.FC<ListOfDetectorsProps> = ({ objectId, detectorsDa
|
||||
)
|
||||
}
|
||||
|
||||
export default ListOfDetectors
|
||||
export default ListOfDetectors
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import * as statusColors from '../../lib/statusColors'
|
||||
|
||||
interface DetectorInfoType {
|
||||
detector_id: number
|
||||
@@ -52,9 +53,9 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
if (status === '#b3261e') return 'text-red-400'
|
||||
if (status === '#fd7c22') return 'text-orange-400'
|
||||
if (status === '#4caf50') return 'text-green-400'
|
||||
if (status === statusColors.STATUS_COLOR_CRITICAL) return 'text-red-400'
|
||||
if (status === statusColors.STATUS_COLOR_WARNING) return 'text-orange-400'
|
||||
if (status === statusColors.STATUS_COLOR_NORMAL || status === '#4caf50') return 'text-green-400'
|
||||
return 'text-gray-400'
|
||||
}
|
||||
|
||||
@@ -147,9 +148,13 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
|
||||
style={{ backgroundColor: detectorInfo.status }}
|
||||
></div>
|
||||
<span className={`text-sm font-medium ${getStatusColor(detectorInfo.status)}`}>
|
||||
{detectorInfo.status === '#b3261e' ? 'Критический' :
|
||||
detectorInfo.status === '#fd7c22' ? 'Предупреждение' :
|
||||
detectorInfo.status === '#4caf50' ? 'Нормальный' : 'Неизвестно'}
|
||||
{detectorInfo.status === statusColors.STATUS_COLOR_CRITICAL
|
||||
? 'Критический'
|
||||
: detectorInfo.status === statusColors.STATUS_COLOR_WARNING
|
||||
? 'Предупреждение'
|
||||
: detectorInfo.status === statusColors.STATUS_COLOR_NORMAL || detectorInfo.status === '#4caf50'
|
||||
? 'Нормальный'
|
||||
: 'Неизвестно'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,4 +186,4 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationDetectorInfo
|
||||
export default NotificationDetectorInfo
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import * as statusColors from '../../lib/statusColors';
|
||||
|
||||
interface NotificationType {
|
||||
id: number;
|
||||
@@ -78,19 +79,27 @@ const ReportsList: React.FC<ReportsListProps> = ({ detectorsData, initialSearchT
|
||||
|
||||
const getStatusColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'critical': return '#b3261e';
|
||||
case 'warning': return '#fd7c22';
|
||||
case 'info': return '#00ff00';
|
||||
default: return '#666';
|
||||
case 'critical':
|
||||
return statusColors.STATUS_COLOR_CRITICAL;
|
||||
case 'warning':
|
||||
return statusColors.STATUS_COLOR_WARNING;
|
||||
case 'info':
|
||||
return statusColors.STATUS_COLOR_NORMAL;
|
||||
default:
|
||||
return statusColors.STATUS_COLOR_UNKNOWN;
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return '#b3261e';
|
||||
case 'medium': return '#fd7c22';
|
||||
case 'low': return '#00ff00';
|
||||
default: return '#666';
|
||||
case 'high':
|
||||
return statusColors.STATUS_COLOR_CRITICAL;
|
||||
case 'medium':
|
||||
return statusColors.STATUS_COLOR_WARNING;
|
||||
case 'low':
|
||||
return statusColors.STATUS_COLOR_NORMAL;
|
||||
default:
|
||||
return statusColors.STATUS_COLOR_UNKNOWN;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -278,4 +287,4 @@ const ReportsList: React.FC<ReportsListProps> = ({ detectorsData, initialSearchT
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportsList;
|
||||
export default ReportsList;
|
||||
|
||||
Reference in New Issue
Block a user