Skip to content

Commit f2c5d54

Browse files
committed
Add maintenance notification UI implementation
Signed-off-by: Vikas <vikas.satyanarayana.bolla@ibm.com>
1 parent 8a80bf5 commit f2c5d54

10 files changed

Lines changed: 1096 additions & 3 deletions

.secrets.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "go.mod|go.sum|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2025-11-27T10:58:34Z",
6+
"generated_at": "2026-04-17T11:13:23Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -340,7 +340,7 @@
340340
"hashed_secret": "6947818ac409551f11fbaa78f0ea6391960aa5b8",
341341
"is_secret": false,
342342
"is_verified": false,
343-
"line_number": 115,
343+
"line_number": 113,
344344
"type": "Secret Keyword",
345345
"verified_result": null
346346
}

web/src/components/App.jsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import Events from "./Events";
2020
import Keys from "./Keys";
2121
import { Theme } from "@carbon/react";
2222
import Feedbacks from "./Feedbacks";
23+
import MaintenanceNotification from "./MaintenanceNotification";
24+
import MaintenanceManager from "./MaintenanceManager";
2325

2426
const RouterClass = React.memo(({ isAdmin }) => {
2527
return (
@@ -84,6 +86,12 @@ const RouterClass = React.memo(({ isAdmin }) => {
8486
element={<TnCRoute Component={Feedbacks} />}
8587
/>
8688
)}
89+
{isAdmin && (
90+
<Route
91+
path="/maintenance"
92+
element={<TnCRoute Component={MaintenanceManager} />}
93+
/>
94+
)}
8795
</Routes>
8896
);
8997
});
@@ -111,14 +119,16 @@ const App = () => {
111119
"/events",
112120
"/keys",
113121
"/feedbacks",
122+
"/maintenance",
114123
].includes(window.location.pathname)
115124
) {
116125
window.location.href = "/login";
117126
return;
118127
}
119128
return (
120129
<React.Fragment>
121-
<Theme theme="g90">{auth === true && <HeaderNav onSideNavToggle={handleSideNavToggle} />} </Theme>
130+
<Theme theme="g90">{auth && <HeaderNav onSideNavToggle={handleSideNavToggle} />} </Theme>
131+
{auth && <MaintenanceNotification />}
122132
<section className={auth ? `contentSection ${isAdmin && isSideNavExpanded ? 'sideNavExpanded' : ''}` : ""}>
123133
<RouterClass isAdmin={isAdmin} />
124134
</section>

web/src/components/Header.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ const HeaderNav = ({ onSideNavToggle }) => {
178178
{isAdmin && <MenuLink url="/keys" label="Keys" />}
179179
{isAdmin && <MenuLink url="/users" label="Users" />}
180180
{isAdmin && <MenuLink url="/events" label="Events" />}
181+
{isAdmin && <MenuLink url="/maintenance" label="Maintenance" />}
181182
</SideNavItems>
182183
</SideNav>
183184
</div>
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import React, { useState, useEffect } from "react";
2+
import {
3+
Toggle,
4+
TextArea,
5+
Button,
6+
InlineNotification,
7+
Loading,
8+
Form,
9+
Stack,
10+
} from "@carbon/react";
11+
import { getMaintenanceStatus, updateMaintenanceConfig } from "../services/request";
12+
import "../styles/maintenance-config.scss";
13+
14+
const MaintenanceConfig = () => {
15+
const [enabled, setEnabled] = useState(false);
16+
const [startDate, setStartDate] = useState("");
17+
const [endDate, setEndDate] = useState("");
18+
const [message, setMessage] = useState("");
19+
const [loading, setLoading] = useState(true);
20+
const [saving, setSaving] = useState(false);
21+
const [notification, setNotification] = useState(null);
22+
23+
// Helper function to format UTC date for datetime-local input
24+
const formatDateForInput = (isoString) => {
25+
if (!isoString) return "";
26+
// datetime-local expects format: YYYY-MM-DDTHH:mm
27+
// We keep it in UTC by using the ISO string directly
28+
return isoString.slice(0, 16);
29+
};
30+
31+
// Helper function to convert datetime-local value to UTC ISO string
32+
const convertToUTC = (dateTimeLocal) => {
33+
if (!dateTimeLocal) return null;
34+
// Treat the input as UTC time
35+
return new Date(dateTimeLocal + ':00Z').toISOString();
36+
};
37+
38+
// Fetch current configuration
39+
useEffect(() => {
40+
fetchConfig();
41+
}, []);
42+
43+
const fetchConfig = async () => {
44+
setLoading(true);
45+
try {
46+
const response = await getMaintenanceStatus();
47+
if (response.type === "GET_MAINTENANCE_STATUS" && response.payload) {
48+
const config = response.payload;
49+
setEnabled(config.enabled || false);
50+
setStartDate(config.start_time ? formatDateForInput(config.start_time) : "");
51+
setEndDate(config.end_time ? formatDateForInput(config.end_time) : "");
52+
setMessage(config.message || "");
53+
}
54+
} catch (error) {
55+
console.error("Error fetching maintenance config:", error);
56+
showNotification("error", "Failed to load maintenance configuration");
57+
} finally {
58+
setLoading(false);
59+
}
60+
};
61+
62+
const showNotification = (kind, subtitle) => {
63+
setNotification({ kind, subtitle });
64+
setTimeout(() => setNotification(null), 5000);
65+
};
66+
67+
const handleSave = async () => {
68+
// Validation
69+
if (enabled) {
70+
if (!startDate || !endDate) {
71+
showNotification("error", "Start time and end time are required when maintenance is enabled");
72+
return;
73+
}
74+
if (new Date(convertToUTC(endDate)) <= new Date(convertToUTC(startDate))) {
75+
showNotification("error", "End time must be after start time");
76+
return;
77+
}
78+
if (!message.trim()) {
79+
showNotification("error", "Message is required when maintenance is enabled");
80+
return;
81+
}
82+
}
83+
84+
setSaving(true);
85+
try {
86+
const payload = {
87+
enabled,
88+
start_time: enabled ? convertToUTC(startDate) : null,
89+
end_time: enabled ? convertToUTC(endDate) : null,
90+
message: enabled ? message.trim() : "",
91+
};
92+
93+
console.log("Sending payload:", payload);
94+
95+
const response = await updateMaintenanceConfig(payload);
96+
97+
if (response.type === "UPDATE_MAINTENANCE_CONFIG") {
98+
const successMsg = enabled
99+
? "Successfully updated maintenance notification"
100+
: "Successfully disabled maintenance notification";
101+
showNotification("success", successMsg);
102+
// Refresh the config to show updated values
103+
await fetchConfig();
104+
} else if (response.type === "API_ERROR") {
105+
const errorMsg = response.payload?.response?.data?.error || response.payload?.message || "Failed to update maintenance configuration";
106+
showNotification("error", errorMsg);
107+
} else {
108+
showNotification("error", "Failed to update maintenance configuration");
109+
}
110+
} catch (error) {
111+
console.error("Error updating maintenance config:", error);
112+
const errorMsg = error.response?.data?.error || error.message || "Failed to update maintenance configuration";
113+
showNotification("error", errorMsg);
114+
} finally {
115+
setSaving(false);
116+
}
117+
};
118+
119+
if (loading) {
120+
return (
121+
<div className="maintenance-config-loading">
122+
<Loading description="Loading maintenance configuration..." withOverlay={false} />
123+
</div>
124+
);
125+
}
126+
127+
return (
128+
<div className="maintenance-config-container">
129+
<h2>Maintenance Notification Configuration</h2>
130+
<p className="maintenance-config-description">
131+
Configure maintenance notifications that will be displayed to all users.
132+
Notifications will appear 24 hours before the maintenance start time and remain visible until the end time.
133+
<strong> All times are in UTC timezone.</strong>
134+
</p>
135+
136+
{notification && (
137+
<InlineNotification
138+
kind={notification.kind}
139+
title={notification.kind === "success" ? "Success" : "Error"}
140+
subtitle={notification.subtitle}
141+
onCloseButtonClick={() => setNotification(null)}
142+
lowContrast
143+
/>
144+
)}
145+
146+
<Form className="maintenance-config-form">
147+
<Stack gap={6}>
148+
<Toggle
149+
id="maintenance-enabled"
150+
labelText="Enable Maintenance Notification"
151+
labelA="Disabled"
152+
labelB="Enabled"
153+
toggled={enabled}
154+
onToggle={(checked) => setEnabled(checked)}
155+
/>
156+
157+
{enabled && (
158+
<>
159+
<div className="date-picker-group">
160+
<label htmlFor="start-date" className="cds--label">
161+
Maintenance Start Time (UTC)
162+
</label>
163+
<input
164+
id="start-date"
165+
type="datetime-local"
166+
className="cds--text-input datetime-input"
167+
value={startDate}
168+
onChange={(e) => setStartDate(e.target.value)}
169+
onBlur={(e) => setStartDate(e.target.value)}
170+
step="60"
171+
min={new Date().toISOString().slice(0, 16)}
172+
/>
173+
<div className="cds--form__helper-text">
174+
Format: YYYY-MM-DDTHH:mm (e.g., {new Date().getFullYear()}-04-15T10:00) - UTC timezone
175+
</div>
176+
</div>
177+
178+
<div className="date-picker-group">
179+
<label htmlFor="end-date" className="cds--label">
180+
Maintenance End Time (UTC)
181+
</label>
182+
<input
183+
id="end-date"
184+
type="datetime-local"
185+
className="cds--text-input datetime-input"
186+
value={endDate}
187+
onChange={(e) => setEndDate(e.target.value)}
188+
onBlur={(e) => setEndDate(e.target.value)}
189+
step="60"
190+
min={new Date().toISOString().slice(0, 16)}
191+
/>
192+
<div className="cds--form__helper-text">
193+
Format: YYYY-MM-DDTHH:mm (e.g., {new Date().getFullYear()}-04-15T14:00) - UTC timezone
194+
</div>
195+
</div>
196+
197+
<TextArea
198+
id="maintenance-message"
199+
labelText="Notification Message"
200+
placeholder="Enter the maintenance notification message..."
201+
value={message}
202+
onChange={(e) => setMessage(e.target.value)}
203+
rows={4}
204+
helperText="This message will be displayed to all users during the maintenance window"
205+
/>
206+
</>
207+
)}
208+
209+
<div className="button-group">
210+
<Button
211+
kind="primary"
212+
onClick={handleSave}
213+
disabled={saving}
214+
>
215+
{saving ? "Saving..." : "Save Configuration"}
216+
</Button>
217+
{enabled && (
218+
<Button
219+
kind="secondary"
220+
onClick={fetchConfig}
221+
disabled={saving}
222+
>
223+
Reset
224+
</Button>
225+
)}
226+
</div>
227+
</Stack>
228+
</Form>
229+
</div>
230+
);
231+
};
232+
233+
export default MaintenanceConfig;
234+

0 commit comments

Comments
 (0)