mirror of https://github.com/kortix-ai/suna.git
feat(frontend): implement maintenance notice and banner components with edge configuration integration
This commit is contained in:
parent
c0fbd794de
commit
be00efbd70
|
@ -5,3 +5,5 @@ NEXT_PUBLIC_BACKEND_URL=""
|
|||
NEXT_PUBLIC_URL=""
|
||||
NEXT_PUBLIC_GOOGLE_CLIENT_ID=""
|
||||
OPENAI_API_KEY=""
|
||||
|
||||
EDGE_CONFIG="https://edge-config.vercel.com/REDACTED?token=REDACTED"
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"@usebasejump/shared": "^0.0.3",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"@vercel/edge-config": "^1.4.0",
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"@xyflow/react": "^12.7.0",
|
||||
"autoprefixer": "10.4.17",
|
||||
|
@ -89,6 +90,7 @@
|
|||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "^2.0.3",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
|
@ -8153,6 +8155,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/edge-config": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/edge-config/-/edge-config-1.4.0.tgz",
|
||||
"integrity": "sha512-69Wg5gw9DzwnyUmnjToSeLRm1nm8mCPgN0kflX8EHRHyqvzH80wPem5A8rI2LXPb2Y9tJNoqN3vXPcQhS2Wh5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@vercel/edge-config-fs": "0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.7.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/edge-config-fs": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/edge-config-fs/-/edge-config-fs-0.1.0.tgz",
|
||||
"integrity": "sha512-NRIBwfcS0bUoUbRWlNGetqjvLSwgYH/BqKqDN7vK1g32p7dN96k0712COgaz6VFizAm9b0g6IG6hR6+hc0KCPg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@vercel/speed-insights": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.2.0.tgz",
|
||||
|
@ -16182,6 +16210,12 @@
|
|||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/server-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
|
||||
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"@uiw/react-codemirror": "^4.23.10",
|
||||
"@usebasejump/shared": "^0.0.3",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"@vercel/edge-config": "^1.4.0",
|
||||
"@vercel/speed-insights": "^1.2.0",
|
||||
"@xyflow/react": "^12.7.0",
|
||||
"autoprefixer": "10.4.17",
|
||||
|
@ -92,6 +93,7 @@
|
|||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "^2.0.3",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SidebarLeft } from '@/components/sidebar/sidebar-left';
|
||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
||||
// import { PricingAlert } from "@/components/billing/pricing-alert"
|
||||
import { MaintenanceAlert } from '@/components/maintenance-alert';
|
||||
import { useAccounts } from '@/hooks/use-accounts';
|
||||
import { useAuth } from '@/components/AuthProvider';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { checkApiHealth } from '@/lib/api';
|
||||
import { MaintenancePage } from '@/components/maintenance/maintenance-page';
|
||||
import { DeleteOperationProvider } from '@/contexts/DeleteOperationContext';
|
||||
import { StatusOverlay } from '@/components/ui/status-overlay';
|
||||
import { VSentry } from '@/components/sentry';
|
||||
import type { IMaintenanceNotice } from '@/lib/edge-flags';
|
||||
import { MaintenanceNotice } from './maintenance-notice';
|
||||
import { MaintenanceBanner } from './maintenance-banner';
|
||||
|
||||
interface DashboardLayoutContentProps {
|
||||
children: React.ReactNode;
|
||||
maintenanceNotice: IMaintenanceNotice;
|
||||
}
|
||||
|
||||
export default function DashboardLayoutContent({
|
||||
children,
|
||||
maintenanceNotice,
|
||||
}: DashboardLayoutContentProps) {
|
||||
// const [showPricingAlert, setShowPricingAlert] = useState(false)
|
||||
const [showMaintenanceAlert, setShowMaintenanceAlert] = useState(false);
|
||||
const [isApiHealthy, setIsApiHealthy] = useState(true);
|
||||
const [isCheckingHealth, setIsCheckingHealth] = useState(true);
|
||||
const { data: accounts } = useAccounts();
|
||||
const personalAccount = accounts?.find((account) => account.personal_account);
|
||||
const { user, isLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// setShowPricingAlert(false)
|
||||
setShowMaintenanceAlert(false);
|
||||
}, []);
|
||||
|
||||
// Check API health
|
||||
useEffect(() => {
|
||||
const checkHealth = async () => {
|
||||
try {
|
||||
const health = await checkApiHealth();
|
||||
setIsApiHealthy(health.status === 'ok');
|
||||
} catch (error) {
|
||||
console.error('API health check failed:', error);
|
||||
setIsApiHealthy(false);
|
||||
} finally {
|
||||
setIsCheckingHealth(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkHealth();
|
||||
// Check health every 30 seconds
|
||||
const interval = setInterval(checkHealth, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Check authentication status
|
||||
useEffect(() => {
|
||||
if (!isLoading && !user) {
|
||||
router.push('/auth');
|
||||
}
|
||||
}, [user, isLoading, router]);
|
||||
|
||||
if (maintenanceNotice.enabled) {
|
||||
const now = new Date();
|
||||
const startTime = maintenanceNotice.startTime;
|
||||
const endTime = maintenanceNotice.endTime;
|
||||
|
||||
if (now > startTime) {
|
||||
return (
|
||||
<div className="w-screen h-screen flex items-center justify-center">
|
||||
<div className="max-w-xl">
|
||||
<MaintenanceNotice endTime={endTime.toISOString()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mantenanceBanner: React.ReactNode | null = null;
|
||||
if (maintenanceNotice.enabled) {
|
||||
mantenanceBanner = (
|
||||
<MaintenanceBanner
|
||||
startTime={maintenanceNotice.startTime.toISOString()}
|
||||
endTime={maintenanceNotice.endTime.toISOString()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading state while checking auth or health
|
||||
if (isLoading || isCheckingHealth) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Don't render anything if not authenticated
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show maintenance page if API is not healthy
|
||||
if (!isApiHealthy) {
|
||||
return <MaintenancePage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DeleteOperationProvider>
|
||||
<SidebarProvider>
|
||||
<SidebarLeft />
|
||||
<SidebarInset>
|
||||
{mantenanceBanner}
|
||||
<div className="bg-background">{children}</div>
|
||||
</SidebarInset>
|
||||
|
||||
{/* <PricingAlert
|
||||
open={showPricingAlert}
|
||||
onOpenChange={setShowPricingAlert}
|
||||
closeable={false}
|
||||
accountId={personalAccount?.account_id}
|
||||
/> */}
|
||||
|
||||
<MaintenanceAlert
|
||||
open={showMaintenanceAlert}
|
||||
onOpenChange={setShowMaintenanceAlert}
|
||||
closeable={true}
|
||||
/>
|
||||
<VSentry />
|
||||
|
||||
{/* Status overlay for deletion operations */}
|
||||
<StatusOverlay />
|
||||
</SidebarProvider>
|
||||
</DeleteOperationProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
'use client';
|
||||
|
||||
import { AlertCircle, X, Info } from 'lucide-react';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface MaintenanceBannerProps {
|
||||
startTime: string; // ISO string
|
||||
endTime: string; // ISO string
|
||||
}
|
||||
|
||||
export function MaintenanceBanner({
|
||||
startTime,
|
||||
endTime,
|
||||
}: MaintenanceBannerProps) {
|
||||
const [timeDisplay, setTimeDisplay] = useState<string>('');
|
||||
const [isMaintenanceActive, setIsMaintenanceActive] = useState(false);
|
||||
const [isDismissed, setIsDismissed] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// Create a unique key for this maintenance window
|
||||
const maintenanceKey = `maintenance-dismissed-${startTime}-${endTime}`;
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
// Check if this maintenance has been dismissed
|
||||
const dismissed = localStorage.getItem(maintenanceKey);
|
||||
if (dismissed === 'true') {
|
||||
setIsDismissed(true);
|
||||
}
|
||||
}, [maintenanceKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateCountdown = () => {
|
||||
const now = new Date();
|
||||
const start = new Date(startTime);
|
||||
const end = new Date(endTime);
|
||||
|
||||
// Check if maintenance is currently active
|
||||
if (now >= start && now <= end) {
|
||||
setIsMaintenanceActive(true);
|
||||
const diffToEnd = end.getTime() - now.getTime();
|
||||
|
||||
if (diffToEnd <= 0) {
|
||||
setTimeDisplay('Maintenance completed');
|
||||
return;
|
||||
}
|
||||
|
||||
const hours = Math.floor(diffToEnd / (1000 * 60 * 60));
|
||||
const minutes = Math.floor(
|
||||
(diffToEnd % (1000 * 60 * 60)) / (1000 * 60),
|
||||
);
|
||||
|
||||
if (hours > 0) {
|
||||
setTimeDisplay(`${hours}h ${minutes}m remaining`);
|
||||
} else {
|
||||
setTimeDisplay(`${minutes}m remaining`);
|
||||
}
|
||||
} else if (now < start) {
|
||||
// Maintenance hasn't started yet
|
||||
setIsMaintenanceActive(false);
|
||||
const diffToStart = start.getTime() - now.getTime();
|
||||
|
||||
if (diffToStart <= 0) {
|
||||
setTimeDisplay('starting now');
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(diffToStart / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor(
|
||||
(diffToStart % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
|
||||
);
|
||||
const minutes = Math.floor(
|
||||
(diffToStart % (1000 * 60 * 60)) / (1000 * 60),
|
||||
);
|
||||
|
||||
if (days > 0) {
|
||||
setTimeDisplay(`starting in ${days}d ${hours}h`);
|
||||
} else if (hours > 0) {
|
||||
setTimeDisplay(`starting in ${hours}h ${minutes}m`);
|
||||
} else {
|
||||
setTimeDisplay(`starting in ${minutes}m`);
|
||||
}
|
||||
} else {
|
||||
// Maintenance is over
|
||||
setTimeDisplay('');
|
||||
}
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
const interval = setInterval(updateCountdown, 60000); // Update every minute
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [startTime, endTime]);
|
||||
|
||||
const handleDismiss = () => {
|
||||
setIsDismissed(true);
|
||||
localStorage.setItem(maintenanceKey, 'true');
|
||||
};
|
||||
|
||||
const formatDateTime = (isoString: string) => {
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleString(undefined, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
};
|
||||
|
||||
const getDuration = () => {
|
||||
const start = new Date(startTime);
|
||||
const end = new Date(endTime);
|
||||
const diff = end.getTime() - start.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
};
|
||||
|
||||
// Don't show banner if maintenance is over or dismissed
|
||||
const now = new Date();
|
||||
const end = new Date(endTime);
|
||||
|
||||
// Don't render until mounted (SSR safety)
|
||||
if (!isMounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (now > end || isDismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
className={`py-2 ${
|
||||
isMaintenanceActive
|
||||
? 'border-orange-200 bg-orange-50 dark:border-orange-800 dark:bg-orange-950'
|
||||
: 'border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950'
|
||||
}`}
|
||||
>
|
||||
<AlertCircle
|
||||
className={`h-4 w-4 ${
|
||||
isMaintenanceActive
|
||||
? 'text-orange-600 dark:text-orange-400'
|
||||
: 'text-yellow-600 dark:text-yellow-400'
|
||||
}`}
|
||||
/>
|
||||
<AlertDescription
|
||||
className={`flex items-center gap-2 ${
|
||||
isMaintenanceActive
|
||||
? 'text-orange-700 dark:text-orange-300'
|
||||
: 'text-yellow-700 dark:text-yellow-300'
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">
|
||||
{isMaintenanceActive
|
||||
? 'Scheduled maintenance in progress'
|
||||
: 'Scheduled maintenance'}
|
||||
</span>
|
||||
{timeDisplay && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{timeDisplay}</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Info icon with tooltip */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3 w-3 cursor-help opacity-60 hover:opacity-100 transition-opacity" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs">
|
||||
<div className="space-y-1 text-xs">
|
||||
{!isMaintenanceActive && (
|
||||
<div>Starts: {formatDateTime(startTime)}</div>
|
||||
)}
|
||||
<div>
|
||||
{isMaintenanceActive ? 'Expected completion' : 'Ends'}:{' '}
|
||||
{formatDateTime(endTime)}
|
||||
</div>
|
||||
<div>Duration: {getDuration()}</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</AlertDescription>
|
||||
|
||||
{/* Dismiss button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`absolute right-2 top-1/2 -translate-y-1/2 h-6 w-6 p-0 hover:bg-transparent ${
|
||||
isMaintenanceActive
|
||||
? 'text-orange-600 hover:text-orange-800 dark:text-orange-400 dark:hover:text-orange-200'
|
||||
: 'text-yellow-600 hover:text-yellow-800 dark:text-yellow-400 dark:hover:text-yellow-200'
|
||||
}`}
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss maintenance notice"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Alert>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
'use client';
|
||||
|
||||
import { Clock, AlertTriangle, Server } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface MaintenanceNoticeProps {
|
||||
endTime: string; // ISO string
|
||||
}
|
||||
|
||||
export function MaintenanceNotice({ endTime }: MaintenanceNoticeProps) {
|
||||
const [timeRemaining, setTimeRemaining] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const updateTimeRemaining = () => {
|
||||
const now = new Date();
|
||||
const end = new Date(endTime);
|
||||
const diff = end.getTime() - now.getTime();
|
||||
|
||||
if (diff <= 0) {
|
||||
setTimeRemaining('Almost done!');
|
||||
return;
|
||||
}
|
||||
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (hours > 0) {
|
||||
setTimeRemaining(`${hours}h ${minutes}m remaining`);
|
||||
} else {
|
||||
setTimeRemaining(`${minutes}m remaining`);
|
||||
}
|
||||
};
|
||||
|
||||
updateTimeRemaining();
|
||||
const interval = setInterval(updateTimeRemaining, 60000); // Update every minute
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [endTime]);
|
||||
|
||||
const formatEndTime = (isoString: string) => {
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleString(undefined, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
timeZoneName: 'short',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto p-4">
|
||||
<div className="bg-background border border-border rounded-lg p-6 shadow-sm">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/20">
|
||||
<Server className="h-6 w-6 text-amber-600 dark:text-amber-500 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Scheduled Maintenance
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
We're performing scheduled maintenance to improve our systems.
|
||||
Some features may be temporarily unavailable.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Expected completion: {formatEndTime(endTime)}</span>
|
||||
</div>
|
||||
|
||||
{timeRemaining && (
|
||||
<div className="inline-flex items-center gap-2 px-3 py-2 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800/30 rounded-md">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-500" />
|
||||
<span className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||
{timeRemaining}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,110 +1,24 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SidebarLeft } from '@/components/sidebar/sidebar-left';
|
||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
||||
// import { PricingAlert } from "@/components/billing/pricing-alert"
|
||||
import { MaintenanceAlert } from '@/components/maintenance-alert';
|
||||
import { useAccounts } from '@/hooks/use-accounts';
|
||||
import { useAuth } from '@/components/AuthProvider';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { checkApiHealth } from '@/lib/api';
|
||||
import { MaintenancePage } from '@/components/maintenance/maintenance-page';
|
||||
import { DeleteOperationProvider } from '@/contexts/DeleteOperationContext';
|
||||
import { StatusOverlay } from '@/components/ui/status-overlay';
|
||||
import { VSentry } from '@/components/sentry';
|
||||
import { maintenanceNoticeFlag } from '@/lib/edge-flags';
|
||||
import DashboardLayoutContent from './_components/layout-content';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
||||
// const [showPricingAlert, setShowPricingAlert] = useState(false)
|
||||
const [showMaintenanceAlert, setShowMaintenanceAlert] = useState(false);
|
||||
const [isApiHealthy, setIsApiHealthy] = useState(true);
|
||||
const [isCheckingHealth, setIsCheckingHealth] = useState(true);
|
||||
const { data: accounts } = useAccounts();
|
||||
const personalAccount = accounts?.find((account) => account.personal_account);
|
||||
const { user, isLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// setShowPricingAlert(false)
|
||||
setShowMaintenanceAlert(false);
|
||||
}, []);
|
||||
|
||||
// Check API health
|
||||
useEffect(() => {
|
||||
const checkHealth = async () => {
|
||||
try {
|
||||
const health = await checkApiHealth();
|
||||
setIsApiHealthy(health.status === 'ok');
|
||||
} catch (error) {
|
||||
console.error('API health check failed:', error);
|
||||
setIsApiHealthy(false);
|
||||
} finally {
|
||||
setIsCheckingHealth(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkHealth();
|
||||
// Check health every 30 seconds
|
||||
const interval = setInterval(checkHealth, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Check authentication status
|
||||
useEffect(() => {
|
||||
if (!isLoading && !user) {
|
||||
router.push('/auth');
|
||||
}
|
||||
}, [user, isLoading, router]);
|
||||
|
||||
// Show loading state while checking auth or health
|
||||
if (isLoading || isCheckingHealth) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Don't render anything if not authenticated
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show maintenance page if API is not healthy
|
||||
if (!isApiHealthy) {
|
||||
return <MaintenancePage />;
|
||||
}
|
||||
const getMaintenanceNotice = async () => {
|
||||
const maintenanceNotice = await maintenanceNoticeFlag();
|
||||
return maintenanceNotice;
|
||||
};
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: DashboardLayoutProps) {
|
||||
await cookies();
|
||||
const maintenanceNotice = await getMaintenanceNotice();
|
||||
return (
|
||||
<DeleteOperationProvider>
|
||||
<SidebarProvider>
|
||||
<SidebarLeft />
|
||||
<SidebarInset>
|
||||
<div className="bg-background">{children}</div>
|
||||
</SidebarInset>
|
||||
|
||||
{/* <PricingAlert
|
||||
open={showPricingAlert}
|
||||
onOpenChange={setShowPricingAlert}
|
||||
closeable={false}
|
||||
accountId={personalAccount?.account_id}
|
||||
/> */}
|
||||
|
||||
<MaintenanceAlert
|
||||
open={showMaintenanceAlert}
|
||||
onOpenChange={setShowMaintenanceAlert}
|
||||
closeable={true}
|
||||
/>
|
||||
<VSentry />
|
||||
|
||||
{/* Status overlay for deletion operations */}
|
||||
<StatusOverlay />
|
||||
</SidebarProvider>
|
||||
</DeleteOperationProvider>
|
||||
<DashboardLayoutContent maintenanceNotice={maintenanceNotice}>
|
||||
{children}
|
||||
</DashboardLayoutContent>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import { flag } from 'flags/next';
|
||||
import { getAll } from '@vercel/edge-config';
|
||||
|
||||
export type IMaintenanceNotice =
|
||||
| {
|
||||
enabled: true;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
}
|
||||
| {
|
||||
enabled: false;
|
||||
startTime?: undefined;
|
||||
endTime?: undefined;
|
||||
};
|
||||
|
||||
export const maintenanceNoticeFlag = flag({
|
||||
key: 'maintenance-notice',
|
||||
async decide() {
|
||||
try {
|
||||
if (!process.env.EDGE_CONFIG) {
|
||||
console.warn('Edge config is not set');
|
||||
return { enabled: false } as const;
|
||||
}
|
||||
|
||||
const flags = await getAll([
|
||||
'maintenance-notice_start-time',
|
||||
'maintenance-notice_end-time',
|
||||
'maintenance-notice_enabled',
|
||||
]);
|
||||
|
||||
if (!flags['maintenance-notice_enabled']) {
|
||||
return { enabled: false } as const;
|
||||
}
|
||||
|
||||
const startTime = new Date(flags['maintenance-notice_start-time']);
|
||||
const endTime = new Date(flags['maintenance-notice_end-time']);
|
||||
|
||||
if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) {
|
||||
throw new Error(
|
||||
`Invalid maintenance notice start or end time: ${flags['maintenance-notice_start-time']} or ${flags['maintenance-notice_end-time']}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
startTime,
|
||||
endTime,
|
||||
} as const;
|
||||
} catch (cause) {
|
||||
console.error(
|
||||
new Error('Failed to get maintenance notice flag', { cause }),
|
||||
);
|
||||
return { enabled: false } as const;
|
||||
}
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue