Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": "go.mod|go.sum|^.secrets.baseline$",
"lines": null
},
"generated_at": "2025-11-27T10:58:34Z",
"generated_at": "2026-04-17T11:13:23Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -340,7 +340,7 @@
"hashed_secret": "6947818ac409551f11fbaa78f0ea6391960aa5b8",
"is_secret": false,
"is_verified": false,
"line_number": 115,
"line_number": 113,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down
12 changes: 11 additions & 1 deletion web/src/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import Events from "./Events";
import Keys from "./Keys";
import { Theme } from "@carbon/react";
import Feedbacks from "./Feedbacks";
import MaintenanceNotification from "./MaintenanceNotification";
import MaintenanceManager from "./MaintenanceManager";

const RouterClass = React.memo(({ isAdmin }) => {
return (
Expand Down Expand Up @@ -84,6 +86,12 @@ const RouterClass = React.memo(({ isAdmin }) => {
element={<TnCRoute Component={Feedbacks} />}
/>
)}
{isAdmin && (
<Route
path="/maintenance"
element={<TnCRoute Component={MaintenanceManager} />}
/>
)}
</Routes>
);
});
Expand Down Expand Up @@ -111,14 +119,16 @@ const App = () => {
"/events",
"/keys",
"/feedbacks",
"/maintenance",
].includes(window.location.pathname)
) {
window.location.href = "/login";
return;
}
return (
<React.Fragment>
<Theme theme="g90">{auth === true && <HeaderNav onSideNavToggle={handleSideNavToggle} />} </Theme>
<Theme theme="g90">{auth && <HeaderNav onSideNavToggle={handleSideNavToggle} />} </Theme>
{auth && <MaintenanceNotification />}
<section className={auth ? `contentSection ${isAdmin && isSideNavExpanded ? 'sideNavExpanded' : ''}` : ""}>
<RouterClass isAdmin={isAdmin} />
</section>
Expand Down
1 change: 1 addition & 0 deletions web/src/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ const HeaderNav = ({ onSideNavToggle }) => {
{isAdmin && <MenuLink url="/keys" label="Keys" />}
{isAdmin && <MenuLink url="/users" label="Users" />}
{isAdmin && <MenuLink url="/events" label="Events" />}
{isAdmin && <MenuLink url="/maintenance" label="Maintenance" />}
</SideNavItems>
</SideNav>
</div>
Expand Down
234 changes: 234 additions & 0 deletions web/src/components/MaintenanceConfig.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import React, { useState, useEffect } from "react";
import {
Toggle,
TextArea,
Button,
InlineNotification,
Loading,
Form,
Stack,
} from "@carbon/react";
import { getMaintenanceStatus, updateMaintenanceConfig } from "../services/request";
import "../styles/maintenance-config.scss";

const MaintenanceConfig = () => {
const [enabled, setEnabled] = useState(false);
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [notification, setNotification] = useState(null);

// Helper function to format UTC date for datetime-local input
const formatDateForInput = (isoString) => {
if (!isoString) return "";
// datetime-local expects format: YYYY-MM-DDTHH:mm
// We keep it in UTC by using the ISO string directly
return isoString.slice(0, 16);
};

// Helper function to convert datetime-local value to UTC ISO string
const convertToUTC = (dateTimeLocal) => {
if (!dateTimeLocal) return null;
// Treat the input as UTC time
return new Date(dateTimeLocal + ':00Z').toISOString();
};

// Fetch current configuration
useEffect(() => {
fetchConfig();
}, []);

const fetchConfig = async () => {
setLoading(true);
try {
const response = await getMaintenanceStatus();
if (response.type === "GET_MAINTENANCE_STATUS" && response.payload) {
const config = response.payload;
setEnabled(config.enabled || false);
setStartDate(config.start_time ? formatDateForInput(config.start_time) : "");
setEndDate(config.end_time ? formatDateForInput(config.end_time) : "");
setMessage(config.message || "");
}
} catch (error) {
console.error("Error fetching maintenance config:", error);
showNotification("error", "Failed to load maintenance configuration");
} finally {
setLoading(false);
}
};

const showNotification = (kind, subtitle) => {
setNotification({ kind, subtitle });
setTimeout(() => setNotification(null), 5000);
};

const handleSave = async () => {
// Validation
if (enabled) {
if (!startDate || !endDate) {
showNotification("error", "Start time and end time are required when maintenance is enabled");
return;
}
if (new Date(convertToUTC(endDate)) <= new Date(convertToUTC(startDate))) {
showNotification("error", "End time must be after start time");
return;
}
if (!message.trim()) {
showNotification("error", "Message is required when maintenance is enabled");
return;
}
}

setSaving(true);
try {
const payload = {
enabled,
start_time: enabled ? convertToUTC(startDate) : null,
end_time: enabled ? convertToUTC(endDate) : null,
message: enabled ? message.trim() : "",
};

console.log("Sending payload:", payload);

const response = await updateMaintenanceConfig(payload);

if (response.type === "UPDATE_MAINTENANCE_CONFIG") {
const successMsg = enabled
? "Successfully updated maintenance notification"
: "Successfully disabled maintenance notification";
showNotification("success", successMsg);
// Refresh the config to show updated values
await fetchConfig();
} else if (response.type === "API_ERROR") {
const errorMsg = response.payload?.response?.data?.error || response.payload?.message || "Failed to update maintenance configuration";
showNotification("error", errorMsg);
} else {
showNotification("error", "Failed to update maintenance configuration");
}
} catch (error) {
console.error("Error updating maintenance config:", error);
const errorMsg = error.response?.data?.error || error.message || "Failed to update maintenance configuration";
showNotification("error", errorMsg);
} finally {
setSaving(false);
}
};

if (loading) {
return (
<div className="maintenance-config-loading">
<Loading description="Loading maintenance configuration..." withOverlay={false} />
</div>
);
}

return (
<div className="maintenance-config-container">
<h2>Maintenance Notification Configuration</h2>
<p className="maintenance-config-description">
Configure maintenance notifications that will be displayed to all users.
Notifications will appear 24 hours before the maintenance start time and remain visible until the end time.
<strong> All times are in UTC timezone.</strong>
</p>

{notification && (
<InlineNotification
kind={notification.kind}
title={notification.kind === "success" ? "Success" : "Error"}
subtitle={notification.subtitle}
onCloseButtonClick={() => setNotification(null)}
lowContrast
/>
)}

<Form className="maintenance-config-form">
<Stack gap={6}>
<Toggle
id="maintenance-enabled"
labelText="Enable Maintenance Notification"
labelA="Disabled"
labelB="Enabled"
toggled={enabled}
onToggle={(checked) => setEnabled(checked)}
/>

{enabled && (
<>
<div className="date-picker-group">
<label htmlFor="start-date" className="cds--label">
Maintenance Start Time (UTC)
</label>
<input
id="start-date"
type="datetime-local"
className="cds--text-input datetime-input"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
onBlur={(e) => setStartDate(e.target.value)}
step="60"
min={new Date().toISOString().slice(0, 16)}
/>
<div className="cds--form__helper-text">
Format: YYYY-MM-DDTHH:mm (e.g., {new Date().getFullYear()}-04-15T10:00) - UTC timezone
</div>
</div>

<div className="date-picker-group">
<label htmlFor="end-date" className="cds--label">
Maintenance End Time (UTC)
</label>
<input
id="end-date"
type="datetime-local"
className="cds--text-input datetime-input"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
onBlur={(e) => setEndDate(e.target.value)}
step="60"
min={new Date().toISOString().slice(0, 16)}
/>
<div className="cds--form__helper-text">
Format: YYYY-MM-DDTHH:mm (e.g., {new Date().getFullYear()}-04-15T14:00) - UTC timezone
</div>
</div>

<TextArea
id="maintenance-message"
labelText="Notification Message"
placeholder="Enter the maintenance notification message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={4}
helperText="This message will be displayed to all users during the maintenance window"
/>
</>
)}

<div className="button-group">
<Button
kind="primary"
onClick={handleSave}
disabled={saving}
>
{saving ? "Saving..." : "Save Configuration"}
</Button>
{enabled && (
<Button
kind="secondary"
onClick={fetchConfig}
disabled={saving}
>
Reset
</Button>
)}
</div>
</Stack>
</Form>
</div>
);
};

export default MaintenanceConfig;

Loading
Loading