Overhaul of the highlight system

This commit is contained in:
iv_vuytsik
2026-01-21 03:16:52 +03:00
parent ce7e39debf
commit 87a1a628d3
13 changed files with 481 additions and 259 deletions

View File

@@ -1,5 +1,5 @@
'use client' 'use client'
import React, { useEffect, useCallback, useState } from 'react' import React, { useEffect, useCallback, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import Sidebar from '../../../components/ui/Sidebar' import Sidebar from '../../../components/ui/Sidebar'
@@ -14,7 +14,8 @@ import Notifications from '../../../components/notifications/Notifications'
import NotificationDetectorInfo from '../../../components/notifications/NotificationDetectorInfo' import NotificationDetectorInfo from '../../../components/notifications/NotificationDetectorInfo'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import type { ModelViewerProps } from '../../../components/model/ModelViewer' import type { ModelViewerProps } from '../../../components/model/ModelViewer'
import * as statusColors from '../../../lib/statusColors'
const ModelViewer = dynamic<ModelViewerProps>(() => import('../../../components/model/ModelViewer'), { const ModelViewer = dynamic<ModelViewerProps>(() => import('../../../components/model/ModelViewer'), {
ssr: false, ssr: false,
loading: () => ( loading: () => (
@@ -109,6 +110,15 @@ const NavigationPage: React.FC = () => {
const [isModelReady, setIsModelReady] = useState(false) const [isModelReady, setIsModelReady] = useState(false)
const [focusedSensorId, setFocusedSensorId] = useState<string | null>(null) const [focusedSensorId, setFocusedSensorId] = useState<string | null>(null)
const [highlightAllSensors, setHighlightAllSensors] = useState(false) const [highlightAllSensors, setHighlightAllSensors] = useState(false)
const sensorStatusMap = React.useMemo(() => {
const map: Record<string, string> = {}
Object.values(detectorsData.detectors).forEach(d => {
if (d.serial_number && d.status) {
map[String(d.serial_number).trim()] = d.status
}
})
return map
}, [detectorsData])
useEffect(() => { useEffect(() => {
if (selectedDetector === null && selectedAlert === null) { if (selectedDetector === null && selectedAlert === null) {
@@ -353,13 +363,13 @@ const NavigationPage: React.FC = () => {
const getStatusText = (status: string) => { const getStatusText = (status: string) => {
const s = (status || '').toLowerCase() const s = (status || '').toLowerCase()
switch (s) { switch (s) {
case '#b3261e': case statusColors.STATUS_COLOR_CRITICAL:
case 'critical': case 'critical':
return 'Критический' return 'Критический'
case '#fd7c22': case statusColors.STATUS_COLOR_WARNING:
case 'warning': case 'warning':
return 'Предупреждение' return 'Предупреждение'
case '#00ff00': case statusColors.STATUS_COLOR_NORMAL:
case 'normal': case 'normal':
return 'Норма' return 'Норма'
default: default:
@@ -546,6 +556,7 @@ const NavigationPage: React.FC = () => {
activeMenu={showSensors ? 'sensors' : showFloorNavigation ? 'floor' : showListOfDetectors ? 'detectors' : null} activeMenu={showSensors ? 'sensors' : showFloorNavigation ? 'floor' : showListOfDetectors ? 'detectors' : null}
focusSensorId={focusedSensorId} focusSensorId={focusedSensorId}
highlightAllSensors={highlightAllSensors} highlightAllSensors={highlightAllSensors}
sensorStatusMap={sensorStatusMap}
isSensorSelectionEnabled={showSensors || showFloorNavigation || showListOfDetectors} isSensorSelectionEnabled={showSensors || showFloorNavigation || showListOfDetectors}
onSensorPick={handleSensorSelection} onSensorPick={handleSensorSelection}
renderOverlay={({ anchor }) => ( renderOverlay={({ anchor }) => (
@@ -580,4 +591,4 @@ const NavigationPage: React.FC = () => {
) )
} }
export default NavigationPage export default NavigationPage

View File

@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth' import { authOptions } from '@/lib/auth'
import * as statusColors from '@/lib/statusColors'
export async function GET() { export async function GET() {
try { try {
@@ -50,15 +51,15 @@ export async function GET() {
} }
const statusToColor: Record<string, string> = { const statusToColor: Record<string, string> = {
critical: '#b3261e', critical: statusColors.STATUS_COLOR_CRITICAL,
warning: '#fd7c22', warning: statusColors.STATUS_COLOR_WARNING,
normal: '#00ff00', normal: statusColors.STATUS_COLOR_NORMAL,
} }
const transformedDetectors: Record<string, any> = {} const transformedDetectors: Record<string, any> = {}
const detectorsObj = detectorsPayload?.detectors ?? {} const detectorsObj = detectorsPayload?.detectors ?? {}
for (const [key, sensor] of Object.entries<any>(detectorsObj)) { for (const [key, sensor] of Object.entries<any>(detectorsObj)) {
const color = statusToColor[sensor.status] ?? '#00ff00' const color = statusToColor[sensor.status] ?? statusColors.STATUS_COLOR_NORMAL
const objectId = titleToIdMap[sensor.object] || sensor.object const objectId = titleToIdMap[sensor.object] || sensor.object
transformedDetectors[key] = { transformedDetectors[key] = {
...sensor, ...sensor,
@@ -100,4 +101,4 @@ export async function GET() {
{ status: 500 } { status: 500 }
) )
} }
} }

View File

@@ -1,4 +1,5 @@
import React, { useState, useMemo } from 'react' import React, { useState, useMemo } from 'react'
import * as statusColors from '../../lib/statusColors'
interface AlertItem { interface AlertItem {
id: number id: number
@@ -31,10 +32,14 @@ const AlertsList: React.FC<AlertsListProps> = ({ alerts, onAcknowledgeToggle, in
const getStatusColor = (type: string) => { const getStatusColor = (type: string) => {
switch (type) { switch (type) {
case 'critical': return '#b3261e' case 'critical':
case 'warning': return '#fd7c22' return statusColors.STATUS_COLOR_CRITICAL
case 'info': return '#00ff00' case 'warning':
default: return '#666' 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"> <td className="py-4">
<span <span
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium text-white" 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' ? 'Средний' : 'Низкий'} {item.priority === 'high' ? 'Высокий' : item.priority === 'medium' ? 'Средний' : 'Низкий'}
</span> </span>
@@ -142,4 +154,4 @@ const AlertsList: React.FC<AlertsListProps> = ({ alerts, onAcknowledgeToggle, in
) )
} }
export default AlertsList export default AlertsList

View File

@@ -1,7 +1,8 @@
'use client' 'use client'
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import * as statusColors from '../../lib/statusColors'
interface Detector { interface Detector {
detector_id: number detector_id: number
name: string name: string
@@ -30,14 +31,14 @@ interface RawDetector {
priority: string priority: string
}> }>
} }
interface DetectorListProps { interface DetectorListProps {
objectId?: string objectId?: string
selectedDetectors: number[] selectedDetectors: number[]
onDetectorSelect: (detectorId: number, selected: boolean) => void onDetectorSelect: (detectorId: number, selected: boolean) => void
initialSearchTerm?: string initialSearchTerm?: string
} }
const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors, onDetectorSelect, initialSearchTerm = '' }) => { const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors, onDetectorSelect, initialSearchTerm = '' }) => {
const [detectors, setDetectors] = useState<Detector[]>([]) const [detectors, setDetectors] = useState<Detector[]>([])
const [searchTerm, setSearchTerm] = useState<string>(initialSearchTerm) const [searchTerm, setSearchTerm] = useState<string>(initialSearchTerm)
@@ -150,12 +151,15 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
<td className="py-3"> <td className="py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={`w-3 h-3 rounded-full`} className="w-3 h-3 rounded-full"
style={{ backgroundColor: detector.status }} style={{ backgroundColor: detector.status }}
></div> ></div>
<span className="text-sm text-gray-300"> <span className="text-sm text-gray-300">
{detector.status === '#b3261e' ? 'Критическое' : {detector.status === statusColors.STATUS_COLOR_CRITICAL
detector.status === '#fd7c22' ? 'Предупреждение' : 'Норма'} ? 'Критическое'
: detector.status === statusColors.STATUS_COLOR_WARNING
? 'Предупреждение'
: 'Норма'}
</span> </span>
</div> </div>
</td> </td>
@@ -187,15 +191,15 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
<div className="text-sm text-gray-400">Всего</div> <div className="text-sm text-gray-400">Всего</div>
</div> </div>
<div className="bg-[#161824] p-4 rounded-lg"> <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 className="text-sm text-gray-400">Норма</div>
</div> </div>
<div className="bg-[#161824] p-4 rounded-lg"> <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 className="text-sm text-gray-400">Предупреждения</div>
</div> </div>
<div className="bg-[#161824] p-4 rounded-lg"> <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 className="text-sm text-gray-400">Критические</div>
</div> </div>
</div> </div>
@@ -209,4 +213,4 @@ const DetectorList: React.FC<DetectorListProps> = ({ objectId, selectedDetectors
) )
} }
export default DetectorList export default DetectorList

View File

@@ -12,9 +12,7 @@ import {
Color4, Color4,
AbstractMesh, AbstractMesh,
Nullable, Nullable,
HighlightLayer, HighlightLayer,
Mesh,
InstancedMesh,
Animation, Animation,
CubicEase, CubicEase,
EasingFunction, EasingFunction,
@@ -24,9 +22,19 @@ import {
Matrix, Matrix,
} from '@babylonjs/core' } from '@babylonjs/core'
import '@babylonjs/loaders' import '@babylonjs/loaders'
import SceneToolbar from './SceneToolbar'; import SceneToolbar from './SceneToolbar';
import LoadingSpinner from '../ui/LoadingSpinner' import LoadingSpinner from '../ui/LoadingSpinner'
import {
getSensorIdFromMesh,
collectSensorMeshes,
applyHighlightToMeshes,
statusToColor3,
} from './sensorHighlight'
import {
computeSensorOverlayCircles,
hexWithAlpha,
} from './sensorHighlightOverlay'
export interface ModelViewerProps { export interface ModelViewerProps {
modelPath: string modelPath: string
@@ -45,6 +53,7 @@ export interface ModelViewerProps {
isSensorSelectionEnabled?: boolean isSensorSelectionEnabled?: boolean
onSensorPick?: (sensorId: string | null) => void onSensorPick?: (sensorId: string | null) => void
highlightAllSensors?: boolean highlightAllSensors?: boolean
sensorStatusMap?: Record<string, string>
} }
const ModelViewer: React.FC<ModelViewerProps> = ({ const ModelViewer: React.FC<ModelViewerProps> = ({
@@ -57,6 +66,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
isSensorSelectionEnabled, isSensorSelectionEnabled,
onSensorPick, onSensorPick,
highlightAllSensors, highlightAllSensors,
sensorStatusMap,
}) => { }) => {
const canvasRef = useRef<HTMLCanvasElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null)
const engineRef = useRef<Nullable<Engine>>(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 [overlayData, setOverlayData] = useState<{ name?: string; sensorId?: string } | null>(null)
const [modelReady, setModelReady] = useState(false) const [modelReady, setModelReady] = useState(false)
const [panActive, setPanActive] = 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); const handlePan = () => setPanActive(!panActive);
@@ -222,7 +236,41 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
if (!canvasRef.current || isInitializedRef.current) return if (!canvasRef.current || isInitializedRef.current) return
const canvas = canvasRef.current 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 engineRef.current = engine
engine.runRenderLoop(() => { engine.runRenderLoop(() => {
@@ -260,7 +308,14 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
fillLight.intensity = 0.3 fillLight.intensity = 0.3
fillLight.diffuse = new Color3(0.8, 0.8, 1) 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 highlightLayerRef.current = hl
const handleResize = () => { const handleResize = () => {
@@ -285,7 +340,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
} }
sceneRef.current = null sceneRef.current = null
} }
}, []) }, [onError])
useEffect(() => { useEffect(() => {
if (!isInitializedRef.current || isDisposedRef.current) { if (!isInitializedRef.current || isDisposedRef.current) {
@@ -427,19 +482,30 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
}, [modelPath, onError, onModelLoaded]) }, [modelPath, onError, onModelLoaded])
useEffect(() => { useEffect(() => {
console.log('[ModelViewer] highlightAllSensors effect triggered:', { highlightAllSensors, modelReady, sceneReady: !!sceneRef.current })
if (!sceneRef.current || isDisposedRef.current || !modelReady) return if (!sceneRef.current || isDisposedRef.current || !modelReady) return
// Если включено выделение всех сенсоров - выделяем их все
if (highlightAllSensors) { if (highlightAllSensors) {
console.log('[ModelViewer] Calling highlightAllSensorMeshes()') const allMeshes = importedMeshesRef.current || []
highlightAllSensorMeshes() 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 return
} }
const sensorId = (focusSensorId ?? '').trim() const sensorId = (focusSensorId ?? '').trim()
if (!sensorId) { if (!sensorId) {
console.log('[ModelViewer] Focus cleared (no Sensor_ID provided)')
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 } for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
highlightedMeshesRef.current = [] highlightedMeshesRef.current = []
highlightLayerRef.current?.removeAllMeshes() highlightLayerRef.current?.removeAllMeshes()
@@ -452,7 +518,6 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
const allMeshes = importedMeshesRef.current || [] const allMeshes = importedMeshesRef.current || []
if (allMeshes.length === 0) { if (allMeshes.length === 0) {
console.warn('[ModelViewer] No meshes available for sensor matching')
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 } for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
highlightedMeshesRef.current = [] highlightedMeshesRef.current = []
highlightLayerRef.current?.removeAllMeshes() highlightLayerRef.current?.removeAllMeshes()
@@ -462,44 +527,8 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
return return
} }
const sensorMeshes = allMeshes.filter((m: any) => { const sensorMeshes = collectSensorMeshes(allMeshes)
try { const chosen = sensorMeshes.find(m => getSensorIdFromMesh(m) === sensorId)
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
}
})
console.log('[ModelViewer] Sensor focus', { console.log('[ModelViewer] Sensor focus', {
requested: sensorId, 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('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) Animation.CreateAndStartAnimation('camRadius', camera, 'radius', frameRate, totalFrames, camera.radius, targetRadius, Animation.ANIMATIONLOOPMODE_CONSTANT, ease)
const hl = highlightLayerRef.current applyHighlightToMeshes(
if (hl) { highlightLayerRef.current,
// Переключаем группу рендеринга для предыдущего выделенного меша highlightedMeshesRef,
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 } [chosen],
highlightedMeshesRef.current = [] mesh => {
const sid = getSensorIdFromMesh(mesh)
hl.removeAllMeshes() const status = sid ? sensorStatusMap?.[sid] : undefined
if (chosen instanceof Mesh) { return statusToColor3(status ?? null)
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))
}
}
}
}
chosenMeshRef.current = chosen chosenMeshRef.current = chosen
setOverlayData({ name: chosen.name, sensorId }) setOverlayData({ name: chosen.name, sensorId })
} catch (error) { } catch {
console.error('[ModelViewer] Error focusing on sensor mesh:', error)
for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 } for (const m of highlightedMeshesRef.current) { m.renderingGroupId = 0 }
highlightedMeshesRef.current = [] highlightedMeshesRef.current = []
highlightLayerRef.current?.removeAllMeshes() highlightLayerRef.current?.removeAllMeshes()
@@ -582,99 +593,6 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [focusSensorId, modelReady, highlightAllSensors]) }, [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(() => { useEffect(() => {
const scene = sceneRef.current const scene = sceneRef.current
@@ -701,7 +619,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
return () => { return () => {
scene.onPointerObservable.remove(pickObserver) scene.onPointerObservable.remove(pickObserver)
} }
}, [modelReady, isSensorSelectionEnabled, onSensorPick, getSensorIdFromMesh]) }, [modelReady, isSensorSelectionEnabled, onSensorPick])
// Расчет позиции оверлея // Расчет позиции оверлея
const computeOverlayPosition = React.useCallback((mesh: AbstractMesh | null) => { const computeOverlayPosition = React.useCallback((mesh: AbstractMesh | null) => {
@@ -733,6 +651,41 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
setOverlayPos(pos) setOverlayPos(pos)
}, [overlayData, computeOverlayPosition]) }, [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(() => { useEffect(() => {
if (!sceneRef.current || !chosenMeshRef.current || !overlayData) return if (!sceneRef.current || !chosenMeshRef.current || !overlayData) return
@@ -767,10 +720,24 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className={`w-full h-full outline-none block transition-opacity duration-500 ${ 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"> <div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-50">
<LoadingSpinner <LoadingSpinner
progress={loadingProgress} progress={loadingProgress}
@@ -778,8 +745,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
strokeWidth={8} strokeWidth={8}
/> />
</div> </div>
)} ) : !modelReady ? (
{!modelReady && !isLoading && (
<div className="absolute inset-0 bg-gray-900 flex items-center justify-center z-40"> <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-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"> <div className="text-gray-400 text-lg font-semibold mb-4">
@@ -790,7 +756,7 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
</div> </div>
</div> </div>
</div> </div>
)} ) : null}
<SceneToolbar <SceneToolbar
onZoomIn={handleZoomIn} onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut} 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 && overlayPos && overlayData
? renderOverlay({ anchor: overlayPos, info: overlayData }) ? renderOverlay({ anchor: overlayPos, info: overlayData })
: null : null
@@ -809,4 +796,4 @@ const ModelViewer: React.FC<ModelViewerProps> = ({
) )
} }
export default ModelViewer export default ModelViewer

View 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)
}
})
}

View 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
}

View File

@@ -4,6 +4,7 @@ import React from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import useNavigationStore from '@/app/store/navigationStore' import useNavigationStore from '@/app/store/navigationStore'
import AreaChart from '../dashboard/AreaChart' import AreaChart from '../dashboard/AreaChart'
import * as statusColors from '../../lib/statusColors'
interface AlertType { interface AlertType {
id: number id: number
@@ -18,7 +19,7 @@ interface AlertType {
acknowledged: boolean acknowledged: boolean
priority: string priority: string
} }
interface AlertMenuProps { interface AlertMenuProps {
alert: AlertType alert: AlertType
isOpen: boolean isOpen: boolean
@@ -27,7 +28,7 @@ interface AlertMenuProps {
compact?: boolean compact?: boolean
anchor?: { left: number; top: number } | null anchor?: { left: number; top: number } | null
} }
const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatusText, compact = false, anchor = null }) => { const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatusText, compact = false, anchor = null }) => {
const router = useRouter() const router = useRouter()
const { setSelectedDetector, currentObject } = useNavigationStore() const { setSelectedDetector, currentObject } = useNavigationStore()
@@ -56,17 +57,19 @@ const AlertMenu: React.FC<AlertMenuProps> = ({ alert, isOpen, onClose, getStatus
} }
const getStatusColorCircle = (status: string) => { const getStatusColorCircle = (status: string) => {
// Use hex colors from Alerts submenu system if (status === statusColors.STATUS_COLOR_CRITICAL) return 'bg-red-500'
if (status === '#b3261e') return 'bg-red-500' if (status === statusColors.STATUS_COLOR_WARNING) return 'bg-orange-500'
if (status === '#fd7c22') return 'bg-orange-500' if (status === statusColors.STATUS_COLOR_NORMAL) return 'bg-green-500'
if (status === '#00ff00') return 'bg-green-500'
// Fallback for text-based status
switch (status?.toLowerCase()) { switch (status?.toLowerCase()) {
case 'critical': return 'bg-red-500' case 'critical':
case 'warning': return 'bg-orange-500' return 'bg-red-500'
case 'normal': return 'bg-green-500' case 'warning':
default: return 'bg-gray-500' 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

View File

@@ -1,11 +1,12 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState } from 'react'
import * as statusColors from '../../lib/statusColors'
interface DetectorsDataType { interface DetectorsDataType {
detectors: Record<string, DetectorType> detectors: Record<string, DetectorType>
} }
interface FloorNavigationProps { interface FloorNavigationProps {
objectId?: string objectId?: string
detectorsData: DetectorsDataType detectorsData: DetectorsDataType
@@ -13,7 +14,7 @@ interface FloorNavigationProps {
onClose?: () => void onClose?: () => void
is3DReady?: boolean is3DReady?: boolean
} }
interface DetectorType { interface DetectorType {
detector_id: number detector_id: number
name: string name: string
@@ -34,7 +35,7 @@ interface DetectorType {
priority: string priority: string
}> }>
} }
const FloorNavigation: React.FC<FloorNavigationProps> = (props) => { const FloorNavigation: React.FC<FloorNavigationProps> = (props) => {
const { objectId, detectorsData, onDetectorMenuClick, onClose, is3DReady = true } = props const { objectId, detectorsData, onDetectorMenuClick, onClose, is3DReady = true } = props
const [expandedFloors, setExpandedFloors] = useState<Set<number>>(new Set()) const [expandedFloors, setExpandedFloors] = useState<Set<number>>(new Set())
@@ -81,19 +82,27 @@ const FloorNavigation: React.FC<FloorNavigationProps> = (props) => {
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case '#b3261e': return 'bg-red-500' case statusColors.STATUS_COLOR_CRITICAL:
case '#fd7c22': return 'bg-orange-500' return 'bg-red-500'
case '#00ff00': return 'bg-green-500' case statusColors.STATUS_COLOR_WARNING:
default: return 'bg-gray-500' return 'bg-orange-500'
case statusColors.STATUS_COLOR_NORMAL:
return 'bg-green-500'
default:
return 'bg-gray-500'
} }
} }
const getStatusText = (status: string) => { const getStatusText = (status: string) => {
switch (status) { switch (status) {
case '#b3261e': return 'Критический' case statusColors.STATUS_COLOR_CRITICAL:
case '#fd7c22': return 'Предупреждение' return 'Критический'
case '#00ff00': return 'Норма' case statusColors.STATUS_COLOR_WARNING:
default: return 'Неизвестно' 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

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState } from 'react'
import * as statusColors from '../../lib/statusColors'
interface DetectorsDataType { interface DetectorsDataType {
detectors: Record<string, DetectorType> detectors: Record<string, DetectorType>
@@ -58,19 +59,27 @@ const ListOfDetectors: React.FC<ListOfDetectorsProps> = ({ objectId, detectorsDa
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case '#b3261e': return 'bg-red-500' case statusColors.STATUS_COLOR_CRITICAL:
case '#fd7c22': return 'bg-orange-500' return 'bg-red-500'
case '#00ff00': return 'bg-green-500' case statusColors.STATUS_COLOR_WARNING:
default: return 'bg-gray-500' return 'bg-orange-500'
case statusColors.STATUS_COLOR_NORMAL:
return 'bg-green-500'
default:
return 'bg-gray-500'
} }
} }
const getStatusText = (status: string) => { const getStatusText = (status: string) => {
switch (status) { switch (status) {
case '#b3261e': return 'Критический' case statusColors.STATUS_COLOR_CRITICAL:
case '#fd7c22': return 'Предупреждение' return 'Критический'
case '#00ff00': return 'Норма' case statusColors.STATUS_COLOR_WARNING:
default: return 'Неизвестно' 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

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import React from 'react' import React from 'react'
import * as statusColors from '../../lib/statusColors'
interface DetectorInfoType { interface DetectorInfoType {
detector_id: number detector_id: number
@@ -52,9 +53,9 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
} }
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
if (status === '#b3261e') return 'text-red-400' if (status === statusColors.STATUS_COLOR_CRITICAL) return 'text-red-400'
if (status === '#fd7c22') return 'text-orange-400' if (status === statusColors.STATUS_COLOR_WARNING) return 'text-orange-400'
if (status === '#4caf50') return 'text-green-400' if (status === statusColors.STATUS_COLOR_NORMAL || status === '#4caf50') return 'text-green-400'
return 'text-gray-400' return 'text-gray-400'
} }
@@ -147,9 +148,13 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
style={{ backgroundColor: detectorInfo.status }} style={{ backgroundColor: detectorInfo.status }}
></div> ></div>
<span className={`text-sm font-medium ${getStatusColor(detectorInfo.status)}`}> <span className={`text-sm font-medium ${getStatusColor(detectorInfo.status)}`}>
{detectorInfo.status === '#b3261e' ? 'Критический' : {detectorInfo.status === statusColors.STATUS_COLOR_CRITICAL
detectorInfo.status === '#fd7c22' ? 'Предупреждение' : ? 'Критический'
detectorInfo.status === '#4caf50' ? 'Нормальный' : 'Неизвестно'} : detectorInfo.status === statusColors.STATUS_COLOR_WARNING
? 'Предупреждение'
: detectorInfo.status === statusColors.STATUS_COLOR_NORMAL || detectorInfo.status === '#4caf50'
? 'Нормальный'
: 'Неизвестно'}
</span> </span>
</div> </div>
</div> </div>
@@ -181,4 +186,4 @@ const NotificationDetectorInfo: React.FC<NotificationDetectorInfoProps> = ({ det
) )
} }
export default NotificationDetectorInfo export default NotificationDetectorInfo

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import * as statusColors from '../../lib/statusColors';
interface NotificationType { interface NotificationType {
id: number; id: number;
@@ -78,19 +79,27 @@ const ReportsList: React.FC<ReportsListProps> = ({ detectorsData, initialSearchT
const getStatusColor = (type: string) => { const getStatusColor = (type: string) => {
switch (type) { switch (type) {
case 'critical': return '#b3261e'; case 'critical':
case 'warning': return '#fd7c22'; return statusColors.STATUS_COLOR_CRITICAL;
case 'info': return '#00ff00'; case 'warning':
default: return '#666'; return statusColors.STATUS_COLOR_WARNING;
case 'info':
return statusColors.STATUS_COLOR_NORMAL;
default:
return statusColors.STATUS_COLOR_UNKNOWN;
} }
}; };
const getPriorityColor = (priority: string) => { const getPriorityColor = (priority: string) => {
switch (priority) { switch (priority) {
case 'high': return '#b3261e'; case 'high':
case 'medium': return '#fd7c22'; return statusColors.STATUS_COLOR_CRITICAL;
case 'low': return '#00ff00'; case 'medium':
default: return '#666'; 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;

View File

@@ -0,0 +1,4 @@
export const STATUS_COLOR_CRITICAL = '#b3261e'
export const STATUS_COLOR_WARNING = '#fd7c22'
export const STATUS_COLOR_NORMAL = '#00ff00'
export const STATUS_COLOR_UNKNOWN = '#666666'