mirror of
https://github.com/community-scripts/ProxmoxVE-Local.git
synced 2026-08-04 03:05:10 -04:00
Wrap modals in portal and use zIndex
Use ModalPortal and zIndex values from useRegisterModal across modal components to avoid hardcoded z-50 stacking. Updated ExecutionModeModal, LXCSettingsModal, PublicKeyModal, ReleaseNotesModal, SetupModal, StorageSelectionModal, and TextViewer to import ModalPortal, capture the zIndex returned by useRegisterModal, wrap modal markup in <ModalPortal>, and apply style={{ zIndex }} to backdrops; LXC result overlay uses zIndex + 10 for proper stacking. Also changed LoadingOverlay in VersionDisplay to render with createPortal(document.body) and added the import. These changes centralize stacking behavior and prevent z-index conflicts when multiple modals/overlays are present.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, type ReactNode } from 'react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { useAuth } from './AuthProvider';
|
||||
import { AuthModal } from './AuthModal';
|
||||
import { SetupModal } from './SetupModal';
|
||||
@@ -9,45 +9,17 @@ interface AuthGuardProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface AuthConfig {
|
||||
username: string | null;
|
||||
enabled: boolean;
|
||||
hasCredentials: boolean;
|
||||
setupCompleted: boolean;
|
||||
}
|
||||
|
||||
export function AuthGuard({ children }: AuthGuardProps) {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
const [authConfig, setAuthConfig] = useState<AuthConfig | null>(null);
|
||||
const [configLoading, setConfigLoading] = useState(true);
|
||||
const [setupCompleted, setSetupCompleted] = useState(false);
|
||||
const { isAuthenticated, isLoading, setupCompleted, authEnabled, refreshConfig } = useAuth();
|
||||
const [localSetupCompleted, setLocalSetupCompleted] = useState(false);
|
||||
|
||||
const handleSetupComplete = async () => {
|
||||
setSetupCompleted(true);
|
||||
// Refresh auth config without reloading the page
|
||||
await fetchAuthConfig();
|
||||
setLocalSetupCompleted(true);
|
||||
await refreshConfig();
|
||||
};
|
||||
|
||||
const fetchAuthConfig = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/settings/auth-credentials');
|
||||
if (response.ok) {
|
||||
const config = await response.json() as AuthConfig;
|
||||
setAuthConfig(config);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching auth config:', error);
|
||||
} finally {
|
||||
setConfigLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void fetchAuthConfig();
|
||||
}, []);
|
||||
|
||||
// Show loading while checking auth status
|
||||
if (isLoading || configLoading) {
|
||||
// Show loading while AuthProvider is still checking
|
||||
if (isLoading || setupCompleted === null) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@@ -59,12 +31,12 @@ export function AuthGuard({ children }: AuthGuardProps) {
|
||||
}
|
||||
|
||||
// Show setup modal if setup has not been completed yet
|
||||
if (authConfig && !authConfig.setupCompleted && !setupCompleted) {
|
||||
if (!setupCompleted && !localSetupCompleted) {
|
||||
return <SetupModal isOpen={true} onComplete={handleSetupComplete} />;
|
||||
}
|
||||
|
||||
// Show auth modal if auth is enabled but user is not authenticated
|
||||
if (authConfig && authConfig.enabled && !isAuthenticated) {
|
||||
if (authEnabled && !isAuthenticated) {
|
||||
return <AuthModal isOpen={true} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,12 @@ interface AuthContextType {
|
||||
username: string | null;
|
||||
isLoading: boolean;
|
||||
expirationTime: number | null;
|
||||
setupCompleted: boolean | null;
|
||||
authEnabled: boolean | null;
|
||||
login: (username: string, password: string) => Promise<boolean>;
|
||||
logout: () => void;
|
||||
checkAuth: () => Promise<void>;
|
||||
refreshConfig: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
@@ -30,6 +33,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [expirationTime, setExpirationTime] = useState<number | null>(null);
|
||||
const [setupCompleted, setSetupCompleted] = useState<boolean | null>(null);
|
||||
const [authEnabled, setAuthEnabled] = useState<boolean | null>(null);
|
||||
|
||||
const checkAuthInternal = async (retryCount = 0) => {
|
||||
try {
|
||||
@@ -41,6 +46,9 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
setSetupCompleted(setupData.setupCompleted);
|
||||
setAuthEnabled(setupData.enabled);
|
||||
|
||||
// If setup is not completed or auth is disabled, don't verify
|
||||
if (!setupData.setupCompleted || !setupData.enabled) {
|
||||
setIsAuthenticated(false);
|
||||
@@ -99,6 +107,22 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
return checkAuthInternal(0);
|
||||
}, []);
|
||||
|
||||
const refreshConfig = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/settings/auth-credentials");
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as {
|
||||
setupCompleted: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
setSetupCompleted(data.setupCompleted);
|
||||
setAuthEnabled(data.enabled);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error refreshing auth config:", error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = async (
|
||||
username: string,
|
||||
password: string,
|
||||
@@ -158,9 +182,12 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
username,
|
||||
isLoading,
|
||||
expirationTime,
|
||||
setupCompleted,
|
||||
authEnabled,
|
||||
login,
|
||||
logout,
|
||||
checkAuth,
|
||||
refreshConfig,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -13,6 +13,34 @@ interface CategorySidebarProps {
|
||||
}
|
||||
|
||||
// Icon mapping for categories
|
||||
const categoryIconColorMap: Record<string, string> = {
|
||||
server: "text-blue-500",
|
||||
monitor: "text-sky-400",
|
||||
box: "text-orange-400",
|
||||
shield: "text-green-500",
|
||||
"shield-check": "text-green-500",
|
||||
key: "text-yellow-500",
|
||||
archive: "text-amber-400",
|
||||
database: "text-indigo-500",
|
||||
"chart-bar": "text-emerald-500",
|
||||
template: "text-violet-500",
|
||||
"folder-open": "text-cyan-500",
|
||||
"document-text": "text-slate-400",
|
||||
film: "text-rose-500",
|
||||
download: "text-cyan-500",
|
||||
"video-camera": "text-pink-500",
|
||||
home: "text-lime-500",
|
||||
wifi: "text-fuchsia-500",
|
||||
"chat-alt": "text-sky-500",
|
||||
clock: "text-orange-500",
|
||||
code: "text-green-400",
|
||||
"external-link": "text-blue-400",
|
||||
sparkles: "text-purple-500",
|
||||
"currency-dollar": "text-emerald-400",
|
||||
puzzle: "text-pink-400",
|
||||
office: "text-stone-500",
|
||||
};
|
||||
|
||||
const CategoryIcon = ({
|
||||
iconName,
|
||||
className = "w-5 h-5",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from './ui/button';
|
||||
import { ColorCodedDropdown } from './ColorCodedDropdown';
|
||||
import { SettingsModal } from './SettingsModal';
|
||||
import { ConfigurationModal, type EnvVars } from './ConfigurationModal';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
import { useRegisterModal, ModalPortal } from './modal/ModalStackProvider';
|
||||
|
||||
|
||||
interface ExecutionModeModalProps {
|
||||
@@ -19,7 +19,7 @@ interface ExecutionModeModalProps {
|
||||
}
|
||||
|
||||
export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName, script }: ExecutionModeModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'execution-mode-modal', allowEscape: true, onClose });
|
||||
const zIndex = useRegisterModal(isOpen, { id: 'execution-mode-modal', allowEscape: true, onClose });
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -95,7 +95,8 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName, scr
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<ModalPortal>
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center p-4" style={{ zIndex }}>
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-md w-full border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
@@ -275,6 +276,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName, scr
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
|
||||
{/* Server Settings Modal */}
|
||||
<SettingsModal
|
||||
|
||||
@@ -13,6 +13,7 @@ import { LXCSettingsModal } from "./LXCSettingsModal";
|
||||
import { StorageSelectionModal } from "./StorageSelectionModal";
|
||||
import { BackupWarningModal } from "./BackupWarningModal";
|
||||
import { CloneCountInputModal } from "./CloneCountInputModal";
|
||||
import { ModalPortal } from "./modal/ModalStackProvider";
|
||||
import type { Storage } from "~/server/services/storageService";
|
||||
import { getContrastColor } from "../../lib/colorUtils";
|
||||
import {
|
||||
@@ -2634,6 +2635,7 @@ export function InstalledScriptsTab() {
|
||||
|
||||
{/* Backup Prompt Modal */}
|
||||
{showBackupPrompt && (
|
||||
<ModalPortal>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm">
|
||||
<div className="bg-card border-border w-full max-w-md rounded-lg border shadow-xl">
|
||||
<div className="border-border flex items-center justify-center border-b p-6">
|
||||
@@ -2695,6 +2697,7 @@ export function InstalledScriptsTab() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
)}
|
||||
|
||||
{/* Storage Selection Modal */}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ContextualHelpIcon } from './ContextualHelpIcon';
|
||||
import { LoadingModal } from './LoadingModal';
|
||||
import { ConfirmationModal } from './ConfirmationModal';
|
||||
import { RefreshCw, AlertTriangle, CheckCircle } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
import { useRegisterModal, ModalPortal } from './modal/ModalStackProvider';
|
||||
|
||||
interface InstalledScript {
|
||||
id: number;
|
||||
@@ -42,7 +42,7 @@ interface LXCSettingsModalProps {
|
||||
}
|
||||
|
||||
export function LXCSettingsModal({ isOpen, script, onClose, onSave: _onSave }: LXCSettingsModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'lxc-settings-modal', allowEscape: true, onClose });
|
||||
const zIndex = useRegisterModal(isOpen, { id: 'lxc-settings-modal', allowEscape: true, onClose });
|
||||
const [activeTab, setActiveTab] = useState<string>('common');
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
const [showResultModal, setShowResultModal] = useState(false);
|
||||
@@ -262,7 +262,8 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave: _onSave }: L
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<ModalPortal>
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center p-4" style={{ zIndex }}>
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-6xl w-full max-h-[95vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-border">
|
||||
@@ -667,6 +668,7 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave: _onSave }: L
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
<ConfirmationModal
|
||||
@@ -697,7 +699,7 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave: _onSave }: L
|
||||
|
||||
{/* Result Modal */}
|
||||
{showResultModal && resultType && resultMessage && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center" style={{ zIndex: zIndex + 10 }}>
|
||||
<div className="bg-card text-card-foreground rounded-lg shadow-xl max-w-md w-full mx-4 border border-border">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import { X, Copy, Check, Server, Globe } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
import { useRegisterModal, ModalPortal } from './modal/ModalStackProvider';
|
||||
|
||||
interface PublicKeyModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,7 +14,7 @@ interface PublicKeyModalProps {
|
||||
}
|
||||
|
||||
export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverIp }: PublicKeyModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'public-key-modal', allowEscape: true, onClose });
|
||||
const zIndex = useRegisterModal(isOpen, { id: 'public-key-modal', allowEscape: true, onClose });
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [commandCopied, setCommandCopied] = useState(false);
|
||||
|
||||
@@ -94,7 +94,8 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<ModalPortal>
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center p-4" style={{ zIndex }}>
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-2xl w-full border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
@@ -215,5 +216,6 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { api } from '~/trpc/react';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { X, ExternalLink, Calendar, Tag, Loader2 } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
import { useRegisterModal, ModalPortal } from './modal/ModalStackProvider';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
@@ -35,7 +35,7 @@ const markVersionAsSeen = (version: string): void => {
|
||||
};
|
||||
|
||||
export function ReleaseNotesModal({ isOpen, onClose, highlightVersion }: ReleaseNotesModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'release-notes-modal', allowEscape: true, onClose });
|
||||
const zIndex = useRegisterModal(isOpen, { id: 'release-notes-modal', allowEscape: true, onClose });
|
||||
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
|
||||
const { data: releasesData, isLoading, error } = api.version.getAllReleases.useQuery(undefined, {
|
||||
enabled: isOpen
|
||||
@@ -66,7 +66,8 @@ export function ReleaseNotesModal({ isOpen, onClose, highlightVersion }: Release
|
||||
const releases: Release[] = releasesData?.success ? releasesData.releases ?? [] : [];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<ModalPortal>
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center p-4" style={{ zIndex }}>
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
@@ -215,6 +216,7 @@ export function ReleaseNotesModal({ isOpen, onClose, highlightVersion }: Release
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -380,13 +380,14 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
.filter((s) => s?.date_created)
|
||||
.sort((a, b) =>
|
||||
(b?.date_created ?? "").localeCompare(a?.date_created ?? ""),
|
||||
);
|
||||
)
|
||||
.slice(0, 15);
|
||||
break;
|
||||
case "updated":
|
||||
scripts = scripts
|
||||
.filter((s) => s?.updateable)
|
||||
.filter((s) => s?.date_updated)
|
||||
.sort((a, b) =>
|
||||
(b?.date_created ?? "").localeCompare(a?.date_created ?? ""),
|
||||
(b?.date_updated ?? "").localeCompare(a?.date_updated ?? ""),
|
||||
);
|
||||
break;
|
||||
case "dev":
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Toggle } from './ui/toggle';
|
||||
import { Lock, User, Shield, AlertCircle } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
import { useRegisterModal, ModalPortal } from './modal/ModalStackProvider';
|
||||
|
||||
interface SetupModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -13,7 +13,7 @@ interface SetupModalProps {
|
||||
}
|
||||
|
||||
export function SetupModal({ isOpen, onComplete }: SetupModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'setup-modal', allowEscape: true, onClose: () => null });
|
||||
const zIndex = useRegisterModal(isOpen, { id: 'setup-modal', allowEscape: true, onClose: () => null });
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
@@ -84,7 +84,8 @@ export function SetupModal({ isOpen, onComplete }: SetupModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<ModalPortal>
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center p-4" style={{ zIndex }}>
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-md w-full border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-center p-6 border-b border-border">
|
||||
@@ -202,5 +203,6 @@ export function SetupModal({ isOpen, onComplete }: SetupModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Database, RefreshCw, CheckCircle } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
import { useRegisterModal, ModalPortal } from './modal/ModalStackProvider';
|
||||
import type { Storage } from '~/server/services/storageService';
|
||||
|
||||
interface StorageSelectionModalProps {
|
||||
@@ -33,7 +33,7 @@ export function StorageSelectionModal({
|
||||
}: StorageSelectionModalProps) {
|
||||
const [selectedStorage, setSelectedStorage] = useState<Storage | null>(null);
|
||||
|
||||
useRegisterModal(isOpen, { id: 'storage-selection-modal', allowEscape: true, onClose });
|
||||
const zIndex = useRegisterModal(isOpen, { id: 'storage-selection-modal', allowEscape: true, onClose });
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -53,7 +53,8 @@ export function StorageSelectionModal({
|
||||
const filteredStorages = filterFn ? storages.filter(filterFn) : storages.filter(s => s.supportsBackup);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<ModalPortal>
|
||||
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center p-4" style={{ zIndex }}>
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-2xl w-full border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
@@ -171,6 +172,7 @@ export function StorageSelectionModal({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { tomorrow } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { Button } from "./ui/button";
|
||||
import { useRegisterModal, ModalPortal } from "./modal/ModalStackProvider";
|
||||
import type { Script } from "../../types/script";
|
||||
|
||||
interface TextViewerProps {
|
||||
@@ -182,11 +183,15 @@ export function TextViewer({
|
||||
}
|
||||
};
|
||||
|
||||
const zIndex = useRegisterModal(isOpen, { id: 'text-viewer-modal', allowEscape: true, onClose });
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<ModalPortal>
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm"
|
||||
className="fixed inset-0 flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm"
|
||||
style={{ zIndex }}
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="bg-card border-border mx-4 flex max-h-[90vh] w-full max-w-6xl flex-col rounded-lg border shadow-xl sm:mx-0">
|
||||
@@ -372,5 +377,6 @@ export function TextViewer({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPortal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { UpdateConfirmationModal } from "./UpdateConfirmationModal";
|
||||
|
||||
import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface VersionDisplayProps {
|
||||
onOpenReleaseNotes?: () => void;
|
||||
@@ -29,7 +30,7 @@ function LoadingOverlay({
|
||||
}, [logs]);
|
||||
|
||||
|
||||
return (
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-card rounded-lg p-8 shadow-2xl border border-border max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
@@ -74,10 +75,10 @@ function LoadingOverlay({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export function VersionDisplay({ onOpenReleaseNotes }: VersionDisplayProps = {}) {
|
||||
const { data: versionStatus, isLoading, error } = api.version.getVersionStatus.useQuery();
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
@@ -479,9 +479,19 @@ async function isVM(scriptId: number, containerId: string, serverId: number | nu
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for batch container type detection – avoids repeated SSH calls
|
||||
const containerTypeCacheByServer = new Map<number, { data: Map<string, boolean>; expiry: number }>();
|
||||
const CONTAINER_TYPE_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Helper function to batch detect container types for all containers on a server
|
||||
// Returns a Map of container_id -> isVM (true for VM, false for LXC)
|
||||
async function batchDetectContainerTypes(server: Server): Promise<Map<string, boolean>> {
|
||||
// Check cache first
|
||||
const cached = containerTypeCacheByServer.get(server.id);
|
||||
if (cached && Date.now() < cached.expiry) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const containerTypeMap = new Map<string, boolean>();
|
||||
|
||||
try {
|
||||
@@ -580,6 +590,9 @@ async function batchDetectContainerTypes(server: Server): Promise<Map<string, bo
|
||||
// Return empty map on error - individual checks will fall back to isVM()
|
||||
}
|
||||
|
||||
// Store in cache
|
||||
containerTypeCacheByServer.set(server.id, { data: containerTypeMap, expiry: Date.now() + CONTAINER_TYPE_CACHE_TTL });
|
||||
|
||||
return containerTypeMap;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ function pbCardToScriptCard(pb: PBScriptCard): ScriptCard {
|
||||
website: pb.website,
|
||||
categoryNames: pb.categories.map((c) => c.name),
|
||||
date_created: pb.script_created,
|
||||
date_updated: pb.script_updated,
|
||||
interface_port: pb.port,
|
||||
is_dev: pb.is_dev,
|
||||
is_disabled: pb.is_disabled,
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface ScriptCard {
|
||||
/** Category names for display / filtering. */
|
||||
categoryNames?: string[];
|
||||
date_created?: string;
|
||||
date_updated?: string;
|
||||
os?: string;
|
||||
version?: string;
|
||||
interface_port?: number | null;
|
||||
|
||||
Reference in New Issue
Block a user