mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Add maintenance banner feature (#9981)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Graham Neubig <neubig@gmail.com> Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
parent
b75a61bce9
commit
03c8312f5f
@ -0,0 +1,52 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { MaintenanceBanner } from "#/components/features/maintenance/maintenance-banner";
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual<typeof import("react-i18next")>("react-i18next");
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { time?: string }) => {
|
||||
const translations: Record<string, string> = {
|
||||
"MAINTENANCE$SCHEDULED_MESSAGE": `Scheduled maintenance will begin at ${options?.time || "{{time}}"}`,
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("MaintenanceBanner", () => {
|
||||
it("renders maintenance banner with formatted time", () => {
|
||||
const startTime = "2024-01-15T10:00:00-05:00"; // EST timestamp
|
||||
|
||||
const { container } = render(<MaintenanceBanner startTime={startTime} />);
|
||||
|
||||
// Check if the banner is rendered
|
||||
expect(screen.getByText(/Scheduled maintenance will begin at/)).toBeInTheDocument();
|
||||
|
||||
// Check if the warning icon (SVG) is present
|
||||
const svgIcon = container.querySelector('svg');
|
||||
expect(svgIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles invalid date gracefully", () => {
|
||||
const invalidTime = "invalid-date";
|
||||
|
||||
render(<MaintenanceBanner startTime={invalidTime} />);
|
||||
|
||||
// Should still render the banner with the original string
|
||||
expect(screen.getByText(/Scheduled maintenance will begin at invalid-date/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("formats ISO date string correctly", () => {
|
||||
const isoTime = "2024-01-15T15:30:00.000Z";
|
||||
|
||||
render(<MaintenanceBanner startTime={isoTime} />);
|
||||
|
||||
// Should render the banner (exact time format will depend on user's timezone)
|
||||
expect(screen.getByText(/Scheduled maintenance will begin at/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -56,6 +56,9 @@ export interface GetConfigResponse {
|
||||
HIDE_LLM_SETTINGS: boolean;
|
||||
HIDE_MICROAGENT_MANAGEMENT?: boolean;
|
||||
};
|
||||
MAINTENANCE?: {
|
||||
startTime: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetVSCodeUrlResponse {
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaTriangleExclamation } from "react-icons/fa6";
|
||||
|
||||
interface MaintenanceBannerProps {
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
export function MaintenanceBanner({ startTime }: MaintenanceBannerProps) {
|
||||
const { t } = useTranslation();
|
||||
// Convert EST timestamp to user's local timezone
|
||||
const formatMaintenanceTime = (estTimeString: string): string => {
|
||||
try {
|
||||
// Parse the EST timestamp
|
||||
// If the string doesn't include timezone info, assume it's EST
|
||||
let dateToFormat: Date;
|
||||
|
||||
if (
|
||||
estTimeString.includes("T") &&
|
||||
(estTimeString.includes("-05:00") ||
|
||||
estTimeString.includes("-04:00") ||
|
||||
estTimeString.includes("EST") ||
|
||||
estTimeString.includes("EDT"))
|
||||
) {
|
||||
// Already has timezone info
|
||||
dateToFormat = new Date(estTimeString);
|
||||
} else {
|
||||
// Assume EST and convert to UTC for proper parsing
|
||||
// EST is UTC-5, EDT is UTC-4, but we'll assume EST for simplicity
|
||||
const estDate = new Date(estTimeString);
|
||||
if (Number.isNaN(estDate.getTime())) {
|
||||
throw new Error("Invalid date");
|
||||
}
|
||||
dateToFormat = estDate;
|
||||
}
|
||||
|
||||
// Format to user's local timezone
|
||||
return dateToFormat.toLocaleString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
});
|
||||
} catch (error) {
|
||||
// Fallback to original string if parsing fails
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Failed to parse maintenance time:", error);
|
||||
return estTimeString;
|
||||
}
|
||||
};
|
||||
|
||||
const localTime = formatMaintenanceTime(startTime);
|
||||
|
||||
return (
|
||||
<div className="bg-primary text-[#0D0F11] p-4 rounded">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<FaTriangleExclamation className="text-white align-middle" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium">
|
||||
{t("MAINTENANCE$SCHEDULED_MESSAGE", { time: localTime })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
// this file generate by script, don't modify it manually!!!
|
||||
export enum I18nKey {
|
||||
MAINTENANCE$SCHEDULED_MESSAGE = "MAINTENANCE$SCHEDULED_MESSAGE",
|
||||
MICROAGENT$NO_REPOSITORY_FOUND = "MICROAGENT$NO_REPOSITORY_FOUND",
|
||||
MICROAGENT$ADD_TO_MICROAGENT = "MICROAGENT$ADD_TO_MICROAGENT",
|
||||
MICROAGENT$WHAT_TO_ADD = "MICROAGENT$WHAT_TO_ADD",
|
||||
|
||||
@ -1,4 +1,20 @@
|
||||
{
|
||||
"MAINTENANCE$SCHEDULED_MESSAGE": {
|
||||
"en": "Scheduled maintenance will begin at {{time}}",
|
||||
"ja": "予定されたメンテナンスは{{time}}に開始されます",
|
||||
"zh-CN": "计划维护将于{{time}}开始",
|
||||
"zh-TW": "計劃維護將於{{time}}開始",
|
||||
"ko-KR": "예정된 유지보수가 {{time}}에 시작됩니다",
|
||||
"no": "Planlagt vedlikehold starter {{time}}",
|
||||
"it": "La manutenzione programmata inizierà alle {{time}}",
|
||||
"pt": "A manutenção programada começará às {{time}}",
|
||||
"es": "El mantenimiento programado comenzará a las {{time}}",
|
||||
"ar": "ستبدأ الصيانة المجدولة في {{time}}",
|
||||
"fr": "La maintenance programmée commencera à {{time}}",
|
||||
"tr": "Planlı bakım {{time}} tarihinde başlayacak",
|
||||
"de": "Die geplante Wartung beginnt um {{time}}",
|
||||
"uk": "Планове технічне обслуговування розпочнеться о {{time}}"
|
||||
},
|
||||
"MICROAGENT$NO_REPOSITORY_FOUND": {
|
||||
"en": "No repository found to launch microagent",
|
||||
"ja": "マイクロエージェントを起動するためのリポジトリが見つかりません",
|
||||
|
||||
@ -187,6 +187,10 @@ export const handlers = [
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: mockSaas,
|
||||
},
|
||||
// Uncomment the following to test the maintenance banner
|
||||
// MAINTENANCE: {
|
||||
// startTime: "2024-01-15T10:00:00-05:00", // EST timestamp
|
||||
// },
|
||||
};
|
||||
|
||||
return HttpResponse.json(config);
|
||||
|
||||
@ -26,6 +26,7 @@ import { useAutoLogin } from "#/hooks/use-auto-login";
|
||||
import { useAuthCallback } from "#/hooks/use-auth-callback";
|
||||
import { LOCAL_STORAGE_KEYS } from "#/utils/local-storage";
|
||||
import { EmailVerificationGuard } from "#/components/features/guards/email-verification-guard";
|
||||
import { MaintenanceBanner } from "#/components/features/maintenance/maintenance-banner";
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
@ -205,6 +206,9 @@ export default function MainApp() {
|
||||
id="root-outlet"
|
||||
className="h-[calc(100%-50px)] md:h-full w-full relative overflow-auto"
|
||||
>
|
||||
{config.data?.MAINTENANCE && (
|
||||
<MaintenanceBanner startTime={config.data.MAINTENANCE.startTime} />
|
||||
)}
|
||||
<EmailVerificationGuard>
|
||||
<Outlet />
|
||||
</EmailVerificationGuard>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user