feat: Add Cluster Status Dashboard and remove old Application Manager page
- Introduced a new `monitor.html` file for the Cluster Status Dashboard featuring real-time cluster health and resource allocation visualization using Chart.js and Tailwind CSS. - Removed the outdated `other.html` file which contained the previous Application Manager interface.
This commit is contained in:
482
frontend/applications.html
Normal file
482
frontend/applications.html
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OCDP Application Manager</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<!-- Lucide icons for UI elements -->
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1a202c 0%, #2d3748 50%, #4a5568 100%);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientBG 15s ease infinite;
|
||||||
|
}
|
||||||
|
@keyframes gradientBG {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: rgba(31, 41, 55, 0.5);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px) scale(1.01);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.tab-button.active {
|
||||||
|
background: rgba(55, 65, 81, 0.5);
|
||||||
|
color: #93c5fd;
|
||||||
|
border-bottom: 2px solid #3b82f6;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background: rgba(31, 41, 55, 0.5);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-900 text-gray-200">
|
||||||
|
|
||||||
|
<!-- Main Application Manager -->
|
||||||
|
<div id="mainAppPage" class="container mx-auto p-4 md:p-8">
|
||||||
|
<header class="mb-8 flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<i data-lucide="layout-dashboard" class="w-10 h-10 text-blue-400"></i>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-white">Application Manager</h1>
|
||||||
|
<p class="text-gray-400">Manage and deploy applications on your cluster.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full sm:w-auto mt-4 sm:mt-0">
|
||||||
|
<input type="text" id="tokenInput" placeholder="Enter JWT Token" class="w-full sm:w-80 bg-gray-900/50 text-white border border-gray-600 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition">
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="flex border-b border-gray-700/50 mb-6">
|
||||||
|
<button id="tabAvailable" class="tab-button px-6 py-3 font-semibold text-gray-400 hover:text-white transition active">Available Applications</button>
|
||||||
|
<button id="tabInstalled" class="tab-button px-6 py-3 font-semibold text-gray-400 hover:text-white transition">Installed Applications</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="contentAvailable" class="tab-content">
|
||||||
|
<!-- Loading indicator for available apps -->
|
||||||
|
<p id="availableLoading" class="text-center text-blue-400 hidden">Loading available applications...</p>
|
||||||
|
<div id="availableAppsList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"></div>
|
||||||
|
</div>
|
||||||
|
<div id="contentInstalled" class="tab-content hidden">
|
||||||
|
<!-- Loading indicator for installed apps -->
|
||||||
|
<p id="installedLoading" class="text-center text-blue-400 hidden">Loading installed applications...</p>
|
||||||
|
<div id="installedAppsList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
|
<div id="installModal" class="fixed inset-0 modal hidden justify-center items-center">
|
||||||
|
<div class="card p-8 w-full max-w-lg">
|
||||||
|
<h3 class="text-2xl font-bold text-white mb-4">Install Application</h3>
|
||||||
|
<p id="modalAppName" class="text-gray-400 mb-4"></p>
|
||||||
|
<form id="installForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="installMode" class="block text-sm font-medium text-gray-300">Deployment Mode</label>
|
||||||
|
<select id="installMode" class="mt-1 block w-full bg-gray-900/50 border border-gray-600 rounded-md shadow-sm p-2 text-white"></select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="userOverrides" class="block text-sm font-medium text-gray-300">User Overrides (JSON)</label>
|
||||||
|
<textarea id="userOverrides" rows="5" class="mt-1 block w-full bg-gray-900/50 border border-gray-600 rounded-md shadow-sm p-2 text-white"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3 pt-4">
|
||||||
|
<button type="button" id="cancelInstallBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg transition">Cancel</button>
|
||||||
|
<button type="submit" id="submitInstallBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition flex items-center justify-center gap-2">
|
||||||
|
<i data-lucide="plus-circle" class="w-4 h-4"></i> Install
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="statusModal" class="fixed inset-0 modal hidden justify-center items-center">
|
||||||
|
<div class="card p-8 w-full max-w-2xl">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-2xl font-bold text-white">Application Status</h3>
|
||||||
|
<button id="closeStatusModal" class="text-gray-400 hover:text-white"><i data-lucide="x" class="w-6 h-6"></i></button>
|
||||||
|
</div>
|
||||||
|
<div id="statusContent" class="space-y-4"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="resultModal" class="fixed inset-0 modal hidden justify-center items-center">
|
||||||
|
<div class="card p-8 w-full max-w-2xl">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 id="resultModalTitle" class="text-2xl font-bold text-white"></h3>
|
||||||
|
<button onclick="document.getElementById('resultModal').classList.add('hidden'); lucide.createIcons();" class="text-gray-400 hover:text-white">
|
||||||
|
<i data-lucide="x" class="w-6 h-6"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre id="resultModalContent" class="bg-gray-900/50 border border-gray-600 rounded-lg p-4 text-sm whitespace-pre-wrap max-h-96 overflow-y-auto"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Constants for API paths
|
||||||
|
const API_ORCHESTRATION_PATH = "/api/v1/orchestration";
|
||||||
|
|
||||||
|
// Global variables
|
||||||
|
let API_BASE_URL = "http://10.6.14.233:8000";
|
||||||
|
let TOKEN = "";
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const tokenInput = document.getElementById('tokenInput');
|
||||||
|
const tabAvailable = document.getElementById('tabAvailable');
|
||||||
|
const tabInstalled = document.getElementById('tabInstalled');
|
||||||
|
const contentAvailable = document.getElementById('contentAvailable');
|
||||||
|
const contentInstalled = document.getElementById('contentInstalled');
|
||||||
|
const availableAppsList = document.getElementById('availableAppsList');
|
||||||
|
const installedAppsList = document.getElementById('installedAppsList');
|
||||||
|
const availableLoading = document.getElementById('availableLoading');
|
||||||
|
const installedLoading = document.getElementById('installedLoading');
|
||||||
|
const installModal = document.getElementById('installModal');
|
||||||
|
const statusModal = document.getElementById('statusModal');
|
||||||
|
const closeStatusModal = document.getElementById('closeStatusModal');
|
||||||
|
const installForm = document.getElementById('installForm');
|
||||||
|
const resultModal = document.getElementById('resultModal');
|
||||||
|
|
||||||
|
// --- Core Logic ---
|
||||||
|
async function fetchAvailableApps() {
|
||||||
|
// Show loading indicator
|
||||||
|
availableLoading.classList.remove('hidden');
|
||||||
|
availableAppsList.innerHTML = '';
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${API_ORCHESTRATION_PATH}/application-templates`);
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
const apps = await response.json();
|
||||||
|
renderAvailableApps(apps);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching available apps:", error);
|
||||||
|
let errorMessage = `Failed to load data: ${error.message}`;
|
||||||
|
if (error instanceof TypeError && error.message === 'Failed to fetch') {
|
||||||
|
errorMessage = 'Network Error: Failed to fetch. Please ensure your API server is running and has CORS enabled.';
|
||||||
|
}
|
||||||
|
availableAppsList.innerHTML = `<p class="text-center text-red-400">${errorMessage}</p>`;
|
||||||
|
} finally {
|
||||||
|
// Hide loading indicator
|
||||||
|
availableLoading.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchInstalledApps() {
|
||||||
|
if (!TOKEN) {
|
||||||
|
installedAppsList.innerHTML = `<p class="text-center text-yellow-400 col-span-full">Please enter a valid JWT token to view installed applications.</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Show loading indicator
|
||||||
|
installedLoading.classList.remove('hidden');
|
||||||
|
installedAppsList.innerHTML = '';
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${API_ORCHESTRATION_PATH}/application-instances`, {
|
||||||
|
headers: { "Authorization": `Bearer ${TOKEN}` }
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
const apps = await response.json();
|
||||||
|
renderInstalledApps(apps);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching installed apps:", error);
|
||||||
|
let errorMessage = `Failed to load data: ${error.message}`;
|
||||||
|
if (error instanceof TypeError && error.message === 'Failed to fetch') {
|
||||||
|
errorMessage = 'Network Error: Failed to fetch. Please ensure your API server is running and has CORS enabled.';
|
||||||
|
}
|
||||||
|
installedAppsList.innerHTML = `<p class="text-center text-red-400">${errorMessage}</p>`;
|
||||||
|
} finally {
|
||||||
|
// Hide loading indicator
|
||||||
|
installedLoading.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAndRenderStatus(namespace, app_template_name, mode) {
|
||||||
|
if (!TOKEN) return;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${API_ORCHESTRATION_PATH}/application-instances/${namespace}/${app_template_name}/status?mode=${mode}`, {
|
||||||
|
headers: { "Authorization": `Bearer ${TOKEN}` }
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch status.");
|
||||||
|
const statusData = await response.json();
|
||||||
|
renderStatusDetails(statusData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching status:", error);
|
||||||
|
let errorMessage = `Failed to load data: ${error.message}`;
|
||||||
|
if (error instanceof TypeError && error.message === 'Failed to fetch') {
|
||||||
|
errorMessage = 'Network Error: Failed to fetch. Please ensure your API server is running and has CORS enabled.';
|
||||||
|
}
|
||||||
|
document.getElementById('statusContent').innerHTML = `<p class="text-red-400">${errorMessage}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uninstallRelease(namespace, app_template_name, mode) {
|
||||||
|
// Use a custom modal instead of alert/confirm
|
||||||
|
if (!confirm(`Are you sure you want to uninstall the Helm release for ${app_template_name}?`)) return;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${API_ORCHESTRATION_PATH}/application-instances/${namespace}/${app_template_name}?mode=${mode}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { "Authorization": `Bearer ${TOKEN}` }
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!response.ok) throw new Error(JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
|
showResultModal("Uninstall Successful!", JSON.stringify(result, null, 2), "success");
|
||||||
|
fetchInstalledApps();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uninstalling release:", error);
|
||||||
|
showResultModal("Uninstall Failed!", error.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteNamespace(namespace) {
|
||||||
|
// Use a custom modal instead of alert/confirm
|
||||||
|
if (!confirm(`WARNING: This will permanently delete the entire namespace '${namespace}' and all its resources.'`)) return;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${API_ORCHESTRATION_PATH}/application-instances/${namespace}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { "Authorization": `Bearer ${TOKEN}` }
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!response.ok) throw new Error(JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
|
showResultModal("Delete Successful!", JSON.stringify(result, null, 2), "success");
|
||||||
|
fetchInstalledApps();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting namespace:", error);
|
||||||
|
showResultModal("Delete Failed!", error.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Render Functions ---
|
||||||
|
function renderAvailableApps(apps) {
|
||||||
|
availableAppsList.innerHTML = apps.map(app => `
|
||||||
|
<div class="card p-6 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white mb-2">${app.name}</h3>
|
||||||
|
<p class="text-sm text-gray-400">Business Name: ${app.metadata.application_name}</p>
|
||||||
|
<p class="text-sm text-gray-400">Chart: ${app.metadata.distributed.chart}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<button onclick="openInstallModal('${app.name}', '${encodeURIComponent(JSON.stringify(app.metadata))}')" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center gap-2 transition">
|
||||||
|
<i data-lucide="plus-circle" class="w-4 h-4"></i> Install
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInstalledApps(apps) {
|
||||||
|
installedAppsList.innerHTML = apps.map(app => `
|
||||||
|
<div class="card p-6 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white mb-2">${app.application_name}</h3>
|
||||||
|
<p class="text-sm text-gray-400">Namespace: <span class="text-blue-300 font-semibold">${app.namespace}</span></p>
|
||||||
|
<p class="text-sm text-gray-400">Release: ${app.release_name}</p>
|
||||||
|
<p class="text-sm text-gray-400">Status: <span class="font-semibold ${app.status === 'deployed' ? 'text-green-300' : 'text-yellow-300'}">${app.status}</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex gap-2">
|
||||||
|
<button onclick="viewStatusDetails('${app.namespace}', '${app.application_name}', 'distributed')" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-2 rounded-lg transition text-sm">
|
||||||
|
View Status
|
||||||
|
</button>
|
||||||
|
${app.status === 'deployed' ?
|
||||||
|
`<button onclick="uninstallRelease('${app.namespace}', '${app.application_name}', 'distributed')" class="flex-1 bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-2 rounded-lg transition text-sm">
|
||||||
|
Uninstall
|
||||||
|
</button>`
|
||||||
|
:
|
||||||
|
`<button onclick="deleteNamespace('${app.namespace}')" class="flex-1 bg-red-800 hover:bg-red-900 text-white font-bold py-2 px-2 rounded-lg transition text-sm">
|
||||||
|
Delete Namespace
|
||||||
|
</button>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatusDetails(statusData) {
|
||||||
|
const content = document.getElementById('statusContent');
|
||||||
|
content.innerHTML = `
|
||||||
|
<p class="text-xl font-bold text-white">Application: ${statusData.application_name}</p>
|
||||||
|
<p class="text-gray-400">Namespace: ${statusData.namespace}</p>
|
||||||
|
<p class="text-gray-400">Ready: <span class="font-semibold ${statusData.is_ready ? 'text-green-300' : 'text-yellow-300'}">${statusData.is_ready}</span></p>
|
||||||
|
${statusData.base_access_url ? `<p class="text-gray-400">Access URL: <a href="${statusData.base_access_url}" target="_blank" class="text-blue-400 hover:underline">${statusData.base_access_url}</a></p>` : ''}
|
||||||
|
<h4 class="font-semibold text-white mt-4 mb-2">Pod Details</h4>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
${statusData.details.map(pod => `
|
||||||
|
<li class="bg-gray-800/50 p-3 rounded-lg flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="font-bold text-white">${pod.pod_name}</p>
|
||||||
|
<p class="text-sm text-gray-400">Phase: ${pod.status_phase} | Ready: ${pod.ready_status}</p>
|
||||||
|
</div>
|
||||||
|
<span class="w-3 h-3 rounded-full ${pod.is_ready ? 'bg-green-400' : 'bg-red-400'}"></span>
|
||||||
|
</li>
|
||||||
|
`).join('')}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
statusModal.classList.remove('hidden');
|
||||||
|
statusModal.classList.add('flex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Event Handlers & Modal Functions ---
|
||||||
|
function openInstallModal(appName, metadataString) {
|
||||||
|
const metadata = JSON.parse(decodeURIComponent(metadataString));
|
||||||
|
const modalTitle = document.getElementById('modalAppName');
|
||||||
|
modalTitle.innerText = `Installing ${appName}`;
|
||||||
|
|
||||||
|
const modeSelect = document.getElementById('installMode');
|
||||||
|
modeSelect.innerHTML = '';
|
||||||
|
const modes = [
|
||||||
|
{ name: 'distributed', data: metadata.distributed },
|
||||||
|
{ name: 'monolithic', data: metadata.monolithic }
|
||||||
|
];
|
||||||
|
|
||||||
|
modes.forEach(mode => {
|
||||||
|
if (mode.data) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = mode.name;
|
||||||
|
option.innerText = mode.name;
|
||||||
|
modeSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
installModal.classList.remove('hidden');
|
||||||
|
installModal.classList.add('flex');
|
||||||
|
installForm.dataset.appName = appName;
|
||||||
|
}
|
||||||
|
|
||||||
|
installForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const appName = installForm.dataset.appName;
|
||||||
|
const mode = document.getElementById('installMode').value;
|
||||||
|
const userOverridesText = document.getElementById('userOverrides').value;
|
||||||
|
let userOverrides = {};
|
||||||
|
if (userOverridesText) {
|
||||||
|
try {
|
||||||
|
userOverrides = JSON.parse(userOverridesText);
|
||||||
|
} catch (error) {
|
||||||
|
// Use a custom modal instead of alert
|
||||||
|
showResultModal("Invalid JSON for user overrides.", "Please ensure your JSON is correctly formatted.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitButton = document.getElementById('submitInstallBtn');
|
||||||
|
const originalButtonText = submitButton.innerHTML;
|
||||||
|
|
||||||
|
submitButton.innerHTML = `<i data-lucide="loader-2" class="animate-spin w-4 h-4"></i> Installing...`;
|
||||||
|
submitButton.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${API_ORCHESTRATION_PATH}/application-instances`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${TOKEN}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
app_template_name: appName,
|
||||||
|
mode: mode,
|
||||||
|
user_overrides: userOverrides
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(JSON.stringify(result, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
showResultModal("Installation Successful!", JSON.stringify(result, null, 2), "success");
|
||||||
|
installModal.classList.remove('flex');
|
||||||
|
installModal.classList.add('hidden');
|
||||||
|
fetchInstalledApps();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Installation error:", error);
|
||||||
|
showResultModal("Installation Failed!", error.message, "error");
|
||||||
|
} finally {
|
||||||
|
submitButton.innerHTML = originalButtonText;
|
||||||
|
submitButton.disabled = false;
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('cancelInstallBtn').addEventListener('click', () => {
|
||||||
|
installModal.classList.remove('flex');
|
||||||
|
installModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
closeStatusModal.addEventListener('click', () => {
|
||||||
|
statusModal.classList.remove('flex');
|
||||||
|
statusModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
function showResultModal(title, message, type) {
|
||||||
|
const resultTitle = document.getElementById('resultModalTitle');
|
||||||
|
const resultContent = document.getElementById('resultModalContent');
|
||||||
|
|
||||||
|
resultTitle.innerText = title;
|
||||||
|
resultTitle.className = `text-2xl font-bold ${type === 'success' ? 'text-green-400' : 'text-red-400'}`;
|
||||||
|
resultContent.innerText = message;
|
||||||
|
|
||||||
|
resultModal.classList.remove('hidden');
|
||||||
|
resultModal.classList.add('flex');
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewStatusDetails(namespace, app_template_name, mode) {
|
||||||
|
document.getElementById('statusContent').innerHTML = `<p class="text-center text-blue-400">Loading status...</p>`;
|
||||||
|
fetchAndRenderStatus(namespace, app_template_name, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tabId) {
|
||||||
|
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(content => content.classList.add('hidden'));
|
||||||
|
|
||||||
|
if (tabId === 'tabAvailable') {
|
||||||
|
tabAvailable.classList.add('active');
|
||||||
|
contentAvailable.classList.remove('hidden');
|
||||||
|
fetchAvailableApps();
|
||||||
|
} else if (tabId === 'tabInstalled') {
|
||||||
|
tabInstalled.classList.add('active');
|
||||||
|
contentInstalled.classList.remove('hidden');
|
||||||
|
fetchInstalledApps();
|
||||||
|
}
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
tabAvailable.addEventListener('click', () => switchTab('tabAvailable'));
|
||||||
|
tabInstalled.addEventListener('click', () => switchTab('tabInstalled'));
|
||||||
|
|
||||||
|
// --- Added: Real-time token update on input ---
|
||||||
|
tokenInput.addEventListener('input', (e) => {
|
||||||
|
TOKEN = e.target.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial page load check
|
||||||
|
window.onload = () => {
|
||||||
|
// The API_BASE_URL is now a default value and can be manually changed.
|
||||||
|
// We'll also remove the token from localStorage for a clean start.
|
||||||
|
localStorage.removeItem('jwtToken');
|
||||||
|
localStorage.removeItem('apiBaseUrl');
|
||||||
|
|
||||||
|
switchTab('tabAvailable'); // Default to the "Available Applications" tab
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
133
frontend/login.html
Normal file
133
frontend/login.html
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>User Login</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<style>
|
||||||
|
/* Custom styles for glassmorphism and animations */
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1a202c 0%, #2d3748 50%, #4a5568 100%);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientBG 15s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientBG {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: rgba(31, 41, 55, 0.5);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="flex items-center justify-center min-h-screen bg-gray-900 text-gray-200">
|
||||||
|
<div class="container mx-auto p-4 md:p-8">
|
||||||
|
<div class="max-w-md mx-auto card p-8">
|
||||||
|
<header class="mb-6 text-center">
|
||||||
|
<i data-lucide="log-in" class="w-12 h-12 text-blue-400 mx-auto mb-2"></i>
|
||||||
|
<h1 class="text-3xl font-bold text-white">Login</h1>
|
||||||
|
<p class="text-gray-400 mt-2">Access your application manager.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form id="loginForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-gray-300">Username</label>
|
||||||
|
<input type="text" id="username" name="username" class="mt-1 block w-full bg-gray-900/50 border border-gray-600 rounded-md shadow-sm p-2 text-white focus:ring-blue-500 focus:border-blue-500" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-gray-300">Password</label>
|
||||||
|
<input type="password" id="password" name="password" class="mt-1 block w-full bg-gray-900/50 border border-gray-600 rounded-md shadow-sm p-2 text-white focus:ring-blue-500 focus:border-blue-500" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center gap-2 transition">
|
||||||
|
<i data-lucide="log-in" class="w-4 h-4"></i> Sign In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="resultContainer" class="mt-6 p-4 rounded-lg hidden">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-2">Login Successful!</h3>
|
||||||
|
<p class="text-gray-400">Your Access Token:</p>
|
||||||
|
<div class="relative mt-2">
|
||||||
|
<textarea id="tokenDisplay" rows="4" class="w-full bg-gray-900/50 text-green-400 border border-gray-600 rounded-md p-2" readonly></textarea>
|
||||||
|
<button id="copyTokenBtn" class="absolute top-2 right-2 p-1 rounded-md bg-gray-700/50 text-gray-400 hover:bg-gray-600/50 transition">
|
||||||
|
<i data-lucide="clipboard" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm mt-2 text-gray-500">This token is valid for a limited time. Use it for authenticated API requests.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorContainer" class="mt-6 p-4 bg-red-800/20 text-red-400 rounded-lg hidden">
|
||||||
|
<p id="errorMessage" class="font-semibold"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_URL = "http://10.6.14.233:8000/api/v1/auth/login";
|
||||||
|
|
||||||
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
const usernameInput = document.getElementById('username');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
const resultContainer = document.getElementById('resultContainer');
|
||||||
|
const tokenDisplay = document.getElementById('tokenDisplay');
|
||||||
|
const copyTokenBtn = document.getElementById('copyTokenBtn');
|
||||||
|
const errorContainer = document.getElementById('errorContainer');
|
||||||
|
const errorMessage = document.getElementById('errorMessage');
|
||||||
|
|
||||||
|
loginForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const username = usernameInput.value;
|
||||||
|
const password = passwordInput.value;
|
||||||
|
|
||||||
|
// Clear previous results and errors
|
||||||
|
resultContainer.classList.add('hidden');
|
||||||
|
errorContainer.classList.add('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(API_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.detail || 'Login failed. Please check your credentials.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = result.access_token;
|
||||||
|
tokenDisplay.value = `Bearer ${token}`;
|
||||||
|
resultContainer.classList.remove('hidden');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login failed:", error);
|
||||||
|
errorMessage.textContent = error.message;
|
||||||
|
errorContainer.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
copyTokenBtn.addEventListener('click', () => {
|
||||||
|
tokenDisplay.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
alert('Token copied to clipboard!');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize icons
|
||||||
|
window.onload = () => {
|
||||||
|
lucide.createIcons();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,406 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>OCDP Application Manager</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<style>
|
|
||||||
/* Custom styles for glassmorphism and animations */
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background: linear-gradient(135deg, #1a202c 0%, #2d3748 50%, #4a5568 100%);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradientBG 15s ease infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradientBG {
|
|
||||||
0% { background-position: 0% 50%; }
|
|
||||||
50% { background-position: 100% 50%; }
|
|
||||||
100% { background-position: 0% 50%; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: rgba(31, 41, 55, 0.5);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
border-radius: 1rem;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.card:hover {
|
|
||||||
transform: translateY(-5px) scale(1.01);
|
|
||||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tabs styling */
|
|
||||||
.tab-button.active {
|
|
||||||
background: rgba(55, 65, 81, 0.5);
|
|
||||||
color: #93c5fd;
|
|
||||||
border-bottom: 2px solid #3b82f6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-900 text-gray-200">
|
|
||||||
<div class="container mx-auto p-4 md:p-8">
|
|
||||||
<header class="mb-8 flex flex-col sm:flex-row justify-between items-center gap-4">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<i data-lucide="layout-dashboard" class="w-10 h-10 text-blue-400"></i>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-3xl font-bold text-white">Application Manager</h1>
|
|
||||||
<p class="text-gray-400">Manage and deploy applications on your cluster.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full sm:w-auto mt-4 sm:mt-0">
|
|
||||||
<input type="text" id="tokenInput" placeholder="Enter JWT Token" class="w-full sm:w-80 bg-gray-900/50 text-white border border-gray-600 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition">
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<div class="flex border-b border-gray-700/50 mb-6">
|
|
||||||
<button id="tabAvailable" class="tab-button px-6 py-3 font-semibold text-gray-400 hover:text-white transition active">Available Applications</button>
|
|
||||||
<button id="tabInstalled" class="tab-button px-6 py-3 font-semibold text-gray-400 hover:text-white transition">Installed Applications</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="contentAvailable" class="tab-content">
|
|
||||||
<div id="availableAppsList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="contentInstalled" class="tab-content hidden">
|
|
||||||
<div id="installedAppsList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="installModal" class="fixed inset-0 bg-gray-900/50 backdrop-blur-md hidden justify-center items-center z-50">
|
|
||||||
<div class="card p-8 w-full max-w-lg">
|
|
||||||
<h3 class="text-2xl font-bold text-white mb-4">Install Application</h3>
|
|
||||||
<p id="modalAppName" class="text-gray-400 mb-4"></p>
|
|
||||||
<form id="installForm" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label for="installMode" class="block text-sm font-medium text-gray-300">Deployment Mode</label>
|
|
||||||
<select id="installMode" class="mt-1 block w-full bg-gray-900/50 border border-gray-600 rounded-md shadow-sm p-2 text-white"></select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="userOverrides" class="block text-sm font-medium text-gray-300">User Overrides (JSON)</label>
|
|
||||||
<textarea id="userOverrides" rows="5" class="mt-1 block w-full bg-gray-900/50 border border-gray-600 rounded-md shadow-sm p-2 text-white"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end gap-3 pt-4">
|
|
||||||
<button type="button" id="cancelInstallBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg transition">Cancel</button>
|
|
||||||
<button type="submit" id="submitInstallBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg transition">Install</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="statusModal" class="fixed inset-0 bg-gray-900/50 backdrop-blur-md hidden justify-center items-center z-50">
|
|
||||||
<div class="card p-8 w-full max-w-2xl">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h3 class="text-2xl font-bold text-white">Application Status</h3>
|
|
||||||
<button id="closeStatusModal" class="text-gray-400 hover:text-white"><i data-lucide="x" class="w-6 h-6"></i></button>
|
|
||||||
</div>
|
|
||||||
<div id="statusContent" class="space-y-4"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const API_PREFIX = "http://localhost:8000/api/v1/orchestration";
|
|
||||||
let TOKEN = "";
|
|
||||||
|
|
||||||
// --- DOM Elements ---
|
|
||||||
const tokenInput = document.getElementById('tokenInput');
|
|
||||||
const tabAvailable = document.getElementById('tabAvailable');
|
|
||||||
const tabInstalled = document.getElementById('tabInstalled');
|
|
||||||
const contentAvailable = document.getElementById('contentAvailable');
|
|
||||||
const contentInstalled = document.getElementById('contentInstalled');
|
|
||||||
const availableAppsList = document.getElementById('availableAppsList');
|
|
||||||
const installedAppsList = document.getElementById('installedAppsList');
|
|
||||||
const installModal = document.getElementById('installModal');
|
|
||||||
const statusModal = document.getElementById('statusModal');
|
|
||||||
const closeStatusModal = document.getElementById('closeStatusModal');
|
|
||||||
const installForm = document.getElementById('installForm');
|
|
||||||
|
|
||||||
let activeRefreshInterval = null;
|
|
||||||
|
|
||||||
// --- Core Logic ---
|
|
||||||
async function fetchAvailableApps() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_PREFIX}/application-templates`);
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch available apps.");
|
|
||||||
const apps = await response.json();
|
|
||||||
renderAvailableApps(apps);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching available apps:", error);
|
|
||||||
availableAppsList.innerHTML = `<p class="text-center text-red-400">Failed to load available applications. Please check your API server.</p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchInstalledApps() {
|
|
||||||
if (!TOKEN) {
|
|
||||||
installedAppsList.innerHTML = `<p class="text-center text-yellow-400 col-span-full">Please enter a valid JWT token to view installed applications.</p>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_PREFIX}/application-instances`, {
|
|
||||||
headers: { "Authorization": `Bearer ${TOKEN}` }
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch installed apps.");
|
|
||||||
const apps = await response.json();
|
|
||||||
renderInstalledApps(apps);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching installed apps:", error);
|
|
||||||
installedAppsList.innerHTML = `<p class="text-center text-red-400">Failed to load installed applications. Token might be invalid or expired.</p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAndRenderStatus(namespace, app_template_name, mode) {
|
|
||||||
if (!TOKEN) return;
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_PREFIX}/application-instances/${namespace}/${app_template_name}/status?mode=${mode}`, {
|
|
||||||
headers: { "Authorization": `Bearer ${TOKEN}` }
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch status.");
|
|
||||||
const statusData = await response.json();
|
|
||||||
renderStatusDetails(statusData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching status:", error);
|
|
||||||
document.getElementById('statusContent').innerHTML = `<p class="text-red-400">Error: ${error.message}</p>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uninstallRelease(namespace, app_template_name, mode) {
|
|
||||||
if (!confirm(`Are you sure you want to uninstall the Helm release for ${app_template_name}?`)) return;
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_PREFIX}/application-instances/${namespace}/${app_template_name}?mode=${mode}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { "Authorization": `Bearer ${TOKEN}` }
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
alert(`Uninstall Release Result:\n${result.message}`);
|
|
||||||
fetchInstalledApps(); // Refresh the list
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error uninstalling release:", error);
|
|
||||||
alert(`Failed to uninstall release: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteNamespace(namespace) {
|
|
||||||
if (!confirm(`WARNING: This will permanently delete the entire namespace '${namespace}' and all its resources.`)) return;
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_PREFIX}/application-instances/${namespace}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { "Authorization": `Bearer ${TOKEN}` }
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
alert(`Delete Namespace Result:\n${result.message}`);
|
|
||||||
fetchInstalledApps(); // Refresh the list
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting namespace:", error);
|
|
||||||
alert(`Failed to delete namespace: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Render Functions ---
|
|
||||||
function renderAvailableApps(apps) {
|
|
||||||
availableAppsList.innerHTML = apps.map(app => `
|
|
||||||
<div class="card p-6 flex flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white mb-2">${app.name}</h3>
|
|
||||||
<p class="text-sm text-gray-400">Business Name: ${app.metadata.application_name}</p>
|
|
||||||
<p class="text-sm text-gray-400">Chart: ${app.metadata.distributed.chart}</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4">
|
|
||||||
<button onclick="openInstallModal('${app.name}', ${JSON.stringify(app.metadata)})" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center gap-2 transition">
|
|
||||||
<i data-lucide="plus-circle" class="w-4 h-4"></i> Install
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
lucide.createIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderInstalledApps(apps) {
|
|
||||||
installedAppsList.innerHTML = apps.map(app => `
|
|
||||||
<div class="card p-6 flex flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white mb-2">${app.application_name}</h3>
|
|
||||||
<p class="text-sm text-gray-400">Namespace: <span class="text-blue-300 font-semibold">${app.namespace}</span></p>
|
|
||||||
<p class="text-sm text-gray-400">Release: ${app.release_name}</p>
|
|
||||||
<p class="text-sm text-gray-400">Status: <span class="font-semibold ${app.status === 'deployed' ? 'text-green-300' : 'text-yellow-300'}">${app.status}</span></p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-4 flex gap-2">
|
|
||||||
<button onclick="viewStatusDetails('${app.namespace}', '${app.application_name}', 'distributed')" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-2 rounded-lg transition text-sm">
|
|
||||||
View Status
|
|
||||||
</button>
|
|
||||||
<button onclick="uninstallRelease('${app.namespace}', '${app.application_name}', 'distributed')" class="flex-1 bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-2 rounded-lg transition text-sm">
|
|
||||||
Uninstall
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
<button onclick="deleteNamespace('${app.namespace}')" class="w-full bg-red-800 hover:bg-red-900 text-white font-bold py-2 px-2 rounded-lg transition text-sm">
|
|
||||||
Delete Namespace
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
lucide.createIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStatusDetails(statusData) {
|
|
||||||
const content = document.getElementById('statusContent');
|
|
||||||
content.innerHTML = `
|
|
||||||
<p class="text-xl font-bold text-white">Application: ${statusData.application_name}</p>
|
|
||||||
<p class="text-gray-400">Namespace: ${statusData.namespace}</p>
|
|
||||||
<p class="text-gray-400">Ready: <span class="font-semibold ${statusData.is_ready ? 'text-green-300' : 'text-yellow-300'}">${statusData.is_ready}</span></p>
|
|
||||||
${statusData.base_access_url ? `<p class="text-gray-400">Access URL: <a href="${statusData.base_access_url}" target="_blank" class="text-blue-400 hover:underline">${statusData.base_access_url}</a></p>` : ''}
|
|
||||||
<h4 class="font-semibold text-white mt-4 mb-2">Pod Details</h4>
|
|
||||||
<ul class="space-y-2">
|
|
||||||
${statusData.details.map(pod => `
|
|
||||||
<li class="bg-gray-800/50 p-3 rounded-lg flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="font-bold text-white">${pod.pod_name}</p>
|
|
||||||
<p class="text-sm text-gray-400">Phase: ${pod.status_phase} | Ready: ${pod.ready_status}</p>
|
|
||||||
</div>
|
|
||||||
<span class="w-3 h-3 rounded-full ${pod.is_ready ? 'bg-green-400' : 'bg-red-400'}"></span>
|
|
||||||
</li>
|
|
||||||
`).join('')}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
statusModal.classList.remove('hidden');
|
|
||||||
statusModal.classList.add('flex');
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Event Handlers & Modal Functions ---
|
|
||||||
function openInstallModal(appName, metadata) {
|
|
||||||
const modalTitle = document.getElementById('modalAppName');
|
|
||||||
modalTitle.innerText = `Installing ${appName}`;
|
|
||||||
|
|
||||||
const modeSelect = document.getElementById('installMode');
|
|
||||||
modeSelect.innerHTML = '';
|
|
||||||
const modes = [
|
|
||||||
{ name: 'distributed', data: metadata.distributed },
|
|
||||||
{ name: 'monolithic', data: metadata.monolithic }
|
|
||||||
];
|
|
||||||
|
|
||||||
modes.forEach(mode => {
|
|
||||||
if (mode.data) {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = mode.name;
|
|
||||||
option.innerText = mode.name;
|
|
||||||
modeSelect.appendChild(option);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
installModal.classList.remove('hidden');
|
|
||||||
installModal.classList.add('flex');
|
|
||||||
installForm.dataset.appName = appName;
|
|
||||||
}
|
|
||||||
|
|
||||||
installForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const appName = installForm.dataset.appName;
|
|
||||||
const mode = document.getElementById('installMode').value;
|
|
||||||
const userOverridesText = document.getElementById('userOverrides').value;
|
|
||||||
let userOverrides = {};
|
|
||||||
if (userOverridesText) {
|
|
||||||
try {
|
|
||||||
userOverrides = JSON.parse(userOverridesText);
|
|
||||||
} catch (error) {
|
|
||||||
alert("Invalid JSON for user overrides.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_PREFIX}/application-instances`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${TOKEN}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
app_template_name: appName,
|
|
||||||
mode: mode,
|
|
||||||
user_overrides: userOverrides
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (!response.ok) throw new Error(result.detail || 'Installation failed.');
|
|
||||||
|
|
||||||
alert(`Installation started successfully:\nNamespace: ${result.namespace}\nMessage: ${result.message}`);
|
|
||||||
installModal.classList.remove('flex');
|
|
||||||
installModal.classList.add('hidden');
|
|
||||||
fetchInstalledApps(); // Refresh installed list
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Installation error:", error);
|
|
||||||
alert(`Installation failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('cancelInstallBtn').addEventListener('click', () => {
|
|
||||||
installModal.classList.remove('flex');
|
|
||||||
installModal.classList.add('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
closeStatusModal.addEventListener('click', () => {
|
|
||||||
statusModal.classList.remove('flex');
|
|
||||||
statusModal.classList.add('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
function viewStatusDetails(namespace, app_template_name, mode) {
|
|
||||||
document.getElementById('statusContent').innerHTML = `<p class="text-center text-blue-400">Loading status...</p>`;
|
|
||||||
fetchAndRenderStatus(namespace, app_template_name, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Tab Switching Logic ---
|
|
||||||
function switchTab(tabId) {
|
|
||||||
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
|
|
||||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.add('hidden'));
|
|
||||||
|
|
||||||
if (tabId === 'tabAvailable') {
|
|
||||||
tabAvailable.classList.add('active');
|
|
||||||
contentAvailable.classList.remove('hidden');
|
|
||||||
fetchAvailableApps();
|
|
||||||
} else if (tabId === 'tabInstalled') {
|
|
||||||
tabInstalled.classList.add('active');
|
|
||||||
contentInstalled.classList.remove('hidden');
|
|
||||||
fetchInstalledApps();
|
|
||||||
}
|
|
||||||
lucide.createIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
tabAvailable.addEventListener('click', () => switchTab('tabAvailable'));
|
|
||||||
tabInstalled.addEventListener('click', () => switchTab('tabInstalled'));
|
|
||||||
|
|
||||||
// --- Initialization ---
|
|
||||||
window.onload = () => {
|
|
||||||
const storedToken = localStorage.getItem('jwtToken');
|
|
||||||
if (storedToken) {
|
|
||||||
tokenInput.value = storedToken;
|
|
||||||
TOKEN = storedToken;
|
|
||||||
switchTab('tabInstalled');
|
|
||||||
} else {
|
|
||||||
switchTab('tabAvailable');
|
|
||||||
}
|
|
||||||
tokenInput.addEventListener('input', (e) => {
|
|
||||||
TOKEN = e.target.value;
|
|
||||||
localStorage.setItem('jwtToken', TOKEN);
|
|
||||||
if (TOKEN) {
|
|
||||||
// Refresh current tab if token is entered
|
|
||||||
const currentTab = document.querySelector('.tab-button.active');
|
|
||||||
if (currentTab) switchTab(currentTab.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
lucide.createIcons();
|
|
||||||
};
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user