mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2026-05-08 07:19:49 -05:00
Improved Agent version checking logic and page with ability to download the binaries from the REPO again
This commit is contained in:
@@ -1,387 +1,282 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { AlertCircle, Code, Download, Plus, Shield, X } from "lucide-react";
|
||||
import { useId, useState } from "react";
|
||||
import { agentFileAPI, settingsAPI } from "../../utils/api";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AlertCircle, CheckCircle, Clock, RefreshCw } from "lucide-react";
|
||||
import api from "../../utils/api";
|
||||
|
||||
const AgentManagementTab = () => {
|
||||
const scriptFileId = useId();
|
||||
const scriptContentId = useId();
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const _queryClient = useQueryClient();
|
||||
|
||||
// Agent file queries and mutations
|
||||
// Agent version queries
|
||||
const {
|
||||
data: agentFileInfo,
|
||||
isLoading: agentFileLoading,
|
||||
error: agentFileError,
|
||||
refetch: refetchAgentFile,
|
||||
data: versionInfo,
|
||||
isLoading: versionLoading,
|
||||
error: versionError,
|
||||
refetch: refetchVersion,
|
||||
} = useQuery({
|
||||
queryKey: ["agentFile"],
|
||||
queryFn: () => agentFileAPI.getInfo().then((res) => res.data),
|
||||
queryKey: ["agentVersion"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await api.get("/agent/version");
|
||||
console.log("🔍 Frontend received version info:", response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch version info:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes
|
||||
enabled: true, // Always enabled
|
||||
retry: 3, // Retry failed requests
|
||||
});
|
||||
|
||||
// Fetch settings for dynamic curl flags
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ["settings"],
|
||||
queryFn: () => settingsAPI.get().then((res) => res.data),
|
||||
const {
|
||||
data: _availableVersions,
|
||||
isLoading: _versionsLoading,
|
||||
error: _versionsError,
|
||||
} = useQuery({
|
||||
queryKey: ["agentVersions"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await api.get("/agent/versions");
|
||||
console.log("🔍 Frontend received available versions:", response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch available versions:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
enabled: true,
|
||||
retry: 3,
|
||||
});
|
||||
|
||||
// Helper function to get curl flags based on settings
|
||||
const _getCurlFlags = () => {
|
||||
return settings?.ignore_ssl_self_signed ? "-sk" : "-s";
|
||||
};
|
||||
|
||||
const uploadAgentMutation = useMutation({
|
||||
mutationFn: (scriptContent) =>
|
||||
agentFileAPI.upload(scriptContent).then((res) => res.data),
|
||||
const checkUpdatesMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
// First check GitHub for updates
|
||||
await api.post("/agent/version/check");
|
||||
// Then refresh current agent version detection
|
||||
await api.post("/agent/version/refresh");
|
||||
},
|
||||
onSuccess: () => {
|
||||
refetchAgentFile();
|
||||
setShowUploadModal(false);
|
||||
refetchVersion();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Upload agent error:", error);
|
||||
console.error("Check updates error:", error);
|
||||
},
|
||||
});
|
||||
|
||||
const downloadUpdateMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
// Download the latest binaries
|
||||
const downloadResult = await api.post("/agent/version/download");
|
||||
// Refresh current agent version detection after download
|
||||
await api.post("/agent/version/refresh");
|
||||
// Return the download result for success handling
|
||||
return downloadResult;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
console.log("Download completed:", data);
|
||||
console.log("Download response data:", data.data);
|
||||
refetchVersion();
|
||||
// Show success message
|
||||
const message =
|
||||
data.data?.message || "Agent binaries downloaded successfully";
|
||||
alert(`✅ ${message}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Download update error:", error);
|
||||
alert(`❌ Download failed: ${error.message}`);
|
||||
},
|
||||
});
|
||||
|
||||
const getVersionStatus = () => {
|
||||
console.log("🔍 getVersionStatus called with:", {
|
||||
versionError,
|
||||
versionInfo,
|
||||
versionLoading,
|
||||
});
|
||||
|
||||
if (versionError) {
|
||||
console.log("❌ Version error detected:", versionError);
|
||||
return {
|
||||
status: "error",
|
||||
message: "Failed to load version info",
|
||||
Icon: AlertCircle,
|
||||
color: "text-red-600",
|
||||
};
|
||||
}
|
||||
|
||||
if (!versionInfo || versionLoading) {
|
||||
console.log("⏳ Loading state:", { versionInfo, versionLoading });
|
||||
return {
|
||||
status: "loading",
|
||||
message: "Loading version info...",
|
||||
Icon: RefreshCw,
|
||||
color: "text-gray-600",
|
||||
};
|
||||
}
|
||||
|
||||
// Use the backend's updateStatus for proper semver comparison
|
||||
switch (versionInfo.updateStatus) {
|
||||
case "update-available":
|
||||
return {
|
||||
status: "update-available",
|
||||
message: `Update available: ${versionInfo.latestVersion}`,
|
||||
Icon: Clock,
|
||||
color: "text-yellow-600",
|
||||
};
|
||||
case "newer-version":
|
||||
return {
|
||||
status: "newer-version",
|
||||
message: `Newer version running: ${versionInfo.currentVersion}`,
|
||||
Icon: CheckCircle,
|
||||
color: "text-blue-600",
|
||||
};
|
||||
case "up-to-date":
|
||||
return {
|
||||
status: "up-to-date",
|
||||
message: `Up to date: ${versionInfo.latestVersion}`,
|
||||
Icon: CheckCircle,
|
||||
color: "text-green-600",
|
||||
};
|
||||
case "no-agent":
|
||||
return {
|
||||
status: "no-agent",
|
||||
message: "No agent binary found",
|
||||
Icon: AlertCircle,
|
||||
color: "text-orange-600",
|
||||
};
|
||||
case "github-unavailable":
|
||||
return {
|
||||
status: "github-unavailable",
|
||||
message: `Agent running: ${versionInfo.currentVersion} (GitHub API unavailable)`,
|
||||
Icon: CheckCircle,
|
||||
color: "text-purple-600",
|
||||
};
|
||||
case "no-data":
|
||||
return {
|
||||
status: "no-data",
|
||||
message: "No version data available",
|
||||
Icon: AlertCircle,
|
||||
color: "text-gray-600",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
status: "unknown",
|
||||
message: "Version status unknown",
|
||||
Icon: AlertCircle,
|
||||
color: "text-gray-600",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const versionStatus = getVersionStatus();
|
||||
const StatusIcon = versionStatus.Icon;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<Code className="h-6 w-6 text-primary-600 mr-3" />
|
||||
<h2 className="text-xl font-semibold text-secondary-900 dark:text-white">
|
||||
Agent File Management
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-secondary-500 dark:text-secondary-300">
|
||||
Manage the PatchMon agent script file used for installations and
|
||||
updates
|
||||
<h2 className="text-2xl font-bold text-secondary-900 dark:text-white">
|
||||
Agent Version Management
|
||||
</h2>
|
||||
<p className="text-secondary-600 dark:text-secondary-300">
|
||||
Monitor agent versions and download updates
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const url = "/api/v1/hosts/agent/download";
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "patchmon-agent.sh";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}}
|
||||
className="btn-outline flex items-center gap-2"
|
||||
onClick={() => checkUpdatesMutation.mutate()}
|
||||
disabled={checkUpdatesMutation.isPending}
|
||||
className="flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowUploadModal(true)}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Replace Script
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 mr-2 ${checkUpdatesMutation.isPending ? "animate-spin" : ""}`}
|
||||
/>
|
||||
Check Updates
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{agentFileLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
) : agentFileError ? (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-red-600 dark:text-red-400">
|
||||
Error loading agent file: {agentFileError.message}
|
||||
</p>
|
||||
</div>
|
||||
) : !agentFileInfo?.exists ? (
|
||||
<div className="text-center py-8">
|
||||
<Code className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
|
||||
<p className="text-secondary-500 dark:text-secondary-300">
|
||||
No agent script found
|
||||
</p>
|
||||
<p className="text-sm text-secondary-400 dark:text-secondary-400 mt-2">
|
||||
Upload an agent script to get started
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Agent File Info */}
|
||||
<div className="bg-secondary-50 dark:bg-secondary-700 rounded-lg p-6">
|
||||
<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">
|
||||
Current Agent Script
|
||||
{/* Download Updates Button */}
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow p-6 border border-secondary-200 dark:border-secondary-600">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
||||
{versionInfo?.currentVersion
|
||||
? "Download Agent Updates"
|
||||
: "Download Agent Binaries"}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
||||
Version:
|
||||
</span>
|
||||
<span className="text-sm text-secondary-900 dark:text-white font-mono">
|
||||
{agentFileInfo.version}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Download className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
||||
Size:
|
||||
</span>
|
||||
<span className="text-sm text-secondary-900 dark:text-white">
|
||||
{agentFileInfo.sizeFormatted}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />
|
||||
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">
|
||||
Modified:
|
||||
</span>
|
||||
<span className="text-sm text-secondary-900 dark:text-white">
|
||||
{new Date(agentFileInfo.lastModified).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Instructions */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<Shield className="h-5 w-5 text-blue-400 dark:text-blue-300" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
Agent Script Usage
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
||||
<p className="mb-2">This script is used for:</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>New agent installations via the install script</li>
|
||||
<li>
|
||||
Agent downloads from the /api/v1/hosts/agent/download
|
||||
endpoint
|
||||
</li>
|
||||
<li>Manual agent deployments and updates</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Uninstall Instructions */}
|
||||
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<Shield className="h-5 w-5 text-red-400 dark:text-red-300" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
|
||||
Agent Uninstall Command
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||
<p className="mb-3">
|
||||
To completely remove PatchMon from a host:
|
||||
</p>
|
||||
|
||||
{/* Go Agent Uninstall */}
|
||||
<div className="mb-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-red-100 dark:bg-red-800 rounded p-2 font-mono text-xs flex-1">
|
||||
sudo patchmon-agent uninstall
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
"sudo patchmon-agent uninstall",
|
||||
);
|
||||
}}
|
||||
className="px-2 py-1 bg-red-200 dark:bg-red-700 text-red-800 dark:text-red-200 rounded text-xs hover:bg-red-300 dark:hover:bg-red-600 transition-colors"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs text-red-600 dark:text-red-400">
|
||||
Options: <code>--remove-config</code>,{" "}
|
||||
<code>--remove-logs</code>, <code>--remove-all</code>,{" "}
|
||||
<code>--force</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-xs">
|
||||
⚠️ This command will remove all PatchMon files,
|
||||
configuration, and crontab entries
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-secondary-600 dark:text-secondary-300">
|
||||
{versionInfo?.currentVersion
|
||||
? "Download the latest agent binaries from GitHub"
|
||||
: "No agent binaries found. Download from GitHub to get started."}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadUpdateMutation.mutate()}
|
||||
disabled={downloadUpdateMutation.isPending}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 mr-2 ${downloadUpdateMutation.isPending ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{downloadUpdateMutation.isPending
|
||||
? "Downloading..."
|
||||
: versionInfo?.currentVersion
|
||||
? "Download Updates"
|
||||
: "Download Agent Binaries"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Agent Upload Modal */}
|
||||
{showUploadModal && (
|
||||
<AgentUploadModal
|
||||
isOpen={showUploadModal}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
onSubmit={uploadAgentMutation.mutate}
|
||||
isLoading={uploadAgentMutation.isPending}
|
||||
error={uploadAgentMutation.error}
|
||||
scriptFileId={scriptFileId}
|
||||
scriptContentId={scriptContentId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Agent Upload Modal Component
|
||||
const AgentUploadModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
error,
|
||||
scriptFileId,
|
||||
scriptContentId,
|
||||
}) => {
|
||||
const [scriptContent, setScriptContent] = useState("");
|
||||
const [uploadError, setUploadError] = useState("");
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setUploadError("");
|
||||
|
||||
if (!scriptContent.trim()) {
|
||||
setUploadError("Script content is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scriptContent.trim().startsWith("#!/")) {
|
||||
setUploadError(
|
||||
"Script must start with a shebang (#!/bin/bash or #!/bin/sh)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(scriptContent);
|
||||
};
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
setScriptContent(event.target.result);
|
||||
setUploadError("");
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="px-6 py-4 border-b border-secondary-200 dark:border-secondary-600">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
|
||||
Replace Agent Script
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="px-6 py-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor={scriptFileId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
|
||||
>
|
||||
Upload Script File
|
||||
</label>
|
||||
<input
|
||||
id={scriptFileId}
|
||||
type="file"
|
||||
accept=".sh"
|
||||
onChange={handleFileUpload}
|
||||
className="block w-full text-sm text-secondary-500 dark:text-secondary-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 dark:file:bg-primary-900 dark:file:text-primary-200"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||
Select a .sh file to upload, or paste the script content below
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor={scriptContentId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
|
||||
>
|
||||
Script Content *
|
||||
</label>
|
||||
<textarea
|
||||
id={scriptContentId}
|
||||
value={scriptContent}
|
||||
onChange={(e) => {
|
||||
setScriptContent(e.target.value);
|
||||
setUploadError("");
|
||||
}}
|
||||
rows={15}
|
||||
className="block w-full border border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white font-mono text-sm"
|
||||
placeholder="#!/bin/bash # PatchMon Agent Script VERSION="1.0.0" # Your script content here..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(uploadError || error) && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-3">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">
|
||||
{uploadError ||
|
||||
error?.response?.data?.error ||
|
||||
error?.message}
|
||||
</p>
|
||||
</div>
|
||||
{/* Version Status Card */}
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg shadow p-6 border border-secondary-200 dark:border-secondary-600">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
||||
Agent Version Status
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
{StatusIcon && (
|
||||
<StatusIcon className={`h-5 w-5 ${versionStatus.color}`} />
|
||||
)}
|
||||
<span className={`text-sm font-medium ${versionStatus.color}`}>
|
||||
{versionStatus.message}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3">
|
||||
<div className="flex">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-600 dark:text-yellow-400 mr-2 mt-0.5" />
|
||||
<div className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<p className="font-medium">Important:</p>
|
||||
<ul className="mt-1 list-disc list-inside space-y-1">
|
||||
<li>This will replace the current agent script file</li>
|
||||
<li>A backup will be created automatically</li>
|
||||
<li>All new installations will use this script</li>
|
||||
<li>
|
||||
Existing agents will download this version on their next
|
||||
update
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{versionInfo && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-secondary-500 dark:text-secondary-400">
|
||||
Current Version:
|
||||
</span>
|
||||
<span className="ml-2 font-medium text-secondary-900 dark:text-white">
|
||||
{versionInfo.currentVersion || "Unknown"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-secondary-500 dark:text-secondary-400">
|
||||
Latest Version:
|
||||
</span>
|
||||
<span className="ml-2 font-medium text-secondary-900 dark:text-white">
|
||||
{versionInfo.latestVersion || "Unknown"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-secondary-500 dark:text-secondary-400">
|
||||
Last Checked:
|
||||
</span>
|
||||
<span className="ml-2 font-medium text-secondary-900 dark:text-white">
|
||||
{versionInfo.lastChecked
|
||||
? new Date(versionInfo.lastChecked).toLocaleString()
|
||||
: "Never"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<button type="button" onClick={onClose} className="btn-outline">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !scriptContent.trim()}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isLoading ? "Uploading..." : "Replace Script"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user