@@ -4,20 +4,27 @@ import "./Monitoring.scss";
44import { monitoring } from "../api/index.js" ;
55import I18n from "../locale/I18n.js" ;
66import SearchIcon from "@surfnet/sds/icons/functional-icons/search.svg" ;
7+ import CheckPlainIcon from "../icons/check-plain.svg" ;
8+ import MonitoringIncidentIcon from "../icons/monitoring_incident.svg" ;
79import SegmentedControl from "../components/SegmentedControl.jsx" ;
810import { formatDate } from "../utils/Date.js" ;
911
10- // Configurable: incidents shorter than this many minutes are ignored
11- const MIN_INCIDENT_MINUTES = 10 ;
12-
1312// Period constants
1413const PERIOD_DEFAULT = 60 ;
1514const PERIOD_ALL = 364 ;
1615
17- // Number of bar segments in the uptime chart
18- const SEGMENT_COUNT = 90 ;
16+ // Severity thresholds (downtime minutes) and their CSS classes + bar colors.
17+ // Evaluated in order — first match wins. Segments with no downtime use "ok".
18+ const SEVERITY_LEVELS = [
19+ { minMinutes : 0 , maxMinutes : 1 , cls : "ok" , color : "#a8dfc0" } ,
20+ { minMinutes : 1 , maxMinutes : 5 , cls : "warn-light" , color : "var(--sl-color-warning-50)" } ,
21+ { minMinutes : 5 , maxMinutes : 15 , cls : "warn-heavy" , color : "var(--sl-color-warning-600)" } ,
22+ { minMinutes : 15 , maxMinutes : Infinity , cls : "critical" , color : "var(--sds--color--red--400)" } ,
23+ ] ;
1924
20- // ─── helpers ──────────────────────────────────────────────────────────────────
25+ // Incidents shorter than the first non-ok severity threshold (minutes) are ignored.
26+ // Derived from SEVERITY_LEVELS so there is a single source of truth.
27+ const MIN_INCIDENT_MINUTES = SEVERITY_LEVELS . find ( l => l . cls !== "ok" ) ?. minMinutes ?? 1 ;
2128
2229function isSignificant ( incident ) {
2330 if ( ! incident . resolvedAt ) return true ;
@@ -30,9 +37,9 @@ function buildUptimeSegments(incidents, period) {
3037 const significant = incidents . filter ( isSignificant ) ;
3138 const now = Date . now ( ) ;
3239 const startMs = now - period * 24 * 60 * 60 * 1000 ;
33- const segMs = ( now - startMs ) / SEGMENT_COUNT ;
40+ const segMs = ( now - startMs ) / period ;
3441
35- return Array . from ( { length : SEGMENT_COUNT } , ( _ , i ) => {
42+ return Array . from ( { length : period } , ( _ , i ) => {
3643 const segStart = startMs + i * segMs ;
3744 const segEnd = startMs + ( i + 1 ) * segMs ;
3845
@@ -50,7 +57,6 @@ function buildUptimeSegments(incidents, period) {
5057 }
5158 }
5259
53- const hasIncident = downtimeMs > 0 ;
5460 const uptimePct = 100 - ( downtimeMs / segMs ) * 100 ;
5561
5662 // Tooltip date: use the midpoint of the segment
@@ -62,7 +68,9 @@ function buildUptimeSegments(incidents, period) {
6268 const downtimeMin = Math . floor ( downtimeSec / 60 ) ;
6369 const downtimeRemSec = downtimeSec % 60 ;
6470
65- return { hasIncident, uptimePct, segDate, downtimeMin, downtimeRemSec} ;
71+ const severity = SEVERITY_LEVELS . find ( l => downtimeMin >= l . minMinutes && downtimeMin < l . maxMinutes ) ?. cls ?? "ok" ;
72+
73+ return { severity, uptimePct, segDate, downtimeMin, downtimeRemSec} ;
6674 } ) ;
6775}
6876
@@ -103,23 +111,19 @@ function groupIncidentsByDate(incidents) {
103111
104112// ─── icons ────────────────────────────────────────────────────────────────────
105113
106- function CheckCircleIcon ( { small} ) {
107- const size = small ? 22 : 28 ;
114+ function CheckCircleIcon ( ) {
108115 return (
109- < svg width = { size } height = { size } viewBox = "0 0 28 28" fill = "none" xmlns = "http://www.w3.org/2000/svg" >
110- < circle cx = "14" cy = "14" r = "14" fill = "#d4edda" />
111- < path d = "M8 14.5l4 4 8-8" stroke = "#2e7d32" strokeWidth = "2.2" strokeLinecap = "round" strokeLinejoin = "round" />
112- </ svg >
116+ < span className = "event-circle event-circle--ok" >
117+ < CheckPlainIcon />
118+ </ span >
113119 ) ;
114120}
115121
116122function WarningCircleIcon ( ) {
117123 return (
118- < svg width = "22" height = "22" viewBox = "0 0 22 22" fill = "none" xmlns = "http://www.w3.org/2000/svg" >
119- < circle cx = "11" cy = "11" r = "11" fill = "#fff3cd" />
120- < path d = "M11 6v6" stroke = "#856404" strokeWidth = "2" strokeLinecap = "round" />
121- < circle cx = "11" cy = "15.5" r = "1.2" fill = "#856404" />
122- </ svg >
124+ < span className = "event-circle event-circle--incident" >
125+ < MonitoringIncidentIcon />
126+ </ span >
123127 ) ;
124128}
125129
@@ -328,7 +332,7 @@ export const Monitoring = () => {
328332 { uptimeSegments . map ( ( seg , i ) => (
329333 < div
330334 key = { i }
331- className = { `bar-segment ${ seg . hasIncident ? "incident" : "ok" } ` }
335+ className = { `bar-segment ${ seg . severity } ` }
332336 onMouseEnter = { e => handleSegmentMouseEnter ( e , seg ) }
333337 onMouseLeave = { handleSegmentMouseLeave }
334338 />
@@ -352,7 +356,7 @@ export const Monitoring = () => {
352356 < strong > { tooltip . seg . uptimePct . toFixed ( 2 ) } %</ strong >
353357 { I18n . t ( "monitoring.tooltipOfTheTime" ) }
354358 </ div >
355- { tooltip . seg . hasIncident && (
359+ { tooltip . seg . severity !== "ok" && (
356360 < div className = "bar-tooltip--downtime" >
357361 { I18n . t ( "monitoring.tooltipDowntime" ) }
358362 < strong >
@@ -394,32 +398,47 @@ export const Monitoring = () => {
394398 < span className = "incident-date" > { group . date } </ span >
395399 < div className = "incident-events" >
396400 { group . incidents . map ( ( inc , idx ) => (
397- < React . Fragment key = { idx } >
398- { inc . resolvedTime && (
399- < div className = "event resolved" >
400- < span className = "event-icon" >
401- < CheckCircleIcon small />
402- </ span >
403- < div className = "event-body" >
404- < span className = "event-message" >
405- { I18n . t ( "monitoring.resolvedMessage" , { name : selectedService . name } ) }
406- </ span >
407- < span className = "event-time" > { inc . resolvedTime } </ span >
401+ < div key = { idx } className = "incident-entry" >
402+ { inc . resolvedTime ? (
403+ < >
404+ { /* Left: icons + connector */ }
405+ < div className = "entry-icons" >
406+ < CheckCircleIcon />
407+ < div className = "event-connector" />
408+ < WarningCircleIcon />
409+ </ div >
410+ { /* Right: text rows */ }
411+ < div className = "entry-texts" >
412+ < div className = "event-body" >
413+ < span className = "event-message" >
414+ { I18n . t ( "monitoring.resolvedMessage" , { name : selectedService . name } ) }
415+ </ span >
416+ < span className = "event-time" > { inc . resolvedTime } </ span >
417+ </ div >
418+ < div className = "event-body" >
419+ < span className = "event-message" >
420+ { I18n . t ( "monitoring.startedMessage" , { name : selectedService . name } ) }
421+ </ span >
422+ < span className = "event-time" > { inc . startedTime } </ span >
423+ </ div >
424+ </ div >
425+ </ >
426+ ) : (
427+ < >
428+ < div className = "entry-icons" >
429+ < WarningCircleIcon />
430+ </ div >
431+ < div className = "entry-texts" >
432+ < div className = "event-body" >
433+ < span className = "event-message" >
434+ { I18n . t ( "monitoring.startedMessage" , { name : selectedService . name } ) }
435+ </ span >
436+ < span className = "event-time" > { inc . startedTime } </ span >
437+ </ div >
408438 </ div >
409- </ div >
439+ </ >
410440 ) }
411- < div className = "event started" >
412- < span className = "event-icon" >
413- < WarningCircleIcon />
414- </ span >
415- < div className = "event-body" >
416- < span className = "event-message" >
417- { I18n . t ( "monitoring.startedMessage" , { name : selectedService . name } ) }
418- </ span >
419- < span className = "event-time" > { inc . startedTime } </ span >
420- </ div >
421- </ div >
422- </ React . Fragment >
441+ </ div >
423442 ) ) }
424443 </ div >
425444 </ div >
0 commit comments