From 5cce38f9cee3fa1ce9633bbb2191b685fd60c7c1 Mon Sep 17 00:00:00 2001 From: Wes Johnson Date: Thu, 11 Sep 2025 16:43:39 +0200 Subject: [PATCH 01/35] dummy commit --- client/src/root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/root.tsx b/client/src/root.tsx index f605592557..df42cdbca2 100644 --- a/client/src/root.tsx +++ b/client/src/root.tsx @@ -46,7 +46,7 @@ export const DEFAULT_META: MetaDescriptor[] = [ { name: "description", content: - "An open-source platform for reproducible and collaborative data science. Share code, data and computational environments whilst tracking provenance and lineage of research objects.", + "An open-source platform reproducible and collaborative data science. Share code, data and computational environments whilst tracking provenance and lineage of research objects.", }, { property: "og:title", From 9c155ec9c33f6f2238b7259313bbcadec6d6b157 Mon Sep 17 00:00:00 2001 From: Wes Johnson Date: Fri, 12 Sep 2025 15:02:04 +0200 Subject: [PATCH 02/35] feat(sessionsV2): add Prometheus query modal --- .../prometheusModal/prometheusModal.tsx | 323 ++++++++++++++++++ .../SessionShowPage/ShowSessionPage.tsx | 83 ++++- server/src/config.ts | 3 + server/src/websocket/handlers/prometheus.ts | 119 +++++++ server/src/websocket/index.ts | 8 + 5 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 client/src/components/prometheusModal/prometheusModal.tsx create mode 100644 server/src/websocket/handlers/prometheus.ts diff --git a/client/src/components/prometheusModal/prometheusModal.tsx b/client/src/components/prometheusModal/prometheusModal.tsx new file mode 100644 index 0000000000..dc2e70c36a --- /dev/null +++ b/client/src/components/prometheusModal/prometheusModal.tsx @@ -0,0 +1,323 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable spellcheck/spell-checker */ +import cx from "classnames"; +import { useCallback, useState, useEffect, useRef } from "react"; +import { Activity, Search, Cpu, Memory } from "react-bootstrap-icons"; +import { Alert, Button, Card, CardBody, Input, InputGroup } from "reactstrap"; + +interface PrometheusQueryResult { + status: string; + data: { + resultType: string; + result: Array<{ + metric: Record; + value?: [number, string]; + values?: Array<[number, string]>; + }>; + }; + requestId?: string; + error?: string; +} + +// Hook to manage WebSocket connection and Prometheus queries +function usePrometheusWebSocket() { + const [ws, setWs] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const pendingRequests = useRef< + Map< + string, + { + resolve: (result: PrometheusQueryResult) => void; + reject: (error: any) => void; + } + > + >(new Map()); + + useEffect(() => { + // Connect to existing WebSocket server + const wsUrl = `wss://${window.location.host}/ui-server/ws`; + const websocket = new WebSocket(wsUrl); + + websocket.onopen = () => { + setIsConnected(true); + setWs(websocket); + }; + + websocket.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + + // Handle Prometheus query responses + if (message.type === "prometheusQuery" && message.data?.requestId) { + const pending = pendingRequests.current.get(message.data.requestId); + if (pending) { + pendingRequests.current.delete(message.data.requestId); + if (message.data.error) { + pending.reject(new Error(message.data.error)); + } else { + pending.resolve(message.data); + } + } + } + } catch (error) { + // Ignore parsing errors for non-Prometheus messages + } + }; + + websocket.onerror = () => { + setIsConnected(false); + }; + + websocket.onclose = () => { + setIsConnected(false); + setWs(null); + }; + + return () => { + websocket.close(); + }; + }, []); + + const sendPrometheusQuery = useCallback( + async (query: string): Promise => { + if (!ws || !isConnected) { + throw new Error("WebSocket not connected"); + } + + const requestId = `prometheus-${Date.now()}-${Math.random()}`; + + return new Promise((resolve, reject) => { + // Store promise handlers + pendingRequests.current.set(requestId, { resolve, reject }); + + // Set timeout + setTimeout(() => { + if (pendingRequests.current.has(requestId)) { + pendingRequests.current.delete(requestId); + reject(new Error("Request timeout")); + } + }, 10000); + + // Send message in the expected WsClientMessage format + const message = { + timestamp: new Date(), + type: "prometheusQuery", + data: { + query, + requestId, + }, + }; + + ws.send(JSON.stringify(message)); + }); + }, + [ws, isConnected] + ); + + return { sendPrometheusQuery, isConnected }; +} + +interface PrometheusQueryBoxProps { + className?: string; + predefinedQueries?: Array<{ + label: string; + query: string; + description?: string; + icon?: string; + unit?: string; + }>; +} + +export function PrometheusQueryBox({ + className, + predefinedQueries, +}: PrometheusQueryBoxProps) { + const [inputValue, setInputValue] = useState(""); + const [queryResult, setQueryResult] = useState( + null + ); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const { sendPrometheusQuery, isConnected } = usePrometheusWebSocket(); + + const executeQuery = useCallback( + async (query: string) => { + if (!query.trim()) return; + + setIsLoading(true); + setError(null); + setQueryResult(null); + + try { + const result = await sendPrometheusQuery(query.trim()); + setQueryResult(result); + } catch (err) { + setError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setIsLoading(false); + } + }, + [sendPrometheusQuery] + ); + + const handleSubmit = useCallback(() => { + executeQuery(inputValue); + }, [inputValue, executeQuery]); + + const handlePredefinedQuery = useCallback( + (predefinedQuery: string) => { + setInputValue(predefinedQuery); + executeQuery(predefinedQuery); + }, + [executeQuery] + ); + + { + /* + const handleKeyPress = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleSubmit(); + } + }, + [handleSubmit] + ); + */ + } + + const hasResults = queryResult?.data?.result?.length > 0; + + return ( + + +
+
+ + Prometheus Query +
+ + {predefinedQueries.length > 0 && ( +
+
+ {predefinedQueries.map((pq, index) => ( + + ))} +
+
+ )} + + {/* + + setInputValue(e.target.value)} + onKeyPress={handleKeyPress} + /> + + + */} +
+ + {!isConnected && ( + + WebSocket not connected + + )} + + {isLoading && ( +
+ Querying Prometheus... +
+ )} + + {error && ( + + Could not query Prometheus: {error} + + )} + + {queryResult && !hasResults && ( + + No data returned from Prometheus + + )} + + {hasResults && ( +
+
+ {queryResult.data.result.map((result, index) => ( +
+
+ {result.value + ? `Value: ${result.value[1]}` + : result.values + ? `${result.values.length} time series points` + : "No value"} + {predefinedQueries && + predefinedQueries.length > 0 && + predefinedQueries.map((pq) => { + if ( + pq.query === inputValue.trim() && + pq.unit && + result.value + ) { + return ` ${pq.unit}`; + } + return ""; + })} +
+
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx b/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx index 5e6800b74a..70260c8357 100644 --- a/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx +++ b/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx @@ -30,6 +30,7 @@ import { Link45deg, PauseCircle, Trash, + Activity, } from "react-bootstrap-icons"; import { Link, generatePath, useNavigate, useParams } from "react-router"; import { @@ -69,6 +70,7 @@ import SessionPaused from "./SessionPaused"; import SessionUnavailable from "./SessionUnavailable"; import styles from "../../session/components/ShowSession.module.scss"; +import { PrometheusQueryBox } from "../../../components/prometheusModal/prometheusModal"; export default function ShowSessionPage() { const dispatch = useAppDispatch(); @@ -113,7 +115,9 @@ export default function ShowSessionPage() { const toggleModalLogs = useCallback(() => { dispatch( - displaySlice.actions.toggleSessionLogsModal({ targetServer: sessionName }) + displaySlice.actions.toggleSessionLogsModal({ + targetServer: sessionName, + }) ); }, [dispatch, sessionName]); @@ -123,6 +127,12 @@ export default function ShowSessionPage() { () => setShowModalPauseOrDeleteSession((show) => !show), [] ); + + const [showPrometheusQuery, setShowPrometheusQuery] = useState(false); + const togglePrometheusQuery = useCallback( + () => setShowPrometheusQuery((show) => !show), + [] + ); const [pauseOrDeleteAction, setPauseOrDeleteAction] = useState< "pause" | "delete" >("pause"); @@ -218,6 +228,7 @@ export default function ShowSessionPage() { > {backButton} + + {showPrometheusQuery && ( +
+ +
+ )} {content} @@ -293,6 +343,37 @@ function LogsBtn({ toggle }: LogsBtnProps) { ); } +interface PrometheusBtnProps { + toggle: () => void; +} +function PrometheusBtn({ toggle }: PrometheusBtnProps) { + const ref = useRef(null); + + return ( +
+ + + Toggle metrics + +
+ ); +} + interface PauseSessionBtnProps { openPauseSession: () => void; } diff --git a/server/src/config.ts b/server/src/config.ts index 9e03087eec..e1664d7f6e 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -92,6 +92,9 @@ const PROMETHEUS = { (process.env.PROMETHEUS_ENABLED ?? "").toLowerCase() ), path: "/metrics", + url: + process.env.PROMETHEUS_URL || + "http://prometheus-server.monitoring.svc.cluster.local", }; const config = { diff --git a/server/src/websocket/handlers/prometheus.ts b/server/src/websocket/handlers/prometheus.ts new file mode 100644 index 0000000000..eb0ef9c177 --- /dev/null +++ b/server/src/websocket/handlers/prometheus.ts @@ -0,0 +1,119 @@ +/*! + * Copyright 2025 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fetch from "cross-fetch"; +import ws from "ws"; + +import config from "../../config"; +import logger from "../../logger"; +import { WsMessage } from "../WsMessages"; +import type { Channel } from "./handlers.types"; + +export function handlerPrometheusQuery( + data: Record, + channel: Channel, + socket: ws +): void { + const { query, requestId } = data; + + if (!config.prometheus.url) { + const errorMessage = new WsMessage( + { + error: "Prometheus server not configured", + requestId, + }, + "user", + "prometheusQuery" + ).toString(); + socket.send(errorMessage); + return; + } + + if (!query || typeof query !== "string") { + const errorMessage = new WsMessage( + { + error: "Missing required 'query' parameter", + requestId, + }, + "user", + "prometheusQuery" + ).toString(); + socket.send(errorMessage); + return; + } + + executePrometheusQuery(query as string, requestId as string, socket); +} + +async function executePrometheusQuery( + query: string, + requestId: string, + socket: ws +): Promise { + try { + const prometheusUrl = new URL("/api/v1/query", config.prometheus.url); + prometheusUrl.searchParams.set("query", query); + + const prometheusResponse = await fetch(prometheusUrl.toString(), { + method: "GET", + headers: { + Accept: "application/json", + }, + }); + + if (!prometheusResponse.ok) { + logger.error( + `Prometheus query failed with status ${prometheusResponse.status}` + ); + const errorMessage = new WsMessage( + { + error: `Prometheus server error: ${prometheusResponse.statusText}`, + requestId, + }, + "user", + "prometheusQuery" + ).toString(); + socket.send(errorMessage); + return; + } + + const responseData = await prometheusResponse.json(); + const successMessage = new WsMessage( + { + ...responseData, + requestId, + }, + "user", + "prometheusQuery" + ).toString(); + socket.send(successMessage); + } catch (error) { + logger.error("Error executing Prometheus query:", error); + + const failureMessage = new WsMessage( + { + error: "Error executing Prometheus query", + details: error.message, + requestId, + }, + "user", + "prometheusQuery" + ).toString(); + socket.send(failureMessage); + } +} diff --git a/server/src/websocket/index.ts b/server/src/websocket/index.ts index ec905afc48..0544de92d7 100644 --- a/server/src/websocket/index.ts +++ b/server/src/websocket/index.ts @@ -44,6 +44,7 @@ import { handlerRequestSessionStatusV2, heartbeatRequestSessionStatusV2, } from "./handlers/sessionsV2"; +import { handlerPrometheusQuery } from "./handlers/prometheus"; // *** Channels *** // No need to store data in Redis since it's used only locally. We can modify this if necessary. @@ -87,6 +88,13 @@ const acceptedMessages: Record> = { handler: handlerRequestSessionStatusV2, } as MessageData, ], + prometheusQuery: [ + { + required: ["query", "requestId"], + optional: null, + handler: handlerPrometheusQuery, + } as MessageData, + ], ping: [ { required: null, From 8ae981a7b3cb58a62d1ff37ae463cd4c2e726570 Mon Sep 17 00:00:00 2001 From: Wes Johnson Date: Fri, 12 Sep 2025 15:38:59 +0200 Subject: [PATCH 03/35] fix(client): linting warnings and error --- client/.eslintrc.json | 2 ++ client/src/components/prometheusModal/prometheusModal.tsx | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/client/.eslintrc.json b/client/.eslintrc.json index da1dce6e9b..337d4c445e 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.json @@ -89,6 +89,7 @@ "skipWords": [ "accessor", "allowfullscreen", + "amalthea", "amazonaws", "apiversion", "ascii", @@ -220,6 +221,7 @@ "presentational", "profiler", "progressbar", + "prometheus", "proxying", "Pupikofer", "pygments", diff --git a/client/src/components/prometheusModal/prometheusModal.tsx b/client/src/components/prometheusModal/prometheusModal.tsx index dc2e70c36a..ee715ab05c 100644 --- a/client/src/components/prometheusModal/prometheusModal.tsx +++ b/client/src/components/prometheusModal/prometheusModal.tsx @@ -20,7 +20,7 @@ import cx from "classnames"; import { useCallback, useState, useEffect, useRef } from "react"; import { Activity, Search, Cpu, Memory } from "react-bootstrap-icons"; -import { Alert, Button, Card, CardBody, Input, InputGroup } from "reactstrap"; +import { Alert, Button, Card, CardBody } from "reactstrap"; interface PrometheusQueryResult { status: string; @@ -45,7 +45,7 @@ function usePrometheusWebSocket() { string, { resolve: (result: PrometheusQueryResult) => void; - reject: (error: any) => void; + reject: (error: Error) => void; } > >(new Map()); @@ -178,10 +178,6 @@ export function PrometheusQueryBox({ [sendPrometheusQuery] ); - const handleSubmit = useCallback(() => { - executeQuery(inputValue); - }, [inputValue, executeQuery]); - const handlePredefinedQuery = useCallback( (predefinedQuery: string) => { setInputValue(predefinedQuery); From 782f718a1cf72ebb10d75f1799312f48b1b626ee Mon Sep 17 00:00:00 2001 From: Wes Johnson Date: Fri, 12 Sep 2025 15:45:56 +0200 Subject: [PATCH 04/35] fix(client): typescript errors --- client/src/components/prometheusModal/prometheusModal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/components/prometheusModal/prometheusModal.tsx b/client/src/components/prometheusModal/prometheusModal.tsx index ee715ab05c..45ed5ab960 100644 --- a/client/src/components/prometheusModal/prometheusModal.tsx +++ b/client/src/components/prometheusModal/prometheusModal.tsx @@ -200,7 +200,7 @@ export function PrometheusQueryBox({ */ } - const hasResults = queryResult?.data?.result?.length > 0; + const hasResults = queryResult?.data?.result?.length ? queryResult.data.result.length > 0 : false; return ( @@ -211,10 +211,10 @@ export function PrometheusQueryBox({ Prometheus Query - {predefinedQueries.length > 0 && ( + {predefinedQueries && predefinedQueries.length > 0 && (
- {predefinedQueries.map((pq, index) => ( + {predefinedQueries?.map((pq, index) => ( - ))} -
-
- )} - - {/* - - setInputValue(e.target.value)} - onKeyPress={handleKeyPress} - /> - - - */} {!isConnected && ( @@ -288,38 +247,29 @@ export function PrometheusQueryBox({ )} - {hasResults && ( -
-
- {queryResult?.data?.result?.map((result, index) => ( -
-
- {result.value - ? `${result.value[1]}` - : result.values - ? `${result.values.length} time series points` - : "No value"} - {predefinedQueries && - predefinedQueries.length > 0 && - predefinedQueries.map((pq) => { - if ( - pq.query === inputValue.trim() && - pq.unit && - result.value - ) { - return ` ${pq.unit}`; - } - return ""; - })} -
-
- ))} + {queryResults.map((qr, idx) => ( +
+
+ {qr.predefinedQuery?.label || `Result ${qr.requestId}`} + {qr.predefinedQuery?.unit && ` (${qr.predefinedQuery.unit})`} +
+
+
+ {qr.data.result[0]?.value + ? `${qr.data.result[0].value[1]}${ + qr.predefinedQuery?.unit + ? ` ${qr.predefinedQuery.unit}` + : "" + }` + : qr.data.result[0]?.values + ? `${qr.data.result[0].values.length} time series points` + : qr.data.result.length === 0 + ? "No data" + : "No value"} +
- )} + ))} ); diff --git a/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx b/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx index 3543e6ed2c..cda86e751c 100644 --- a/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx +++ b/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx @@ -263,39 +263,48 @@ export default function ShowSessionPage() { className={cx(styles.fullscreenContent, "w-100")} data-cy="session-page" > - {showPrometheusQuery && ( -
- -
- )} +
+ 80`, + description: "CPU usage percentage for this session", + icon: "cpu", + unit: "%", + alertThreshold: 0, + }, + { + label: "Memory Usage", + query: `round((container_memory_usage_bytes{pod=~"${sessionName}.*",container="amalthea-session"} / container_spec_memory_limit_bytes{pod=~"${sessionName}.*",container="amalthea-session"}) * 100, 0.01) > 80`, + description: "Memory usage percentage for this session", + icon: "memory", + unit: "%", + alertThreshold: 0, + }, + { + label: "Disk Usage %", + query: `round((kubelet_volume_stats_used_bytes{persistentvolumeclaim="${sessionName}"} / kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${sessionName}"}) * 100, 0.01) > 80`, + description: "Disk usage percentage for this session", + icon: "memory", + unit: "%", + alertThreshold: 0, + }, + { + label: "OOMKilled", + query: `sum by (namespace, pod, container) (rate(kube_pod_container_status_restarts_total{pod="${sessionName}.*"}[60m])) * on(namespace, pod, container) group_left(reason) kube_pod_container_status_last_terminated_reason{reason="OOMKilled", pod="${sessionName}.*"} > 0`, + description: "Disk usage percentage for this session", + icon: "memory", + unit: "%", + alertThreshold: 0, + }, + ]} + onClose={togglePrometheusQuery} + /> +
{content}
From 2fd7dd53c990e45201a331b06ad96fb4871598da Mon Sep 17 00:00:00 2001 From: Wes Johnson Date: Fri, 19 Sep 2025 17:19:55 +0200 Subject: [PATCH 13/35] only show modal when alerts are active --- .../prometheusModal/prometheusModal.tsx | 60 +++++-------------- .../SessionShowPage/ShowSessionPage.tsx | 6 +- 2 files changed, 17 insertions(+), 49 deletions(-) diff --git a/client/src/components/prometheusModal/prometheusModal.tsx b/client/src/components/prometheusModal/prometheusModal.tsx index c295e4229b..3e3af73f81 100644 --- a/client/src/components/prometheusModal/prometheusModal.tsx +++ b/client/src/components/prometheusModal/prometheusModal.tsx @@ -1,5 +1,5 @@ /*! - * Copyright 2024 - Swiss Data Science Center (SDSC) + * Copyright 2025 - Swiss Data Science Center (SDSC) * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and * Eidgenössische Technische Hochschule Zürich (ETHZ). * @@ -20,7 +20,7 @@ import cx from "classnames"; import { useCallback, useState, useEffect, useRef } from "react"; import { Activity } from "react-bootstrap-icons"; -import { Alert, Card, CardBody, CloseButton } from "reactstrap"; +import { Card, CardBody, CloseButton } from "reactstrap"; interface PrometheusQueryResult { status: string; @@ -43,8 +43,8 @@ interface PrometheusQueryBoxProps { query: string; description?: string; icon?: string; - unit?: string; - alertThreshold?: number; + unit: string; + alertThreshold: number; }>; onClose: () => void; } @@ -200,10 +200,6 @@ export function PrometheusQueryBox({ return () => clearInterval(interval); }, [executeQuery]); - const hasResults = queryResult?.data?.result?.length - ? queryResult.data.result.length > 0 - : false; - if (queryResults.length === 0) { return null; } @@ -223,48 +219,20 @@ export function PrometheusQueryBox({ /> - {!isConnected && ( - - WebSocket not connected - - )} - - {isLoading && ( -
- Querying Prometheus... -
- )} - - {error && ( - - Could not query Prometheus: {error} - - )} - - {queryResult && !hasResults && ( - - No data returned from Prometheus - - )} - {queryResults.map((qr, idx) => (
-
- {qr.predefinedQuery?.label || `Result ${qr.requestId}`} - {qr.predefinedQuery?.unit && ` (${qr.predefinedQuery.unit})`} -
+
{qr.predefinedQuery.label}
-
+
+ qr.predefinedQuery.alertThreshold + ? "text-danger" + : "text-warning" + } + > {qr.data.result[0]?.value - ? `${qr.data.result[0].value[1]}${ - qr.predefinedQuery?.unit - ? ` ${qr.predefinedQuery.unit}` - : "" - }` - : qr.data.result[0]?.values - ? `${qr.data.result[0].values.length} time series points` - : qr.data.result.length === 0 - ? "No data" + ? `${qr.data.result[0].value[1]}${qr.predefinedQuery?.unit}` : "No value"}
diff --git a/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx b/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx index cda86e751c..efc7a5f032 100644 --- a/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx +++ b/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx @@ -275,7 +275,7 @@ export default function ShowSessionPage() { description: "CPU usage percentage for this session", icon: "cpu", unit: "%", - alertThreshold: 0, + alertThreshold: 90, }, { label: "Memory Usage", @@ -283,7 +283,7 @@ export default function ShowSessionPage() { description: "Memory usage percentage for this session", icon: "memory", unit: "%", - alertThreshold: 0, + alertThreshold: 90, }, { label: "Disk Usage %", @@ -291,7 +291,7 @@ export default function ShowSessionPage() { description: "Disk usage percentage for this session", icon: "memory", unit: "%", - alertThreshold: 0, + alertThreshold: 90, }, { label: "OOMKilled", From ca75137e6690fe3249315d2405edaa68038975de Mon Sep 17 00:00:00 2001 From: Wes Johnson Date: Mon, 22 Sep 2025 16:57:19 +0200 Subject: [PATCH 14/35] change metrics toggle button colour when alerts fire --- .../prometheusModal/prometheusModal.tsx | 42 +++++++++---------- .../SessionShowPage/ShowSessionPage.tsx | 19 ++++++--- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/client/src/components/prometheusModal/prometheusModal.tsx b/client/src/components/prometheusModal/prometheusModal.tsx index 3e3af73f81..5f1ff93686 100644 --- a/client/src/components/prometheusModal/prometheusModal.tsx +++ b/client/src/components/prometheusModal/prometheusModal.tsx @@ -47,6 +47,8 @@ interface PrometheusQueryBoxProps { alertThreshold: number; }>; onClose: () => void; + setPrometheusQueryBtnColor: (color: string) => void; + showPrometheusQuery: boolean; } function usePrometheusWebSocket() { @@ -141,66 +143,64 @@ export function PrometheusQueryBox({ className, predefinedQueries, onClose, + setPrometheusQueryBtnColor, + showPrometheusQuery, }: PrometheusQueryBoxProps) { - const [queryResult, setQueryResult] = useState( - null - ); const [queryResults, setQueryResults] = useState([]); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const { sendPrometheusQuery, isConnected } = usePrometheusWebSocket(); + const { sendPrometheusQuery } = usePrometheusWebSocket(); const executeQuery = useCallback( async (predefinedQuery: Array) => { if (!predefinedQuery.query.trim()) return; - setIsLoading(true); - setError(null); - setQueryResult(null); - try { const result = await sendPrometheusQuery(predefinedQuery.query.trim()); return result; } catch (err) { - setError(err instanceof Error ? err.message : "Unknown error"); - } finally { - setIsLoading(false); + return null; } }, [sendPrometheusQuery] ); const getAllQueryResults = useCallback(async () => { - setQueryResults([]); const filteredResults = []; + let newColor = "text-dark"; for (const pq of predefinedQueries || []) { const result = await executeQuery(pq); if (result?.data?.result?.length > 0) { filteredResults.push({ ...result, predefinedQuery: pq }); + if ( + result.data.result[0]?.value[1] > pq.alertThreshold && + newColor !== "text-danger" + ) { + newColor = "text-danger"; + } else { + if (newColor !== "text-danger") { + newColor = "text-warning"; + } + } } } + setPrometheusQueryBtnColor(newColor); setQueryResults(filteredResults); - }, [executeQuery, predefinedQueries]); + }, [executeQuery, predefinedQueries, setPrometheusQueryBtnColor]); const handleCloseButton = useCallback(() => { - setQueryResult(null); - setError(null); - setIsLoading(false); onClose(); }, [onClose]); useEffect(() => { const interval = setInterval(() => { - console.log("Re-executing query"); getAllQueryResults(); }, 15000); return () => clearInterval(interval); - }, [executeQuery]); + }, [getAllQueryResults]); - if (queryResults.length === 0) { + if (queryResults.length === 0 || showPrometheusQuery === false) { return null; } diff --git a/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx b/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx index efc7a5f032..a0226ecf19 100644 --- a/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx +++ b/client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx @@ -20,6 +20,7 @@ import { skipToken } from "@reduxjs/toolkit/query"; import cx from "classnames"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { + Activity, ArrowLeft, Box, Briefcase, @@ -30,7 +31,6 @@ import { Link45deg, PauseCircle, Trash, - Activity, } from "react-bootstrap-icons"; import { Link, generatePath, useNavigate, useParams } from "react-router"; import { @@ -128,7 +128,9 @@ export default function ShowSessionPage() { [] ); - const [showPrometheusQuery, setShowPrometheusQuery] = useState(false); + const [showPrometheusQuery, setShowPrometheusQuery] = useState(true); + const [prometheusQueryBtnColor, setPrometheusQueryBtnColor] = + useState("text-dark"); const togglePrometheusQuery = useCallback( () => setShowPrometheusQuery((show) => !show), [] @@ -228,7 +230,6 @@ export default function ShowSessionPage() { > {backButton} - +
{content} @@ -348,20 +355,22 @@ function LogsBtn({ toggle }: LogsBtnProps) { interface PrometheusBtnProps { toggle: () => void; + color: string; } -function PrometheusBtn({ toggle }: PrometheusBtnProps) { +function PrometheusBtn({ toggle, color }: PrometheusBtnProps) { const ref = useRef(null); return (