Skip to content

Commit ac00dae

Browse files
committed
Fixed caching of OhDear endpoints
1 parent 9a38926 commit ac00dae

8 files changed

Lines changed: 175 additions & 81 deletions

File tree

Lines changed: 4 additions & 0 deletions
Loading

client/src/icons/monitoring_ok.svg

Lines changed: 11 additions & 0 deletions
Loading

client/src/locale/nl.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,10 +1351,10 @@ const nl = {
13511351
week: "Week",
13521352
custom: "Aangepast",
13531353
export: "Exporteer CSV",
1354-
total: "inlogpogingen",
1354+
total: "Logins",
13551355
unique: "unieke gebruikers",
13561356
overTime: "Over de tijd",
1357-
logins: "Inlogpogingen",
1357+
logins: "Logins",
13581358
uniqueUsers: "Unieke gebruikers",
13591359
perInstitute: "per instelling",
13601360
perApp: "per app",
@@ -1363,7 +1363,7 @@ const nl = {
13631363
showMore: "Toon meer",
13641364
absolute: "#",
13651365
percentage: "%",
1366-
successRate: "succesvolle inlogpogingen",
1366+
successRate: "succesvolle logins",
13671367
from: "Van",
13681368
to: "Tot",
13691369
for: "voor",

client/src/pages/Monitoring.jsx

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@ import "./Monitoring.scss";
44
import {monitoring} from "../api/index.js";
55
import I18n from "../locale/I18n.js";
66
import 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";
79
import SegmentedControl from "../components/SegmentedControl.jsx";
810
import {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
1413
const PERIOD_DEFAULT = 60;
1514
const 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

2229
function 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

116122
function 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>&nbsp;
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")}&nbsp;
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>

client/src/pages/Monitoring.scss

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,13 @@
5656
.period-control {
5757
display: flex;
5858
flex-direction: column;
59-
align-items: flex-end;
6059
gap: 6px;
6160
flex-shrink: 0;
6261

6362
.period-label {
64-
font-size: 0.85rem;
65-
font-weight: 600;
66-
color: var(--sl-color-neutral-700);
63+
font-size: 1rem;
64+
font-weight: 700;
65+
color: var(--sl-color-neutral-900);
6766
}
6867
}
6968
}
@@ -272,8 +271,16 @@
272271
background: #a8dfc0;
273272
}
274273

275-
&.incident {
276-
background: #f5c6cb;
274+
&.warn-light {
275+
background: var(--sl-color-warning-50);
276+
}
277+
278+
&.warn-heavy {
279+
background: var(--sl-color-warning-600);
280+
}
281+
282+
&.critical {
283+
background: var(--sds--color--red--400);
277284
}
278285

279286
&:hover {
@@ -332,34 +339,78 @@
332339
}
333340
}
334341

335-
// Single event row (resolved / started)
336-
.event {
342+
// Timeline incident entry
343+
.incident-entry {
337344
display: flex;
345+
flex-direction: row;
338346
gap: 12px;
339-
align-items: flex-start;
347+
align-items: stretch;
340348

341-
.event-icon {
342-
flex-shrink: 0;
349+
// Left column: icons stacked with connector between them
350+
.entry-icons {
343351
display: flex;
352+
flex-direction: column;
344353
align-items: center;
345-
padding-top: 1px;
354+
flex-shrink: 0;
355+
356+
.event-connector {
357+
width: 2px;
358+
background: #d0d0d0;
359+
flex: 1;
360+
min-height: 12px;
361+
}
346362
}
347363

348-
.event-body {
364+
// Right column: text rows aligned to each icon
365+
.entry-texts {
349366
display: flex;
350367
flex-direction: column;
351-
gap: 2px;
368+
flex: 1;
369+
gap: 0;
370+
}
371+
}
352372

353-
.event-message {
354-
font-size: 0.9rem;
355-
font-weight: 500;
356-
color: var(--sl-color-neutral-800);
357-
}
373+
.event-circle {
374+
display: inline-flex;
375+
align-items: center;
376+
justify-content: center;
377+
border-radius: 50%;
378+
flex-shrink: 0;
379+
width: 36px;
380+
height: 36px;
358381

359-
.event-time {
360-
font-size: 0.8rem;
361-
color: var(--sl-color-neutral-500);
362-
}
382+
svg {
383+
width: 60%;
384+
height: 60%;
385+
}
386+
387+
&--ok {
388+
background: #c8e6c9;
389+
color: #2e7d32;
390+
}
391+
392+
&--incident {
393+
background: #fff9c4;
394+
color: #a37a00;
395+
}
396+
}
397+
398+
.event-body {
399+
display: flex;
400+
flex-direction: column;
401+
gap: 2px;
402+
justify-content: center;
403+
min-height: 36px; // match icon height so text aligns to circle centre
404+
405+
.event-message {
406+
font-size: 0.9rem;
407+
font-weight: 500;
408+
color: var(--sl-color-neutral-800);
409+
}
410+
411+
.event-time {
412+
font-size: 0.8rem;
413+
color: var(--sl-color-neutral-500);
363414
}
364415
}
365416
}

0 commit comments

Comments
 (0)