@cryptotaxi247 / CoPilot / commits / c45ee480

530 influxdb alert status filter (#547)

* Enhance InfluxDB alert retrieval with advanced filtering options and improved response model * Fix endpoint path for InfluxDB alerts retrieval to include '/alerts' * Remove commented-out code for InfluxDB alerts and related services * Refactor InfluxDB alert components and types for improved structure and filtering options * Refactor HealthcheckCard and healthcheck store to use InfluxDBAlertSeverity for filtering alerts * Add endpoint to retrieve unique check names from InfluxDB and refactor alert query parameters * Add check name filtering to healthcheck list and implement API call for check names * precommit-fixes * Fix: Styles and key prop in healthcheck list Fixes: Minor style issues in the healthcheck list component. The width of the select elements are fixed. Adds: A fallback to an empty string on the check_id for the key prop to prevent errors. * format * Fix: Conditionally renders severity tag Ensures the severity tag is only displayed if alert severity is present, preventing potential errors or unexpected UI behavior. Related to #530 * Add limit parameter to alert queries to control result size --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Dec 3, 2025 at 19:32 UTC c45ee4804c3ea212099217be507f845e7b9f4240
10 files changed +1250 -651
backend/app/connectors/influxdb/routes/alerts.py
+76 -14
@@ -1,29 +1,91 @@
1 +from typing import List
2 +from typing import Optional
3 +
4 from fastapi import APIRouter
5 +from fastapi import Depends
6 +from fastapi import Query
7 from fastapi import Security
3 -from loguru import logger
8 +from sqlalchemy.ext.asyncio import AsyncSession
9
10 from app.auth.utils import AuthHandler
6 -from app.connectors.influxdb.schema.alerts import InfluxDBAlertsResponse
7 -from app.connectors.influxdb.services.alerts import get_alerts
8 -
9 -# App specific imports
10 -
11 +from app.connectors.influxdb.schema.alerts import AlertStatus
12 +from app.connectors.influxdb.schema.alerts import GetInfluxDBAlertQueryParams
13 +from app.connectors.influxdb.schema.alerts import InfluxDBAlertResponse
14 +from app.connectors.influxdb.schema.alerts import InfluxDBCheckNamesResponse
15 +from app.connectors.influxdb.schema.alerts import SeverityFilter
16 +from app.connectors.influxdb.services.alerts import get_influxdb_alerts
17 +from app.connectors.influxdb.services.alerts import get_influxdb_check_names
18 +from app.db.db_session import get_db
19
20 influxdb_alerts_router = APIRouter()
21
22
23 @influxdb_alerts_router.get(
24 "/alerts",
17 - response_model=InfluxDBAlertsResponse,
18 - description="Get influxdb alerts",
25 + response_model=InfluxDBAlertResponse,
26 + description="Get alerts from InfluxDB with advanced filtering",
27 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
28 )
21 -async def get_all_alerts():
29 +async def get_alerts_route(
30 + days: int = Query(7, ge=1, le=90, description="Number of days to look back"),
31 + severity: Optional[List[SeverityFilter]] = Query(None, description="Filter by severity (can specify multiple)"),
32 + check_name: Optional[str] = Query(None, description="Filter by check name"),
33 + sensor_type: Optional[str] = Query(None, description="Filter by sensor type"),
34 + status: AlertStatus = Query(AlertStatus.ALL, description="Filter by status: active, cleared, or all"),
35 + latest_only: bool = Query(False, description="Return only latest alert per check"),
36 + exclude_ok: bool = Query(False, description="Exclude alerts with 'ok' status"),
37 + limit: Optional[int] = Query(500, ge=1, le=1000, description="Limit the number of returned alerts"),
38 + session: AsyncSession = Depends(get_db),
39 +) -> InfluxDBAlertResponse:
40 + """
41 + Get alerts from InfluxDB with advanced filtering options.
42 +
43 + The connector is always 'InfluxDB' and doesn't need to be specified.
44 +
45 + **Filtering Options:**
46 + - `severity`: Filter by severity levels (ok, warning, error, critical)
47 + - `check_name`: Filter by specific check name (e.g., "CPU CHECK", "Host Offline")
48 + - `sensor_type`: Filter by sensor type keyword
49 + - `status`: Show only active alerts, cleared alerts, or all
50 + - `latest_only`: Show only the latest alert per check
51 + - `exclude_ok`: Automatically exclude 'ok' status alerts for a cleaner view
52 +
53 + **Common Use Cases:**
54 + - See only current issues: `exclude_ok=true` or `status=active`
55 + - Latest status per check: `latest_only=true`
56 + - Active alerts only: `status=active` (shows alerts that haven't been cleared)
57 """
23 - Fetches all alerts from InfluxDB.
58 + query_params = GetInfluxDBAlertQueryParams(
59 + days=days,
60 + severity=severity,
61 + check_name=check_name,
62 + sensor_type=sensor_type,
63 + status=status,
64 + latest_only=latest_only,
65 + exclude_ok=exclude_ok,
66 + limit=limit,
67 + )
68 +
69 + return await get_influxdb_alerts(query_params, session)
70 +
71 +
72 +@influxdb_alerts_router.get(
73 + "/check-names",
74 + response_model=InfluxDBCheckNamesResponse,
75 + description="Get available check names from InfluxDB",
76 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
77 +)
78 +async def get_check_names_route(
79 + session: AsyncSession = Depends(get_db),
80 +) -> InfluxDBCheckNamesResponse:
81 + """
82 + Get a list of all available check names from InfluxDB.
83 +
84 + This endpoint retrieves unique check names from the last 30 days,
85 + useful for populating filter dropdowns or autocomplete fields.
86
25 - Returns:
26 - InfluxDBAlertsResponse: The response model containing the alerts.
87 + **Returns:**
88 + - List of unique check names (sorted alphabetically)
89 + - Total count of check names
90 """
28 - logger.info("Fetching all alerts from influxdb")
29 - return await get_alerts()
91 + return await get_influxdb_check_names(session)
backend/app/connectors/influxdb/schema/alerts.py
+106 -4
@@ -1,18 +1,120 @@
1 from datetime import datetime
2 +from enum import Enum
3 +from typing import Optional
4
5 from pydantic import BaseModel
6 +from pydantic import Field
7 +
8 +
9 +class SeverityFilter(str, Enum):
10 + """Severity levels for filtering"""
11 +
12 + OK = "ok"
13 + WARNING = "warning"
14 + ERROR = "error"
15 + CRITICAL = "critical"
16 +
17 +
18 +class AlertStatus(str, Enum):
19 + """Alert status"""
20 +
21 + ACTIVE = "active"
22 + CLEARED = "cleared"
23 + ALL = "all"
24 +
25 +
26 +class GetInfluxDBAlertQueryParams(BaseModel):
27 + """
28 + Query parameters for getting InfluxDB alerts with filtering
29 + """
30 +
31 + days: int = Field(default=7, ge=1, le=90, description="Number of days to look back")
32 + severity: Optional[list[SeverityFilter]] = Field(default=None, description="Filter by severity levels (can specify multiple)")
33 + check_name: Optional[str] = Field(default=None, description="Filter by specific check name")
34 + sensor_type: Optional[str] = Field(default=None, description="Filter by sensor type")
35 + status: AlertStatus = Field(default=AlertStatus.ALL, description="Filter by alert status (active/cleared/all)")
36 + latest_only: bool = Field(default=False, description="Return only the latest alert per check")
37 + exclude_ok: bool = Field(default=False, description="Exclude alerts with 'ok' status")
38 + limit: Optional[int] = Field(default=500, ge=1, le=1000, description="Limit the number of returned alerts")
39
40
41 class InfluxDBAlert(BaseModel):
42 + """
43 + Single InfluxDB alert
44 + """
45 +
46 time: datetime
47 + check_name: str
48 + sensor_type: str
49 + severity: str
50 + message: str
51 + status: Optional[str] = None # active or cleared
52 + check_id: Optional[str] = None # For internal filtering
53 +
54 + class Config:
55 + from_attributes = True
56 +
57 +
58 +class InfluxDBAlertResponse(BaseModel):
59 + """
60 + Response for InfluxDB alerts query
61 + """
62 +
63 + success: bool
64 message: str
9 - checkID: str
10 - checkName: str
11 - level: str
65 + alerts: list[InfluxDBAlert]
66 + total_count: int
67 + filtered_count: int
68 + active_alerts_count: int = 0
69 + cleared_alerts_count: int = 0
70 +
71 + class Config:
72 + from_attributes = True
73 + json_schema_extra = {
74 + "example": {
75 + "success": True,
76 + "message": "Successfully retrieved alerts",
77 + "alerts": [
78 + {
79 + "time": "2025-12-01T10:30:00Z",
80 + "check_name": "CPU CHECK",
81 + "sensor_type": "CPU",
82 + "severity": "warning",
83 + "message": "CPU usage high",
84 + "status": "active",
85 + },
86 + ],
87 + "total_count": 150,
88 + "filtered_count": 25,
89 + "active_alerts_count": 5,
90 + "cleared_alerts_count": 20,
91 + },
92 + }
93
94
14 -# If you need to parse a list of these alerts:
95 class InfluxDBAlertsResponse(BaseModel):
96 alerts: list[InfluxDBAlert]
97 success: bool
98 message: str
99 +
100 +
101 +class InfluxDBCheckNamesResponse(BaseModel):
102 + """
103 + Response for available check names query
104 + """
105 +
106 + success: bool
107 + message: str
108 + check_names: list[str]
109 + total_count: int
110 +
111 + class Config:
112 + from_attributes = True
113 + json_schema_extra = {
114 + "example": {
115 + "success": True,
116 + "message": "Successfully retrieved check names",
117 + "check_names": ["CPU CHECK", "Host Offline", "Memory Usage", "Disk Space"],
118 + "total_count": 4,
119 + },
120 + }
backend/app/connectors/influxdb/services/alerts.py
+289 -62
@@ -1,89 +1,316 @@
1 -from typing import List
2 -
3 -from fastapi import HTTPException
1 from loguru import logger
2 +from sqlalchemy.ext.asyncio import AsyncSession
3
4 +from app.connectors.influxdb.schema.alerts import AlertStatus
5 +from app.connectors.influxdb.schema.alerts import GetInfluxDBAlertQueryParams
6 from app.connectors.influxdb.schema.alerts import InfluxDBAlert
7 -from app.connectors.influxdb.schema.alerts import InfluxDBAlertsResponse
7 +from app.connectors.influxdb.schema.alerts import InfluxDBAlertResponse
8 +from app.connectors.influxdb.schema.alerts import InfluxDBCheckNamesResponse
9 from app.connectors.influxdb.utils.universal import create_influxdb_client
10 from app.connectors.influxdb.utils.universal import get_influxdb_organization
11 +from app.connectors.utils import get_connector_info_from_db
12
11 -# Constants
12 -BUCKET_NAME = "_monitoring"
13
14 +async def get_influxdb_alerts(
15 + query_params: GetInfluxDBAlertQueryParams,
16 + session: AsyncSession,
17 +) -> InfluxDBAlertResponse:
18 + """
19 + Retrieve alerts from InfluxDB with advanced filtering
20
15 -def construct_query() -> str:
16 - """Constructs the InfluxDB query.
21 + Args:
22 + query_params: Query parameters for filtering
23 + session: Database session
24
25 Returns:
19 - str: The constructed InfluxDB query.
26 + InfluxDBAlertResponse with filtered alerts
27 + """
28 + logger.info("Fetching InfluxDB alerts")
29 +
30 + # Get connector info to verify it exists
31 + connector_info = await get_connector_info_from_db("InfluxDB", session)
32 + if not connector_info:
33 + logger.error("InfluxDB connector not found")
34 + return InfluxDBAlertResponse(
35 + success=False,
36 + message="InfluxDB connector not found",
37 + alerts=[],
38 + total_count=0,
39 + filtered_count=0,
40 + )
41 +
42 + # Get org and bucket info
43 + try:
44 + influxdb_org = await get_influxdb_organization()
45 + # Use _monitoring bucket
46 + influxdb_bucket = "_monitoring"
47 + except Exception as e:
48 + logger.error(f"Error getting InfluxDB configuration: {e}")
49 + influxdb_org = "SOCFORTRESS"
50 + influxdb_bucket = "_monitoring"
51 +
52 + # Create InfluxDB client using existing function
53 + try:
54 + influxdb_client = await create_influxdb_client("InfluxDB")
55 + except Exception as e:
56 + logger.error(f"Failed to create InfluxDB client: {e}")
57 + return InfluxDBAlertResponse(
58 + success=False,
59 + message=f"Failed to connect to InfluxDB: {str(e)}",
60 + alerts=[],
61 + total_count=0,
62 + filtered_count=0,
63 + )
64 +
65 + query_api = influxdb_client.query_api()
66 +
67 + # Build time range - use relative time for better performance
68 + days_ago = f"-{query_params.days}d"
69 +
70 + try:
71 + # Build Flux query - matching the actual InfluxDB structure
72 + flux_query = f"""
73 + from(bucket: "{influxdb_bucket}")
74 + |> range(start: {days_ago})
75 + |> filter(fn: (r) => r._measurement == "statuses" and r._field == "_message")
76 + |> filter(fn: (r) => exists r._check_id and exists r._check_name and exists r._level)
77 + """
78 +
79 + # Add severity/level filter
80 + if query_params.exclude_ok or query_params.severity:
81 + severity_filters = []
82 +
83 + if query_params.severity:
84 + # Map severity to InfluxDB levels (crit, warn, info, ok)
85 + level_mapping = {"critical": "crit", "error": "crit", "warning": "warn", "ok": "ok"}
86 + for sev in query_params.severity:
87 + level = level_mapping.get(sev.value, sev.value)
88 + severity_filters.append(f'r._level == "{level}"')
89 + elif query_params.exclude_ok:
90 + # Exclude 'ok' level
91 + severity_filters = ['r._level == "warn"', 'r._level == "crit"', 'r._level == "info"']
92 +
93 + if severity_filters:
94 + severity_filter_str = " or ".join(severity_filters)
95 + flux_query += f"\n |> filter(fn: (r) => {severity_filter_str})"
96 +
97 + # Add check name filter
98 + if query_params.check_name:
99 + flux_query += f"""
100 + |> filter(fn: (r) => r._check_name =~ /{query_params.check_name}/)
101 + """
102 +
103 + # Add sensor type filter (if applicable)
104 + if query_params.sensor_type:
105 + flux_query += f"""
106 + |> filter(fn: (r) => r._check_name =~ /{query_params.sensor_type}/)
107 + """
108 +
109 + # Get only latest per check if requested
110 + if query_params.latest_only:
111 + flux_query += """
112 + |> group(columns: ["_check_name"])
113 + |> sort(columns: ["_time"], desc: true)
114 + |> limit(n: 1)
115 + |> group()
116 + """
117 + else:
118 + flux_query += """
119 + |> sort(columns: ["_time"], desc: true)
120 + """
121 +
122 + # Add limit to prevent overwhelming results
123 + flux_query += f"""
124 + |> limit(n: {query_params.limit})
125 + """
126 +
127 + logger.info(f"Executing Flux query:\n{flux_query}")
128 +
129 + # Execute query
130 + result = await query_api.query(flux_query, org=influxdb_org)
131 +
132 + # Process results
133 + alerts = []
134 + check_states = {} # Track latest state per check for status calculation
135 +
136 + for table in result:
137 + for record in table.records:
138 + alert_time = record.get_time()
139 + check_name = record.values.get("_check_name", "unknown")
140 + check_id = record.values.get("_check_id", "unknown")
141 + level = record.values.get("_level", "unknown")
142 + message = record.values.get("_value", "No message")
143 +
144 + # Map InfluxDB levels to severity
145 + severity_mapping = {"crit": "critical", "warn": "warning", "info": "info", "ok": "ok"}
146 + severity = severity_mapping.get(level, level)
147 +
148 + # Track the latest state for this check ID
149 + if check_id not in check_states or alert_time > check_states[check_id]["time"]:
150 + check_states[check_id] = {
151 + "time": alert_time,
152 + "level": level,
153 + "check_name": check_name,
154 + }
155 +
156 + # Status: if level is 'ok', it's cleared; otherwise active
157 + status = "cleared" if level == "ok" else "active"
158 +
159 + alerts.append(
160 + InfluxDBAlert(
161 + time=alert_time,
162 + check_name=check_name,
163 + sensor_type=check_name.split()[0] if " " in check_name else check_name,
164 + severity=severity,
165 + message=message,
166 + status=status,
167 + check_id=str(check_id),
168 + ),
169 + )
170 +
171 + # Apply status filter if requested
172 + if query_params.status != AlertStatus.ALL:
173 + if query_params.status == AlertStatus.ACTIVE:
174 + # Only keep alerts from checks that are currently NOT in 'ok' state
175 + active_check_ids = {check_id for check_id, state in check_states.items() if state["level"] != "ok"}
176 +
177 + logger.info(f"Active check IDs: {active_check_ids}")
178 +
179 + alerts = [alert for alert in alerts if alert.check_id in active_check_ids]
180 + elif query_params.status == AlertStatus.CLEARED:
181 + # Only keep alerts from checks that are currently in 'ok' state
182 + cleared_check_ids = {check_id for check_id, state in check_states.items() if state["level"] == "ok"}
183 +
184 + alerts = [alert for alert in alerts if alert.check_id in cleared_check_ids]
185 +
186 + # Calculate counts
187 + total_count = len(alerts)
188 + active_count = sum(1 for a in alerts if a.status == "active")
189 + cleared_count = sum(1 for a in alerts if a.status == "cleared")
190 +
191 + logger.info(f"Retrieved {len(alerts)} alerts from InfluxDB (active: {active_count}, cleared: {cleared_count})")
192 +
193 + return InfluxDBAlertResponse(
194 + success=True,
195 + message="Successfully retrieved InfluxDB alerts",
196 + alerts=alerts,
197 + total_count=total_count,
198 + filtered_count=len(alerts),
199 + active_alerts_count=active_count,
200 + cleared_alerts_count=cleared_count,
201 + )
202 +
203 + except Exception as e:
204 + logger.error(f"Error querying InfluxDB alerts: {e}")
205 + import traceback
206 +
207 + logger.error(f"Traceback: {traceback.format_exc()}")
208 + return InfluxDBAlertResponse(
209 + success=False,
210 + message=f"Error querying InfluxDB: {str(e)}",
211 + alerts=[],
212 + total_count=0,
213 + filtered_count=0,
214 + )
215 + finally:
216 + await influxdb_client.close()
217 +
218 +
219 +async def get_influxdb_check_names(
220 + session: AsyncSession,
221 +) -> InfluxDBCheckNamesResponse:
222 """
21 - return """
22 - from(bucket: "{bucket_name}")
23 - |> range(start: -1h, stop: now())
24 - |> filter(fn: (r) => r._measurement == "statuses" and r._field == "_message")
25 - |> filter(fn: (r) => exists r._check_id and exists r._value and exists r._check_name and exists r._level)
26 - |> keep(columns: ["_time", "_value", "_check_id", "_check_name", "_level"])
27 - |> rename(columns: {{ "_time": "time", "_value": "message", "_check_id": "checkID", "_check_name": "checkName", "_level": "level" }})
28 - |> group()
29 - |> sort(columns: ["time"], desc: true)
30 - |> limit(n: 100, offset: 29)
31 - """.format(
32 - bucket_name=BUCKET_NAME,
33 - )
34 -
35 -
36 -async def process_alert_records(result) -> List[InfluxDBAlert]:
37 - """Processes alert records from InfluxDB query result.
223 + Retrieve unique check names from InfluxDB
224
225 Args:
40 - result: The query result from InfluxDB.
226 + session: Database session
227
228 Returns:
43 - A list of InfluxDBAlert objects representing the processed alert records.
229 + InfluxDBCheckNamesResponse with list of available check names
230 """
45 - alerts = []
46 - for table in result:
47 - for record in table.records:
48 - alert = InfluxDBAlert(
49 - time=record.values.get("time").isoformat() if record.values.get("time") else None,
50 - message=record.values.get("message"),
51 - checkID=record.values.get("checkID"),
52 - checkName=record.values.get("checkName"),
53 - level=record.values.get("level"),
54 - )
55 - alerts.append(alert)
56 - return alerts
57 -
58 -
59 -async def get_alerts() -> InfluxDBAlertsResponse:
60 - """Fetches alerts from InfluxDB and returns them.
231 + logger.info("Fetching InfluxDB check names")
232
62 - Returns:
63 - InfluxDBAlertsResponse: The response object containing the fetched alerts.
233 + # Get connector info to verify it exists
234 + connector_info = await get_connector_info_from_db("InfluxDB", session)
235 + if not connector_info:
236 + logger.error("InfluxDB connector not found")
237 + return InfluxDBCheckNamesResponse(
238 + success=False,
239 + message="InfluxDB connector not found",
240 + check_names=[],
241 + total_count=0,
242 + )
243
65 - Raises:
66 - HTTPException: If there is an error fetching the alerts.
67 - """
68 - client = await create_influxdb_client("InfluxDB")
244 + # Get org and bucket info
245 + try:
246 + influxdb_org = await get_influxdb_organization()
247 + influxdb_bucket = "_monitoring"
248 + except Exception as e:
249 + logger.error(f"Error getting InfluxDB configuration: {e}")
250 + influxdb_org = "SOCFORTRESS"
251 + influxdb_bucket = "_monitoring"
252 +
253 + # Create InfluxDB client
254 try:
70 - query = construct_query()
71 - query_api = client.query_api()
72 - result = await query_api.query(
73 - org=await get_influxdb_organization(),
74 - query=query,
255 + influxdb_client = await create_influxdb_client("InfluxDB")
256 + except Exception as e:
257 + logger.error(f"Failed to create InfluxDB client: {e}")
258 + return InfluxDBCheckNamesResponse(
259 + success=False,
260 + message=f"Failed to connect to InfluxDB: {str(e)}",
261 + check_names=[],
262 + total_count=0,
263 )
264
77 - alerts = await process_alert_records(result)
265 + query_api = influxdb_client.query_api()
266
79 - return InfluxDBAlertsResponse(
80 - alerts=alerts,
267 + try:
268 + # Query to get unique check names from the last 30 days
269 + flux_query = f"""
270 + from(bucket: "{influxdb_bucket}")
271 + |> range(start: -30d)
272 + |> filter(fn: (r) => r._measurement == "statuses" and r._field == "_message")
273 + |> filter(fn: (r) => exists r._check_name)
274 + |> group(columns: ["_check_name"])
275 + |> distinct(column: "_check_name")
276 + |> keep(columns: ["_check_name"])
277 + """
278 +
279 + logger.info(f"Executing Flux query:\n{flux_query}")
280 +
281 + # Execute query
282 + result = await query_api.query(flux_query, org=influxdb_org)
283 +
284 + # Process results - collect unique check names
285 + check_names = set()
286 + for table in result:
287 + for record in table.records:
288 + check_name = record.values.get("_check_name")
289 + if check_name:
290 + check_names.add(check_name)
291 +
292 + # Convert to sorted list
293 + check_names_list = sorted(list(check_names))
294 +
295 + logger.info(f"Retrieved {len(check_names_list)} unique check names")
296 +
297 + return InfluxDBCheckNamesResponse(
298 success=True,
82 - message="Successfully fetched alerts",
299 + message="Successfully retrieved check names",
300 + check_names=check_names_list,
301 + total_count=len(check_names_list),
302 )
303
304 except Exception as e:
86 - logger.error(f"Error fetching alerts: {e}")
87 - raise HTTPException(status_code=500, detail=f"Error fetching healthcheck alerts from InfluxDB: {e}")
305 + logger.error(f"Error querying InfluxDB check names: {e}")
306 + import traceback
307 +
308 + logger.error(f"Traceback: {traceback.format_exc()}")
309 + return InfluxDBCheckNamesResponse(
310 + success=False,
311 + message=f"Error querying InfluxDB: {str(e)}",
312 + check_names=[],
313 + total_count=0,
314 + )
315 finally:
89 - await client.close()
316 + await influxdb_client.close()
frontend/src/api/endpoints/healthchecks.ts
+11 -3
@@ -1,10 +1,18 @@
1 import type { FlaskBaseResponse } from "@/types/flask.d"
2 -import type { InfluxDBAlert } from "@/types/healthchecks.d"
2 +import type {
3 + InfluxDBAlertQueryParams,
4 + InfluxDBAlertResponse,
5 + InfluxDBCheckNamesResponse
6 +} from "@/types/healthchecks.d"
7 import { HttpClient } from "../httpClient"
8
9 export default {
6 - getHealthchecks() {
7 - return HttpClient.get<FlaskBaseResponse & { alerts: InfluxDBAlert[] }>(`/influxdb/alerts`)
10 + getHealthchecks(params?: InfluxDBAlertQueryParams) {
11 + return HttpClient.get<FlaskBaseResponse & InfluxDBAlertResponse>(`/influxdb/alerts`, { params })
12 + },
13 +
14 + getCheckNames() {
15 + return HttpClient.get<FlaskBaseResponse & InfluxDBCheckNamesResponse>(`/influxdb/check-names`)
16 }
17
18 // index health : Api.wazuh.indices.getClusterHealth()
frontend/src/components/healthcheck/HealthcheckItem.vue
+81 -16
@@ -1,20 +1,39 @@
1 <template>
2 <div>
3 - <CardEntity :status="alert.level === InfluxDBAlertLevel.Crit ? 'warning' : undefined">
4 - <template #headerMain>#{{ alert.checkID }}</template>
5 - <template #headerExtra>{{ formatDate(alert.time) }}</template>
3 + <CardEntity :status="statusType">
4 + <template #headerMain>
5 + <div class="flex items-center gap-2">
6 + <span>{{ alert.check_name }}</span>
7 + <n-tag
8 + v-if="alert.status === InfluxDBAlertStatus.Active"
9 + type="error"
10 + size="small"
11 + :bordered="false"
12 + >
13 + Active
14 + </n-tag>
15 + <n-tag v-else type="success" size="small" :bordered="false">Cleared</n-tag>
16 + </div>
17 + </template>
18 + <template #headerExtra>
19 + <div class="flex items-center gap-2">
20 + <n-tag v-if="alert.severity" :type="severityTagType" size="small" :bordered="false">
21 + {{ alert.severity.toUpperCase() }}
22 + </n-tag>
23 + <span>{{ formatDate(alert.time) }}</span>
24 + </div>
25 + </template>
26 <template #default>
27 <div class="flex items-center gap-3">
28 <div class="mt-1">
9 - <Icon
10 - v-if="alert.level === InfluxDBAlertLevel.Crit"
11 - :name="WarningIcon"
12 - :size="20"
13 - class="text-warning"
14 - />
15 - <Icon v-else :name="OKIcon" :size="20" class="text-success" />
29 + <Icon :name="severityIcon" :size="20" :class="severityIconClass" />
30 + </div>
31 + <div class="grow">
32 + <div class="font-mono text-sm" v-html="formattedMessage"></div>
33 + <div v-if="alert.sensor_type" class="mt-1 text-xs opacity-50">
34 + Sensor: {{ alert.sensor_type }}
35 + </div>
36 </div>
17 - <div class="grow" v-html="message"></div>
37 </div>
38 </template>
39 </CardEntity>
@@ -23,20 +42,66 @@
42
43 <script setup lang="ts">
44 import type { InfluxDBAlert } from "@/types/healthchecks.d"
45 +import { NTag } from "naive-ui"
46 import { computed } from "vue"
47 import CardEntity from "@/components/common/cards/CardEntity.vue"
48 import Icon from "@/components/common/Icon.vue"
49 import { useSettingsStore } from "@/stores/settings"
30 -import { InfluxDBAlertLevel } from "@/types/healthchecks.d"
50 +import { InfluxDBAlertSeverity, InfluxDBAlertStatus } from "@/types/healthchecks.d"
51 import dayjs from "@/utils/dayjs"
52
53 const { alert } = defineProps<{ alert: InfluxDBAlert }>()
54
35 -const WarningIcon = "carbon:warning-alt-filled"
36 -const OKIcon = "carbon:checkmark-filled"
55 +const formattedMessage = computed(() => {
56 + return alert.message.replace(/\r?\n/g, " <span class='mx-1'>•</span> ")
57 +})
58 +
59 +const statusType = computed(() => {
60 + if (alert.severity === InfluxDBAlertSeverity.Critical) {
61 + return "error"
62 + } else if (alert.severity === InfluxDBAlertSeverity.Warning) {
63 + return "warning"
64 + }
65 + return undefined
66 +})
67 +
68 +const severityTagType = computed(() => {
69 + switch (alert.severity) {
70 + case InfluxDBAlertSeverity.Critical:
71 + return "error"
72 + case InfluxDBAlertSeverity.Warning:
73 + return "warning"
74 + case InfluxDBAlertSeverity.Info:
75 + return "info"
76 + default:
77 + return "success"
78 + }
79 +})
80 +
81 +const severityIcon = computed(() => {
82 + switch (alert.severity) {
83 + case InfluxDBAlertSeverity.Critical:
84 + return "carbon:warning-alt-filled"
85 + case InfluxDBAlertSeverity.Warning:
86 + return "carbon:warning"
87 + case InfluxDBAlertSeverity.Info:
88 + return "carbon:information-filled"
89 + default:
90 + return "carbon:checkmark-filled"
91 + }
92 +})
93
38 -const message = computed(() => {
39 - return alert.message.replace(/\n/g, " <span class='mx-1'>•</span> ")
94 +const severityIconClass = computed(() => {
95 + switch (alert.severity) {
96 + case InfluxDBAlertSeverity.Critical:
97 + return "text-error-500"
98 + case InfluxDBAlertSeverity.Warning:
99 + return "text-warning-500"
100 + case InfluxDBAlertSeverity.Info:
101 + return "text-info-500"
102 + default:
103 + return "text-success-500"
104 + }
105 })
106
107 const dFormats = useSettingsStore().dateFormat
frontend/src/components/healthcheck/HealthcheckList.vue
+93 -10
@@ -5,7 +5,7 @@
5 <n-popover overlap placement="bottom-start">
6 <template #trigger>
7 <div class="bg-default rounded-lg">
8 - <n-button size="small" class="!cursor-help">
8 + <n-button size="small" class="cursor-help!">
9 <template #icon>
10 <Icon :name="InfoIcon" />
11 </template>
@@ -15,15 +15,43 @@
15 <div class="flex flex-col gap-2">
16 <div class="box">
17 Total :
18 - <code>{{ total }}</code>
18 + <code>{{ stats?.total_count || 0 }}</code>
19 </div>
20 - <div class="box text-warning">
20 + <div class="box text-error-500">
21 + Active :
22 + <code>{{ stats?.active_alerts_count || 0 }}</code>
23 + </div>
24 + <div class="box text-warning-500">
25 Critical :
26 <code>{{ criticalTotal }}</code>
27 </div>
28 + <div class="box text-success-500">
29 + Cleared :
30 + <code>{{ stats?.cleared_alerts_count || 0 }}</code>
31 + </div>
32 </div>
33 </n-popover>
34 </div>
35 + <div class="flex items-center gap-2">
36 + <n-select
37 + v-model:value="checkNameFilter"
38 + :options="checkNameOptions"
39 + size="small"
40 + class="w-48!"
41 + placeholder="Filter by check name"
42 + clearable
43 + filterable
44 + @update:value="getData"
45 + />
46 + <n-select
47 + v-model:value="statusFilter"
48 + :options="statusOptions"
49 + size="small"
50 + class="w-32!"
51 + @update:value="getData"
52 + />
53 + <n-checkbox v-model:checked="excludeOk" size="small" @update:checked="getData">Exclude OK</n-checkbox>
54 + </div>
55 <n-pagination
56 v-model:page="currentPage"
57 v-model:page-size="pageSize"
@@ -39,7 +67,7 @@
67 <template v-if="healthcheckList.length">
68 <HealthcheckItem
69 v-for="alert of itemsPaginated"
42 - :key="alert.checkID + alert.time"
70 + :key="(alert.check_id || '') + alert.time"
71 :alert="alert"
72 class="item-appear item-appear-bottom item-appear-005"
73 />
@@ -62,19 +90,21 @@
90 </template>
91
92 <script setup lang="ts">
65 -import type { InfluxDBAlert } from "@/types/healthchecks.d"
93 +import type { InfluxDBAlert, InfluxDBAlertResponse } from "@/types/healthchecks.d"
94 import { useResizeObserver } from "@vueuse/core"
95 import _orderBy from "lodash/orderBy"
68 -import { NButton, NEmpty, NPagination, NPopover, NSpin, useMessage } from "naive-ui"
96 +import { NButton, NCheckbox, NEmpty, NPagination, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
97 import { computed, onBeforeMount, ref } from "vue"
98 import Api from "@/api"
99 import Icon from "@/components/common/Icon.vue"
72 -import { InfluxDBAlertLevel } from "@/types/healthchecks.d"
100 +import { InfluxDBAlertSeverity } from "@/types/healthchecks.d"
101 import HealthcheckItem from "./HealthcheckItem.vue"
102
103 const message = useMessage()
104 const loading = ref(false)
105 const healthcheckList = ref<InfluxDBAlert[]>([])
106 +const stats = ref<InfluxDBAlertResponse | null>(null)
107 +const checkNames = ref<string[]>([])
108
109 const pageSize = ref(25)
110 const currentPage = ref(1)
@@ -84,11 +114,43 @@ const pageSizes = [10, 25, 50, 100]
114 const header = ref()
115 const pageSlot = ref(8)
116
117 +// Filters
118 +const statusFilter = ref<"all" | "active" | "cleared">("all")
119 +const excludeOk = ref(false)
120 +const checkNameFilter = ref<string | null>(null)
121 +
122 +const statusOptions = [
123 + { label: "All", value: "all" },
124 + { label: "Active", value: "active" },
125 + { label: "Cleared", value: "cleared" }
126 +]
127 +
128 +const checkNameOptions = computed(() => [
129 + { label: "All Checks" },
130 + ...checkNames.value.map(name => ({ label: name, value: name }))
131 +])
132 +
133 const itemsPaginated = computed(() => {
134 const from = (currentPage.value - 1) * pageSize.value
135 const to = currentPage.value * pageSize.value
136
91 - const list = _orderBy(healthcheckList.value, ["level", "time"], ["asc", "desc"])
137 + const list = _orderBy(
138 + healthcheckList.value,
139 + [
140 + // Sort by severity priority
141 + item => {
142 + const severityOrder = {
143 + [InfluxDBAlertSeverity.Critical]: 0,
144 + [InfluxDBAlertSeverity.Warning]: 1,
145 + [InfluxDBAlertSeverity.Info]: 2,
146 + [InfluxDBAlertSeverity.Ok]: 3
147 + }
148 + return severityOrder[item.severity as InfluxDBAlertSeverity]
149 + },
150 + "time"
151 + ],
152 + ["asc", "desc"]
153 + )
154
155 return list.slice(from, to)
156 })
@@ -100,23 +162,43 @@ const total = computed<number>(() => {
162 })
163
164 const criticalTotal = computed<number>(() => {
103 - return healthcheckList.value.filter(o => o.level === InfluxDBAlertLevel.Crit).length || 0
165 + return healthcheckList.value.filter(o => o.severity === InfluxDBAlertSeverity.Critical).length || 0
166 })
167
168 +function getCheckNames() {
169 + Api.healthchecks
170 + .getCheckNames()
171 + .then(res => {
172 + if (res.data.success) {
173 + checkNames.value = res.data.check_names || []
174 + }
175 + })
176 + .catch(() => {
177 + checkNames.value = []
178 + })
179 +}
180 +
181 function getData() {
182 loading.value = true
183
184 Api.healthchecks
110 - .getHealthchecks()
185 + .getHealthchecks({
186 + days: 7,
187 + status: statusFilter.value,
188 + exclude_ok: excludeOk.value,
189 + check_name: checkNameFilter.value || undefined
190 + })
191 .then(res => {
192 if (res.data.success) {
193 healthcheckList.value = res.data.alerts || []
194 + stats.value = res.data
195 } else {
196 message.warning(res.data?.message || "An error occurred. Please try again later.")
197 }
198 })
199 .catch(err => {
200 healthcheckList.value = []
201 + stats.value = null
202
203 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
204 })
@@ -134,6 +216,7 @@ useResizeObserver(header, entries => {
216 })
217
218 onBeforeMount(() => {
219 + getCheckNames()
220 getData()
221 })
222 </script>
frontend/src/components/incidentManagement/sources/SourceConfigurationForm.vue
+535 -518
@@ -1,247 +1,260 @@
1 <template>
2 - <n-spin :show="loading" class="creation-report-form">
3 - <n-form ref="formRef" :model="form" :rules="rules">
4 - <div class="flex flex-col gap-8">
5 - <div class="flex flex-col gap-2">
6 - <n-form-item v-if="showIndexNameField" label="Index name" path="index_name">
7 - <n-select
8 - v-model:value="form.index_name"
9 - :options="indexNamesOptions"
10 - placeholder="Select..."
11 - clearable
12 - filterable
13 - to="body"
14 - :disabled="disableIndexNameField"
15 - :loading="loadingIndexNames"
16 - />
17 - </n-form-item>
18 -
19 - <div v-if="showSourceField">
20 - <n-form-item path="source" :show-require-mark="false" class="source-field">
21 - <template #label>
22 - <div class="flex items-end justify-between gap-2">
23 - <span>
24 - Source
25 - <span class="n-form-item-label__asterisk">*</span>
26 - </span>
27 -
28 - <span v-if="isSocfortressRecommendsAvailable">
29 - <n-button
30 - :loading="loadingSocfortressRecommendsWazuh"
31 - size="tiny"
32 - ghost
33 - type="primary"
34 - @click="getSocfortressRecommendsWazuh()"
35 - >
36 - SOCFortress Recommends
37 - </n-button>
38 - </span>
39 - </div>
40 - </template>
41 - <n-input
42 - v-if="arbitrarySourceField"
43 - v-model:value.trim="form.source"
44 - placeholder="Please insert Source"
45 - clearable
46 - />
47 - <n-input
48 - v-else
49 - v-model:value.trim="form.source"
50 - placeholder="Please insert Source"
51 - clearable
52 - :disabled="disableSourceField"
53 - :loading="loadingSource"
54 - @update:value="resetIndexAvailable()"
55 - />
56 - </n-form-item>
57 - </div>
58 -
59 - <n-alert v-if="isSourceNotAllowed" title="Source already exists" type="warning" class="mb-5">
60 - A configuration for
61 - <strong>"{{ form.source }}"</strong>
62 - already exists. Please select a different
63 - <strong>Index name</strong>
64 - to proceed.
65 - </n-alert>
66 -
67 - <n-alert
68 - v-if="arbitrarySourceField"
69 - title="Proceed with caution, incorrect settings can disrupt system operation"
70 - type="warning"
71 - class="mb-5"
72 - ></n-alert>
73 -
74 - <n-form-item label="Field names" path="field_names">
75 - <n-select
76 - v-model:value="form.field_names"
77 - :options="availableMappingsOptions"
78 - placeholder="Select..."
79 - clearable
80 - filterable
81 - multiple
82 - :tag="arbitrarySourceField"
83 - to="body"
84 - :disabled="!isFieldEnabled"
85 - :loading="loadingAvailableMappings"
86 - >
87 - <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
88 - </n-select>
89 - </n-form-item>
90 - <n-form-item label="IOC Field names" path="ioc_field_names">
91 - <n-select
92 - v-model:value="form.ioc_field_names"
93 - :options="availableMappingsOptions"
94 - placeholder="Select..."
95 - clearable
96 - filterable
97 - multiple
98 - :tag="arbitrarySourceField"
99 - to="body"
100 - :disabled="!isFieldEnabled"
101 - :loading="loadingAvailableMappings"
102 - >
103 - <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
104 - </n-select>
105 - </n-form-item>
106 - <n-form-item path="asset_name_array" :show-require-mark="false">
107 - <template #label>
108 - <div class="flex flex-col gap-1">
109 - <span>
110 - Asset name fields
111 - <span class="n-form-item-label__asterisk">*</span>
112 - </span>
113 - <n-text depth="3" style="font-size: 11px; font-weight: 400">
114 - Add multiple fields. First field has priority (checked first).
115 - </n-text>
116 - </div>
117 - </template>
118 - <n-dynamic-tags
119 - v-model:value="form.asset_name_array"
120 - :max="10"
121 - :disabled="!isFieldEnabled"
122 - :render-tag="renderAssetTag"
123 - >
124 - <template #input="{ submit, deactivate }">
125 - <n-auto-complete
126 - v-model:value="assetNameInput"
127 - :options="filteredAssetNameOptions"
128 - :disabled="!isFieldEnabled"
129 - placeholder="Type or select field name"
130 - @select="handleAssetSelect($event, submit)"
131 - @blur="deactivate"
132 - @keyup.enter="handleAssetEnter(submit)"
133 - />
134 - </template>
135 - <template #trigger="{ activate, disabled }">
136 - <n-button
137 - size="small"
138 - type="primary"
139 - dashed
140 - :disabled="disabled || !isFieldEnabled"
141 - @click="activate()"
142 - >
143 - <template #icon>
144 - <Icon :name="AddIcon" />
145 - </template>
146 - Add Field
147 - </n-button>
148 - </template>
149 - </n-dynamic-tags>
150 - </n-form-item>
151 - <n-form-item label="Timefield name" path="timefield_name">
152 - <n-select
153 - v-model:value="form.timefield_name"
154 - :options="availableMappingsOptions"
155 - placeholder="Select..."
156 - clearable
157 - filterable
158 - :tag="arbitrarySourceField"
159 - to="body"
160 - :disabled="!isFieldEnabled"
161 - :loading="loadingAvailableMappings"
162 - >
163 - <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
164 - </n-select>
165 - </n-form-item>
166 - <n-form-item label="Alert title name" path="alert_title_name">
167 - <n-select
168 - v-model:value="form.alert_title_name"
169 - :options="availableMappingsOptions"
170 - placeholder="Select..."
171 - clearable
172 - filterable
173 - :tag="arbitrarySourceField"
174 - to="body"
175 - :disabled="!isFieldEnabled"
176 - :loading="loadingAvailableMappings"
177 - >
178 - <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
179 - </n-select>
180 - </n-form-item>
181 - </div>
182 - <div class="flex justify-between gap-3">
183 - <div>
184 - <slot name="additionalActions"></slot>
185 - </div>
186 - <div class="flex items-center gap-3">
187 - <n-button :disabled="loading" @click="reset()">Reset</n-button>
188 - <n-button
189 - type="primary"
190 - :disabled="!isValid"
191 - :loading="submitting"
192 - @click="validate(() => submit())"
193 - >
194 - Submit
195 - </n-button>
196 - </div>
197 - </div>
198 - </div>
199 - </n-form>
200 - </n-spin>
2 + <n-spin :show="loading" class="creation-report-form">
3 + <n-form ref="formRef" :model="form" :rules="rules">
4 + <div class="flex flex-col gap-8">
5 + <div class="flex flex-col gap-2">
6 + <n-form-item v-if="showIndexNameField" label="Index name" path="index_name">
7 + <n-select
8 + v-model:value="form.index_name"
9 + :options="indexNamesOptions"
10 + placeholder="Select..."
11 + clearable
12 + filterable
13 + to="body"
14 + :disabled="disableIndexNameField"
15 + :loading="loadingIndexNames"
16 + />
17 + </n-form-item>
18 +
19 + <div v-if="showSourceField">
20 + <n-form-item path="source" :show-require-mark="false" class="source-field">
21 + <template #label>
22 + <div class="flex items-end justify-between gap-2">
23 + <span>
24 + Source
25 + <span class="n-form-item-label__asterisk">*</span>
26 + </span>
27 +
28 + <span v-if="isSocfortressRecommendsAvailable">
29 + <n-button
30 + :loading="loadingSocfortressRecommendsWazuh"
31 + size="tiny"
32 + ghost
33 + type="primary"
34 + @click="getSocfortressRecommendsWazuh()"
35 + >
36 + SOCFortress Recommends
37 + </n-button>
38 + </span>
39 + </div>
40 + </template>
41 + <n-input
42 + v-if="arbitrarySourceField"
43 + v-model:value.trim="form.source"
44 + placeholder="Please insert Source"
45 + clearable
46 + />
47 + <n-input
48 + v-else
49 + v-model:value.trim="form.source"
50 + placeholder="Please insert Source"
51 + clearable
52 + :disabled="disableSourceField"
53 + :loading="loadingSource"
54 + @update:value="resetIndexAvailable()"
55 + />
56 + </n-form-item>
57 + </div>
58 +
59 + <n-alert v-if="isSourceNotAllowed" title="Source already exists" type="warning" class="mb-5">
60 + A configuration for
61 + <strong>"{{ form.source }}"</strong>
62 + already exists. Please select a different
63 + <strong>Index name</strong>
64 + to proceed.
65 + </n-alert>
66 +
67 + <n-alert
68 + v-if="arbitrarySourceField"
69 + title="Proceed with caution, incorrect settings can disrupt system operation"
70 + type="warning"
71 + class="mb-5"
72 + ></n-alert>
73 +
74 + <n-form-item label="Field names" path="field_names">
75 + <n-select
76 + v-model:value="form.field_names"
77 + :options="availableMappingsOptions"
78 + placeholder="Select..."
79 + clearable
80 + filterable
81 + multiple
82 + :tag="arbitrarySourceField"
83 + to="body"
84 + :disabled="!isFieldEnabled"
85 + :loading="loadingAvailableMappings"
86 + >
87 + <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
88 + </n-select>
89 + </n-form-item>
90 + <n-form-item label="IOC Field names" path="ioc_field_names">
91 + <n-select
92 + v-model:value="form.ioc_field_names"
93 + :options="availableMappingsOptions"
94 + placeholder="Select..."
95 + clearable
96 + filterable
97 + multiple
98 + :tag="arbitrarySourceField"
99 + to="body"
100 + :disabled="!isFieldEnabled"
101 + :loading="loadingAvailableMappings"
102 + >
103 + <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
104 + </n-select>
105 + </n-form-item>
106 + <n-form-item path="asset_name_array" :show-require-mark="false">
107 + <template #label>
108 + <div class="flex flex-col gap-1">
109 + <span>
110 + Asset name fields
111 + <span class="n-form-item-label__asterisk">*</span>
112 + </span>
113 + <small class="text-secondary">
114 + Add multiple fields. First field has priority (checked first).
115 + </small>
116 + </div>
117 + </template>
118 + <n-dynamic-tags
119 + v-model:value="form.asset_name_array"
120 + :max="10"
121 + :disabled="!isFieldEnabled"
122 + :render-tag="renderAssetTag"
123 + >
124 + <template #input="{ submit, deactivate }">
125 + <n-auto-complete
126 + v-model:value="assetNameInput"
127 + :options="filteredAssetNameOptions"
128 + :disabled="!isFieldEnabled"
129 + placeholder="Type or select field name"
130 + @select="handleAssetSelect($event, submit)"
131 + @blur="deactivate"
132 + @keyup.enter="handleAssetEnter(submit)"
133 + />
134 + </template>
135 + <template #trigger="{ activate, disabled }">
136 + <n-button
137 + size="small"
138 + type="primary"
139 + dashed
140 + :disabled="disabled || !isFieldEnabled"
141 + @click="activate()"
142 + >
143 + <template #icon>
144 + <Icon :name="AddIcon" />
145 + </template>
146 + Add Field
147 + </n-button>
148 + </template>
149 + </n-dynamic-tags>
150 + </n-form-item>
151 + <n-form-item label="Timefield name" path="timefield_name">
152 + <n-select
153 + v-model:value="form.timefield_name"
154 + :options="availableMappingsOptions"
155 + placeholder="Select..."
156 + clearable
157 + filterable
158 + :tag="arbitrarySourceField"
159 + to="body"
160 + :disabled="!isFieldEnabled"
161 + :loading="loadingAvailableMappings"
162 + >
163 + <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
164 + </n-select>
165 + </n-form-item>
166 + <n-form-item label="Alert title name" path="alert_title_name">
167 + <n-select
168 + v-model:value="form.alert_title_name"
169 + :options="availableMappingsOptions"
170 + placeholder="Select..."
171 + clearable
172 + filterable
173 + :tag="arbitrarySourceField"
174 + to="body"
175 + :disabled="!isFieldEnabled"
176 + :loading="loadingAvailableMappings"
177 + >
178 + <template v-if="arbitrarySourceField" #empty>Press Enter to add the typed value</template>
179 + </n-select>
180 + </n-form-item>
181 + </div>
182 + <div class="flex justify-between gap-3">
183 + <div>
184 + <slot name="additionalActions"></slot>
185 + </div>
186 + <div class="flex items-center gap-3">
187 + <n-button :disabled="loading" @click="reset()">Reset</n-button>
188 + <n-button
189 + type="primary"
190 + :disabled="!isValid"
191 + :loading="submitting"
192 + @click="validate(() => submitForm())"
193 + >
194 + Submit
195 + </n-button>
196 + </div>
197 + </div>
198 + </div>
199 + </n-form>
200 + </n-spin>
201 </template>
202
203 <script setup lang="ts">
204 import type { FormInst, FormItemRule, FormRules, FormValidationError, MessageReactive } from "naive-ui"
205 import type { SourceConfiguration, SourceConfigurationModel, SourceName } from "@/types/incidentManagement/sources.d"
206 import _intersection from "lodash/intersection"
207 -import { NAlert, NButton, NForm, NFormItem, NInput, NSelect, NSpin, NDynamicTags, NAutoComplete, NTag, NText, useMessage } from "naive-ui"
207 +import {
208 + NAlert,
209 + NAutoComplete,
210 + NButton,
211 + NDynamicTags,
212 + NForm,
213 + NFormItem,
214 + NInput,
215 + NSelect,
216 + NSpin,
217 + NTag,
218 + NText,
219 + useMessage
220 +} from "naive-ui"
221 import { computed, h, onBeforeMount, onMounted, ref, toRefs, watch } from "vue"
222 import Api from "@/api"
223 import Icon from "@/components/common/Icon.vue"
224
212 -const AddIcon = "carbon:add"
213 -
225 const props = defineProps<{
215 - sourceConfigurationModel?: SourceConfigurationModel
216 - showSourceField?: boolean
217 - arbitrarySourceField?: boolean
218 - disableSourceField?: boolean
219 - showIndexNameField?: boolean
220 - disableIndexNameField?: boolean
221 - applyFieldsSanitize?: boolean
222 - disabledSources?: SourceName[]
226 + sourceConfigurationModel?: SourceConfigurationModel
227 + showSourceField?: boolean
228 + arbitrarySourceField?: boolean
229 + disableSourceField?: boolean
230 + showIndexNameField?: boolean
231 + disableIndexNameField?: boolean
232 + applyFieldsSanitize?: boolean
233 + disabledSources?: SourceName[]
234 }>()
235
236 const emit = defineEmits<{
226 - (e: "submitted", value: SourceConfiguration): void
227 - (
228 - e: "mounted",
229 - value: {
230 - reset: () => void
231 - toggleSubmittingFlag: () => boolean
232 - }
233 - ): void
237 + (e: "submitted", value: SourceConfiguration): void
238 + (
239 + e: "mounted",
240 + value: {
241 + reset: () => void
242 + toggleSubmittingFlag: () => boolean
243 + }
244 + ): void
245 }>()
246
247 +const AddIcon = "carbon:add"
248 +
249 const {
237 - sourceConfigurationModel,
238 - showSourceField,
239 - arbitrarySourceField,
240 - disableSourceField,
241 - showIndexNameField,
242 - disableIndexNameField,
243 - applyFieldsSanitize,
244 - disabledSources
250 + sourceConfigurationModel,
251 + showSourceField,
252 + arbitrarySourceField,
253 + disableSourceField,
254 + showIndexNameField,
255 + disableIndexNameField,
256 + applyFieldsSanitize,
257 + disabledSources
258 } = toRefs(props)
259
260 const submitting = ref(false)
@@ -259,396 +272,400 @@ const indexNamesOptions = ref<{ label: string; value: string }[]>([])
272 const assetNameInput = ref("")
273
274 const rules: FormRules = {
262 - source: {
263 - required: true,
264 - message: "Please input the Source",
265 - trigger: ["input", "blur"]
266 - },
267 - field_names: {
268 - required: true,
269 - validator: validateAtLeastOne,
270 - trigger: ["input", "blur"]
271 - },
272 - asset_name_array: {
273 - required: true,
274 - validator: validateAtLeastOneAsset,
275 - trigger: ["input", "blur", "change"]
276 - },
277 - timefield_name: {
278 - required: true,
279 - message: "Please input the timefield name",
280 - trigger: ["input", "blur"]
281 - },
282 - alert_title_name: {
283 - required: true,
284 - message: "Please input the alert title name",
285 - trigger: ["input", "blur"]
286 - }
275 + source: {
276 + required: true,
277 + message: "Please input the Source",
278 + trigger: ["input", "blur"]
279 + },
280 + field_names: {
281 + required: true,
282 + validator: validateAtLeastOne,
283 + trigger: ["input", "blur"]
284 + },
285 + asset_name_array: {
286 + required: true,
287 + validator: validateAtLeastOneAsset,
288 + trigger: ["input", "blur", "change"]
289 + },
290 + timefield_name: {
291 + required: true,
292 + message: "Please input the timefield name",
293 + trigger: ["input", "blur"]
294 + },
295 + alert_title_name: {
296 + required: true,
297 + message: "Please input the alert title name",
298 + trigger: ["input", "blur"]
299 + }
300 }
301
302 let validationMessage: MessageReactive | null = null
303
304 const isSourceNotAllowed = computed(
292 - () => form.value.source && disabledSources.value?.length && disabledSources.value.includes(form.value.source)
305 + () => form.value.source && disabledSources.value?.length && disabledSources.value.includes(form.value.source)
306 )
307 const isFieldEnabled = computed(
295 - () => (!!form.value.index_name && !isSourceNotAllowed.value) || arbitrarySourceField.value
308 + () => (!!form.value.index_name && !isSourceNotAllowed.value) || arbitrarySourceField.value
309 )
310
311 const isValid = computed(() => {
299 - if (
300 - !form.value.field_names.length ||
301 - !form.value.asset_name_array?.length ||
302 - !form.value.timefield_name ||
303 - !form.value.alert_title_name ||
304 - !form.value.source ||
305 - isSourceNotAllowed.value
306 - ) {
307 - return false
308 - }
309 -
310 - return true
312 + if (
313 + !form.value.field_names.length ||
314 + !form.value.asset_name_array?.length ||
315 + !form.value.timefield_name ||
316 + !form.value.alert_title_name ||
317 + !form.value.source ||
318 + isSourceNotAllowed.value
319 + ) {
320 + return false
321 + }
322 +
323 + return true
324 })
325
326 const isSocfortressRecommendsAvailable = computed(() => form.value.source?.toLowerCase() === "wazuh")
327
328 const filteredAssetNameOptions = computed(() => {
316 - return availableMappingsOptions.value.filter(
317 - option => !form.value.asset_name_array?.includes(option.value)
318 - )
329 + return availableMappingsOptions.value.filter(option => !form.value.asset_name_array?.includes(option.value))
330 })
331
332 watch(sourceConfigurationModel, () => {
322 - reset()
323 - init()
333 + reset()
334 + init()
335 })
336
337 watch(
327 - () => form.value.index_name,
328 - val => {
329 - if (val) {
330 - getAvailableMappings(val)
331 - getSourceByIndex(val)
332 - } else {
333 - availableMappingsOptions.value = []
334 - }
335 - }
338 + () => form.value.index_name,
339 + val => {
340 + if (val) {
341 + getAvailableMappings(val)
342 + getSourceByIndex(val)
343 + } else {
344 + availableMappingsOptions.value = []
345 + }
346 + }
347 )
348
349 watch(
339 - () => form.value.asset_name_array,
340 - (newVal) => {
341 - if (newVal && newVal.length > 0) {
342 - form.value.asset_name = newVal.join(", ")
343 - } else {
344 - form.value.asset_name = null
345 - }
346 - },
347 - { deep: true }
350 + () => form.value.asset_name_array,
351 + newVal => {
352 + if (newVal && newVal.length > 0) {
353 + form.value.asset_name = newVal.join(", ")
354 + } else {
355 + form.value.asset_name = null
356 + }
357 + },
358 + { deep: true }
359 )
360
361 function renderAssetTag(tag: string, index: number) {
351 - return h(
352 - NTag,
353 - {
354 - type: index === 0 ? "primary" : "default",
355 - closable: true,
356 - onClose: () => {
357 - form.value.asset_name_array?.splice(index, 1)
358 - }
359 - },
360 - {
361 - default: () => tag,
362 - icon: () => (index === 0 ? h(Icon, { name: "carbon:star-filled", size: 14 }) : null)
363 - }
364 - )
362 + return h(
363 + NTag,
364 + {
365 + type: index === 0 ? "primary" : "default",
366 + closable: true,
367 + onClose: () => {
368 + form.value.asset_name_array?.splice(index, 1)
369 + }
370 + },
371 + {
372 + default: () => tag,
373 + icon: () => (index === 0 ? h(Icon, { name: "carbon:star-filled", size: 14 }) : null)
374 + }
375 + )
376 }
377
378 function handleAssetSelect(value: string, submit: (value: string) => void) {
368 - if (value && !form.value.asset_name_array?.includes(value)) {
369 - submit(value)
370 - assetNameInput.value = ""
371 - }
379 + if (value && !form.value.asset_name_array?.includes(value)) {
380 + submit(value)
381 + assetNameInput.value = ""
382 + }
383 }
384
385 function handleAssetEnter(submit: (value: string) => void) {
375 - const value = assetNameInput.value.trim()
376 - if (value && !form.value.asset_name_array?.includes(value)) {
377 - submit(value)
378 - assetNameInput.value = ""
379 - }
386 + const value = assetNameInput.value.trim()
387 + if (value && !form.value.asset_name_array?.includes(value)) {
388 + submit(value)
389 + assetNameInput.value = ""
390 + }
391 }
392
393 function resetIndexAvailable() {
383 - form.value.index_name = null
384 - if (form.value.source) {
385 - getAvailableIndices(form.value.source)
386 - }
394 + form.value.index_name = null
395 + if (form.value.source) {
396 + getAvailableIndices(form.value.source)
397 + }
398 }
399
400 function validateAtLeastOne(_rule: FormItemRule, value: string[]) {
390 - if (!value || !value.length) {
391 - return new Error("Please select at least one option")
392 - }
401 + if (!value || !value.length) {
402 + return new Error("Please select at least one option")
403 + }
404
394 - return true
405 + return true
406 }
407
408 function validateAtLeastOneAsset(_rule: FormItemRule, value: string[]) {
398 - if (!value || !value.length) {
399 - return new Error("Please add at least one asset name field")
400 - }
401 - return true
409 + if (!value || !value.length) {
410 + return new Error("Please add at least one asset name field")
411 + }
412 + return true
413 }
414
415 function validate(cb?: () => void) {
405 - if (!formRef.value) return
406 -
407 - formRef.value.validate((errors?: Array<FormValidationError>) => {
408 - if (!errors) {
409 - validationMessage?.destroy()
410 - validationMessage = null
411 - if (cb) cb()
412 - } else {
413 - if (!validationMessage) {
414 - validationMessage = message.warning("You must fill in the required fields correctly.")
415 - }
416 - return false
417 - }
418 - })
416 + if (!formRef.value) return
417 +
418 + formRef.value.validate((errors?: Array<FormValidationError>) => {
419 + if (!errors) {
420 + validationMessage?.destroy()
421 + validationMessage = null
422 + if (cb) cb()
423 + } else {
424 + if (!validationMessage) {
425 + validationMessage = message.warning("You must fill in the required fields correctly.")
426 + }
427 + return false
428 + }
429 + })
430 }
431
432 function getSourceConfigurationForm(): SourceConfigurationModel {
422 - const assetName = sourceConfigurationModel.value?.asset_name
423 - const assetNameArray = assetName
424 - ? assetName.split(",").map(s => s.trim()).filter(Boolean)
425 - : []
426 -
427 - return {
428 - field_names: sourceConfigurationModel.value?.field_names || [],
429 - ioc_field_names: sourceConfigurationModel.value?.ioc_field_names || [],
430 - asset_name: sourceConfigurationModel.value?.asset_name || null,
431 - asset_name_array: assetNameArray,
432 - timefield_name: sourceConfigurationModel.value?.timefield_name || null,
433 - alert_title_name: sourceConfigurationModel.value?.alert_title_name || null,
434 - source: sourceConfigurationModel.value?.source || "",
435 - index_name: sourceConfigurationModel.value?.index_name || null
436 - }
433 + const assetName = sourceConfigurationModel.value?.asset_name
434 + const assetNameArray = assetName
435 + ? assetName
436 + .split(",")
437 + .map(s => s.trim())
438 + .filter(Boolean)
439 + : []
440 +
441 + return {
442 + field_names: sourceConfigurationModel.value?.field_names || [],
443 + ioc_field_names: sourceConfigurationModel.value?.ioc_field_names || [],
444 + asset_name: sourceConfigurationModel.value?.asset_name || null,
445 + asset_name_array: assetNameArray,
446 + timefield_name: sourceConfigurationModel.value?.timefield_name || null,
447 + alert_title_name: sourceConfigurationModel.value?.alert_title_name || null,
448 + source: sourceConfigurationModel.value?.source || "",
449 + index_name: sourceConfigurationModel.value?.index_name || null
450 + }
451 }
452
453 function reset() {
440 - if (!loading.value) {
441 - resetForm()
442 - formRef.value?.restoreValidation()
443 - }
454 + if (!loading.value) {
455 + resetForm()
456 + formRef.value?.restoreValidation()
457 + }
458 }
459
460 function resetForm() {
447 - form.value = getSourceConfigurationForm()
461 + form.value = getSourceConfigurationForm()
462 }
463
464 function sanitizeFields() {
451 - const availableMappings = availableMappingsOptions.value.map(o => o.value)
452 -
453 - form.value.field_names = _intersection(availableMappings, form.value.field_names)
454 - form.value.ioc_field_names = _intersection(availableMappings, form.value.ioc_field_names)
455 -
456 - if (form.value.asset_name_array?.length) {
457 - form.value.asset_name_array = form.value.asset_name_array.filter(name =>
458 - availableMappings.includes(name) || arbitrarySourceField.value
459 - )
460 - }
461 -
462 - if (form.value.timefield_name && !availableMappings.includes(form.value.timefield_name)) {
463 - form.value.timefield_name = null
464 - }
465 - if (form.value.alert_title_name && !availableMappings.includes(form.value.alert_title_name)) {
466 - form.value.alert_title_name = null
467 - }
465 + const availableMappings = availableMappingsOptions.value.map(o => o.value)
466 +
467 + form.value.field_names = _intersection(availableMappings, form.value.field_names)
468 + form.value.ioc_field_names = _intersection(availableMappings, form.value.ioc_field_names)
469 +
470 + if (form.value.asset_name_array?.length) {
471 + form.value.asset_name_array = form.value.asset_name_array.filter(
472 + name => availableMappings.includes(name) || arbitrarySourceField.value
473 + )
474 + }
475 +
476 + if (form.value.timefield_name && !availableMappings.includes(form.value.timefield_name)) {
477 + form.value.timefield_name = null
478 + }
479 + if (form.value.alert_title_name && !availableMappings.includes(form.value.alert_title_name)) {
480 + form.value.alert_title_name = null
481 + }
482 }
483
470 -function submit() {
471 - const payload: SourceConfiguration = {
472 - field_names: form.value?.field_names || [],
473 - ioc_field_names: form.value?.ioc_field_names || [],
474 - asset_name: form.value?.asset_name || "",
475 - timefield_name: form.value?.timefield_name || "",
476 - alert_title_name: form.value?.alert_title_name || "",
477 - source: form.value?.source || ""
478 - }
479 - emit("submitted", payload)
484 +function submitForm() {
485 + const payload: SourceConfiguration = {
486 + field_names: form.value?.field_names || [],
487 + ioc_field_names: form.value?.ioc_field_names || [],
488 + asset_name: form.value?.asset_name || "",
489 + timefield_name: form.value?.timefield_name || "",
490 + alert_title_name: form.value?.alert_title_name || "",
491 + source: form.value?.source || ""
492 + }
493 + emit("submitted", payload)
494 }
495
496 function toggleSubmittingFlag(status?: boolean) {
483 - if (status !== undefined) {
484 - submitting.value = status
485 - } else {
486 - submitting.value = !submitting.value
487 - }
497 + if (status !== undefined) {
498 + submitting.value = status
499 + } else {
500 + submitting.value = !submitting.value
501 + }
502
489 - return submitting.value
503 + return submitting.value
504 }
505
506 function resetSource() {
493 - form.value.source = ""
507 + form.value.source = ""
508 }
509
510 function setSocfortressRecommendsWazuh() {
497 - form.value.field_names = socfortressRecommendsWazuh.value?.field_names || []
498 - form.value.ioc_field_names = socfortressRecommendsWazuh.value?.ioc_field_names || []
499 -
500 - const assetName = socfortressRecommendsWazuh.value?.asset_name
501 - form.value.asset_name_array = assetName
502 - ? assetName.split(",").map(s => s.trim()).filter(Boolean)
503 - : []
504 -
505 - form.value.timefield_name = socfortressRecommendsWazuh.value?.timefield_name || null
506 - form.value.alert_title_name = socfortressRecommendsWazuh.value?.alert_title_name || null
507 - form.value.source = socfortressRecommendsWazuh.value?.source || ""
511 + form.value.field_names = socfortressRecommendsWazuh.value?.field_names || []
512 + form.value.ioc_field_names = socfortressRecommendsWazuh.value?.ioc_field_names || []
513 +
514 + const assetName = socfortressRecommendsWazuh.value?.asset_name
515 + form.value.asset_name_array = assetName
516 + ? assetName
517 + .split(",")
518 + .map(s => s.trim())
519 + .filter(Boolean)
520 + : []
521 +
522 + form.value.timefield_name = socfortressRecommendsWazuh.value?.timefield_name || null
523 + form.value.alert_title_name = socfortressRecommendsWazuh.value?.alert_title_name || null
524 + form.value.source = socfortressRecommendsWazuh.value?.source || ""
525 }
526
527 function getSocfortressRecommendsWazuh() {
511 - if (socfortressRecommendsWazuh.value) {
512 - setSocfortressRecommendsWazuh()
513 - return
514 - }
515 -
516 - loadingSocfortressRecommendsWazuh.value = true
517 -
518 - Api.incidentManagement.sources
519 - .getSocfortressRecommendsWazuh()
520 - .then(res => {
521 - if (res.data.success) {
522 - socfortressRecommendsWazuh.value = {
523 - field_names: res.data.field_names,
524 - ioc_field_names: res.data.ioc_field_names,
525 - asset_name: res.data.asset_name,
526 - timefield_name: res.data.timefield_name,
527 - alert_title_name: res.data.alert_title_name,
528 - source: res.data.source
529 - }
530 -
531 - setSocfortressRecommendsWazuh()
532 - } else {
533 - message.warning(res.data?.message || "An error occurred. Please try again later.")
534 - }
535 - })
536 - .catch(err => {
537 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
538 - })
539 - .finally(() => {
540 - loadingSocfortressRecommendsWazuh.value = false
541 - })
528 + if (socfortressRecommendsWazuh.value) {
529 + setSocfortressRecommendsWazuh()
530 + return
531 + }
532 +
533 + loadingSocfortressRecommendsWazuh.value = true
534 +
535 + Api.incidentManagement.sources
536 + .getSocfortressRecommendsWazuh()
537 + .then(res => {
538 + if (res.data.success) {
539 + socfortressRecommendsWazuh.value = {
540 + field_names: res.data.field_names,
541 + ioc_field_names: res.data.ioc_field_names,
542 + asset_name: res.data.asset_name,
543 + timefield_name: res.data.timefield_name,
544 + alert_title_name: res.data.alert_title_name,
545 + source: res.data.source
546 + }
547 +
548 + setSocfortressRecommendsWazuh()
549 + } else {
550 + message.warning(res.data?.message || "An error occurred. Please try again later.")
551 + }
552 + })
553 + .catch(err => {
554 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
555 + })
556 + .finally(() => {
557 + loadingSocfortressRecommendsWazuh.value = false
558 + })
559 }
560
561 function getAvailableMappings(indexName: string) {
545 - loadingAvailableMappings.value = true
546 -
547 - Api.incidentManagement.sources
548 - .getAvailableMappings(indexName)
549 - .then(res => {
550 - if (res.data.success) {
551 - availableMappingsOptions.value = (res.data?.available_mappings || []).map(o => ({
552 - label: o,
553 - value: o
554 - }))
555 - if (applyFieldsSanitize.value) {
556 - sanitizeFields()
557 - }
558 - } else {
559 - message.warning(res.data?.message || "An error occurred. Please try again later.")
560 - }
561 - })
562 - .catch(err => {
563 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
564 - })
565 - .finally(() => {
566 - loadingAvailableMappings.value = false
567 - })
562 + loadingAvailableMappings.value = true
563 +
564 + Api.incidentManagement.sources
565 + .getAvailableMappings(indexName)
566 + .then(res => {
567 + if (res.data.success) {
568 + availableMappingsOptions.value = (res.data?.available_mappings || []).map(o => ({
569 + label: o,
570 + value: o
571 + }))
572 + if (applyFieldsSanitize.value) {
573 + sanitizeFields()
574 + }
575 + } else {
576 + message.warning(res.data?.message || "An error occurred. Please try again later.")
577 + }
578 + })
579 + .catch(err => {
580 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
581 + })
582 + .finally(() => {
583 + loadingAvailableMappings.value = false
584 + })
585 }
586
587 function getAvailableIndices(source: SourceName) {
571 - loadingIndexNames.value = true
572 -
573 - Api.incidentManagement.sources
574 - .getAvailableIndices(source)
575 - .then(res => {
576 - if (res.data.success) {
577 - indexNamesOptions.value = (res.data?.indices || []).map(o => ({
578 - label: o,
579 - value: o
580 - }))
581 - } else {
582 - resetSource()
583 - message.warning(res.data?.message || "An error occurred. Please try again later.")
584 - }
585 - })
586 - .catch(err => {
587 - resetSource()
588 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
589 - })
590 - .finally(() => {
591 - loadingIndexNames.value = false
592 - })
588 + loadingIndexNames.value = true
589 +
590 + Api.incidentManagement.sources
591 + .getAvailableIndices(source)
592 + .then(res => {
593 + if (res.data.success) {
594 + indexNamesOptions.value = (res.data?.indices || []).map(o => ({
595 + label: o,
596 + value: o
597 + }))
598 + } else {
599 + resetSource()
600 + message.warning(res.data?.message || "An error occurred. Please try again later.")
601 + }
602 + })
603 + .catch(err => {
604 + resetSource()
605 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
606 + })
607 + .finally(() => {
608 + loadingIndexNames.value = false
609 + })
610 }
611
612 function getSourceByIndex(indexName: string) {
596 - loadingSource.value = true
597 -
598 - Api.incidentManagement.sources
599 - .getSourceByIndex(indexName)
600 - .then(res => {
601 - if (res.data.success) {
602 - form.value.source = res.data.source
603 - } else {
604 - resetSource()
605 - message.warning(res.data?.message || "An error occurred. Please try again later.")
606 - }
607 - })
608 - .catch(err => {
609 - resetSource()
610 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
611 - })
612 - .finally(() => {
613 - loadingSource.value = false
614 - })
613 + loadingSource.value = true
614 +
615 + Api.incidentManagement.sources
616 + .getSourceByIndex(indexName)
617 + .then(res => {
618 + if (res.data.success) {
619 + form.value.source = res.data.source
620 + } else {
621 + resetSource()
622 + message.warning(res.data?.message || "An error occurred. Please try again later.")
623 + }
624 + })
625 + .catch(err => {
626 + resetSource()
627 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
628 + })
629 + .finally(() => {
630 + loadingSource.value = false
631 + })
632 }
633
634 function init() {
618 - if (sourceConfigurationModel.value?.index_name) {
619 - getAvailableMappings(sourceConfigurationModel.value.index_name)
620 -
621 - if (!sourceConfigurationModel.value?.source) {
622 - getSourceByIndex(sourceConfigurationModel.value.index_name)
623 - }
624 - }
625 - if (sourceConfigurationModel.value?.source) {
626 - getAvailableIndices(sourceConfigurationModel.value.source)
627 - }
635 + if (sourceConfigurationModel.value?.index_name) {
636 + getAvailableMappings(sourceConfigurationModel.value.index_name)
637 +
638 + if (!sourceConfigurationModel.value?.source) {
639 + getSourceByIndex(sourceConfigurationModel.value.index_name)
640 + }
641 + }
642 + if (sourceConfigurationModel.value?.source) {
643 + getAvailableIndices(sourceConfigurationModel.value.source)
644 + }
645 }
646
647 onBeforeMount(() => {
631 - init()
648 + init()
649 })
650
651 onMounted(() => {
635 - emit("mounted", {
636 - reset,
637 - toggleSubmittingFlag
638 - })
652 + emit("mounted", {
653 + reset,
654 + toggleSubmittingFlag
655 + })
656 })
657 </script>
658
659 <style lang="scss" scoped>
660 .source-field {
644 - :deep() {
645 - .n-form-item-label__text {
646 - width: 100%;
647 - }
648 - }
661 + :deep() {
662 + .n-form-item-label__text {
663 + width: 100%;
664 + }
665 + }
666 }
667
668 :deep(.n-dynamic-tags) {
652 - width: 100%;
669 + width: 100%;
670 }
671 </style>
frontend/src/components/overview/HealthcheckCard.vue
+10 -3
@@ -23,7 +23,7 @@ import CardStatsIcon from "@/components/common/cards/CardStatsIcon.vue"
23 import CardStatsMulti from "@/components/common/cards/CardStatsMulti.vue"
24 import { useGoto } from "@/composables/useGoto"
25 import { useThemeStore } from "@/stores/theme"
26 -import { InfluxDBAlertLevel } from "@/types/healthchecks.d"
26 +import { InfluxDBAlertSeverity } from "@/types/healthchecks.d"
27
28 const HealthcheckIcon = "ph:heartbeat"
29 const { gotoHealthcheck } = useGoto()
@@ -31,12 +31,15 @@ const message = useMessage()
31 const loading = ref(false)
32 const healthcheck = ref<InfluxDBAlert[]>([])
33 const style = computed(() => useThemeStore().style)
34 +
35 const total = computed<number>(() => {
36 return healthcheck.value.length || 0
37 })
38 +
39 const criticalTotal = computed<number>(() => {
38 - return healthcheck.value.filter(o => o.level === InfluxDBAlertLevel.Crit).length || 0
40 + return healthcheck.value.filter(o => o.severity === InfluxDBAlertSeverity.Critical).length || 0
41 })
42 +
43 const values = computed<ItemProps[]>(() => [
44 { value: total.value, label: "Total" },
45 { value: criticalTotal.value, label: "Critical", status: criticalTotal.value ? "warning" : undefined }
@@ -46,7 +49,11 @@ function getData() {
49 loading.value = true
50
51 Api.healthchecks
49 - .getHealthchecks()
52 + .getHealthchecks({
53 + days: 1,
54 + status: "active",
55 + exclude_ok: true
56 + })
57 .then(res => {
58 if (res.data.success) {
59 healthcheck.value = res.data.alerts || []
frontend/src/stores/healthcheck.ts
+8 -16
@@ -2,7 +2,7 @@ import type { InfluxDBAlert } from "@/types/healthchecks.d"
2 import _toNumber from "lodash/toNumber"
3 import { acceptHMRUpdate, defineStore } from "pinia"
4 import Api from "@/api"
5 -import { InfluxDBAlertLevel } from "@/types/healthchecks.d"
5 +import { InfluxDBAlertSeverity } from "@/types/healthchecks.d"
6 import { IndexHealth } from "@/types/indices.d"
7 import { useAuthStore } from "./auth"
8
@@ -52,26 +52,18 @@ export const useHealthcheckStore = defineStore("healthcheck", {
52 },
53 getHealthchecks() {
54 Api.healthchecks
55 - .getHealthchecks()
55 + .getHealthchecks({
56 + days: 1,
57 + status: "active",
58 + exclude_ok: true
59 + })
60 .then(res => {
61 if (res.data.success) {
58 - this.alerts = res.data.alerts.filter(o => o.level === InfluxDBAlertLevel.Crit)
62 + // Filter to only show critical alerts
63 + this.alerts = res.data.alerts.filter(o => o.severity === InfluxDBAlertSeverity.Critical)
64 } else {
65 this.alerts = null
66 }
62 -
63 - /*
64 - this.alerts = [
65 - {
66 - time: new Date(),
67 - message: "string",
68 - checkID: "string",
69 - checkName: "string",
70 - level: InfluxDBAlertLevel.Crit
71 - }
72 - ]
73 - this.alerts = []
74 - */
67 })
68 .catch(() => {
69 this.alerts = null
frontend/src/types/healthchecks.d.ts
+41 -5
@@ -1,12 +1,48 @@
1 export interface InfluxDBAlert {
2 time: string | Date
3 + check_name: string
4 + sensor_type: string
5 + severity: InfluxDBAlertSeverity
6 message: string
4 - checkID: string
5 - checkName: string
6 - level: InfluxDBAlertLevel
7 + status: InfluxDBAlertStatus
8 + check_id?: string
9 }
10
9 -export enum InfluxDBAlertLevel {
11 +export enum InfluxDBAlertSeverity {
12 Ok = "ok",
11 - Crit = "crit"
13 + Info = "info",
14 + Warning = "warning",
15 + Critical = "critical"
16 +}
17 +
18 +export enum InfluxDBAlertStatus {
19 + Active = "active",
20 + Cleared = "cleared"
21 +}
22 +
23 +export interface InfluxDBAlertResponse {
24 + success: boolean
25 + message: string
26 + alerts: InfluxDBAlert[]
27 + total_count: number
28 + filtered_count: number
29 + active_alerts_count: number
30 + cleared_alerts_count: number
31 +}
32 +
33 +export interface InfluxDBAlertQueryParams {
34 + days?: number
35 + severity?: InfluxDBAlertSeverity[]
36 + check_name?: string
37 + sensor_type?: string
38 + status?: "active" | "cleared" | "all"
39 + latest_only?: boolean
40 + exclude_ok?: boolean
41 +}
42 +
43 +export interface InfluxDBCheckNamesResponse {
44 + success: boolean
45 + message: string
46 + check_names: string[]
47 + total_count: number
48 }