Skip to content

Commit 57fdd41

Browse files
committed
feat: add session alerts
1 parent e662f56 commit 57fdd41

File tree

3 files changed

+154
-3
lines changed

3 files changed

+154
-3
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*!
2+
* Copyright 2025 - Swiss Data Science Center (SDSC)
3+
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
4+
* Eidgenössische Technische Hochschule Zürich (ETHZ).
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import cx from "classnames";
20+
import { useRef } from "react";
21+
import { ExclamationTriangleFill } from "react-bootstrap-icons";
22+
import ReactMarkdown from "react-markdown";
23+
import {
24+
Badge,
25+
Button,
26+
PopoverBody,
27+
PopoverHeader,
28+
UncontrolledPopover,
29+
} from "reactstrap";
30+
import { skipToken } from "@reduxjs/toolkit/query";
31+
import { useGetAlertsQuery, type Alert } from "../api/sessionsV2.api";
32+
33+
interface SessionAlertsProps {
34+
sessionName?: string;
35+
inline?: boolean;
36+
}
37+
38+
const POLL_INTERVAL = 12000;
39+
40+
export default function SessionAlerts({
41+
sessionName,
42+
inline = false,
43+
}: SessionAlertsProps) {
44+
const { data: alerts } = useGetAlertsQuery(
45+
sessionName ? { sessionName } : skipToken,
46+
{
47+
pollingInterval: POLL_INTERVAL,
48+
refetchOnMountOrArgChange: true,
49+
}
50+
);
51+
52+
if (!alerts || alerts.length === 0) {
53+
return null;
54+
}
55+
56+
return <Alerts alerts={alerts} />;
57+
}
58+
59+
interface AlertsProps {
60+
alerts: Alert[];
61+
}
62+
63+
function Alerts({ alerts }: AlertsProps) {
64+
const ref = useRef<HTMLButtonElement>(null);
65+
66+
return (
67+
<>
68+
<div className="position-relative">
69+
<Button
70+
innerRef={ref}
71+
className={cx(
72+
"bg-danger",
73+
"border-0",
74+
"no-focus",
75+
"rounded",
76+
"shadow-none",
77+
"text-white"
78+
)}
79+
style={{ padding: "0.25rem 0.5rem" }}
80+
data-cy="session-alerts"
81+
>
82+
<ExclamationTriangleFill className="bi" />
83+
</Button>
84+
{alerts.length > 1 && (
85+
<Badge
86+
color="dark"
87+
pill
88+
className="position-absolute"
89+
style={{
90+
fontSize: "0.65rem",
91+
top: "-6px",
92+
right: "-8px",
93+
minWidth: "20px",
94+
}}
95+
>
96+
{alerts.length}
97+
</Badge>
98+
)}
99+
</div>
100+
<UncontrolledPopover
101+
target={ref}
102+
trigger="click"
103+
placement="auto"
104+
popperClassName="session-alerts-popover"
105+
>
106+
{alerts.map((alert, index) => (
107+
<div key={alert.id}>
108+
<PopoverHeader className="text-bg-danger">
109+
{alert.title}
110+
</PopoverHeader>
111+
<PopoverBody className={cx("text-dark", "bg-danger-subtle")}>
112+
<ReactMarkdown>{alert.message}</ReactMarkdown>
113+
</PopoverBody>
114+
</div>
115+
))}
116+
</UncontrolledPopover>
117+
</>
118+
);
119+
}
120+

client/src/features/sessionsV2/SessionShowPage/ShowSessionPage.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,15 @@ import {
5959
useGetProjectsByProjectIdSessionLaunchersQuery as useGetProjectSessionLaunchersQuery,
6060
type SessionLauncher,
6161
} from "../api/sessionLaunchersV2.api";
62-
import { useGetSessionsQuery } from "../api/sessionsV2.api";
62+
import { useGetSessionsQuery, useGetAlertsQuery } from "../api/sessionsV2.api";
6363
import PauseOrDeleteSessionModal from "../PauseOrDeleteSessionModal";
6464
import { getSessionFavicon } from "../session.utils";
6565
import { SessionV2 } from "../sessionsV2.types";
6666
import SessionLaunchLinkModal from "../SessionView/SessionLaunchLinkModal";
6767
import SessionIframe from "./SessionIframe";
6868
import SessionPaused from "./SessionPaused";
6969
import SessionUnavailable from "./SessionUnavailable";
70+
import SessionAlerts from "./SessionAlerts";
7071

7172
import styles from "../../session/components/ShowSession.module.scss";
7273

@@ -99,6 +100,11 @@ export default function ShowSessionPage() {
99100
return sessions.find(({ name }) => name === sessionName);
100101
}, [sessionName, sessions]);
101102

103+
const { data: alerts } = useGetAlertsQuery(
104+
sessionName ? { sessionName } : skipToken
105+
);
106+
const hasAlerts = alerts && alerts.length > 0;
107+
102108
useEffect(() => {
103109
const faviconByStatus = getSessionFavicon(
104110
thisSession?.status?.state,
@@ -225,6 +231,7 @@ export default function ShowSessionPage() {
225231
namespace={namespace}
226232
slug={slug}
227233
/>
234+
<SessionAlerts sessionName={sessionName} inline />
228235
</div>
229236
<div
230237
className={cx(
@@ -243,7 +250,7 @@ export default function ShowSessionPage() {
243250
slug={slug}
244251
/>
245252
</div>
246-
<div className={cx("pe-3", "text-white")}>
253+
<div className={cx("pe-3", hasAlerts ? "text-warning" : "text-white")}>
247254
<RenkuFrogIcon size={24} />
248255
</div>
249256
</div>

client/src/features/sessionsV2/api/sessionsV2.api.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@
1818

1919
import { sessionsV2GeneratedApi } from "./sessionsV2.generated-api";
2020

21+
// Alert types (manually added - not in generated API yet)
22+
export interface Alert {
23+
id: string;
24+
title: string;
25+
message: string;
26+
event_type: string;
27+
user_id: string;
28+
session_name?: string;
29+
creation_date: string;
30+
resolved_at?: string;
31+
}
32+
33+
export type AlertList = Alert[];
34+
2135
// Adds tag handling for cache management
2236
const withTagHandling = sessionsV2GeneratedApi.enhanceEndpoints({
2337
addTagTypes: ["Session"],
@@ -61,13 +75,21 @@ const withTagHandling = sessionsV2GeneratedApi.enhanceEndpoints({
6175
},
6276
});
6377

64-
// Adds tag invalidation endpoints
78+
// Adds tag invalidation endpoints and alerts
6579
export const sessionsV2Api = withTagHandling.injectEndpoints({
6680
endpoints: (build) => ({
6781
invalidateSessions: build.mutation<null, void>({
6882
queryFn: () => ({ data: null }),
6983
invalidatesTags: ["Session"],
7084
}),
85+
getAlerts: build.query<AlertList, { sessionName?: string }>({
86+
query: (queryArg) => ({
87+
url: `/alerts`,
88+
params: queryArg.sessionName
89+
? { session_name: queryArg.sessionName }
90+
: {},
91+
}),
92+
}),
7193
}),
7294
});
7395

@@ -80,6 +102,8 @@ export const {
80102
useDeleteSessionsBySessionIdMutation,
81103
useGetSessionsBySessionIdLogsQuery,
82104
useGetSessionsImagesQuery,
105+
// "alerts" hooks
106+
useGetAlertsQuery,
83107
} = sessionsV2Api;
84108

85109
export type * from "./sessionsV2.generated-api";

0 commit comments

Comments
 (0)