Files
ocdp/frontend/other.html
jackyliu c7f8e69d61 feat: Add orchestration models and services for Kubernetes cluster management
- Implemented Pydantic models for Kubernetes cluster state representation in `cluster.py`.
- Created a `Resource` class for converting JSON/dict to Python objects in `resource.py`.
- Established user models and services for user management, including password hashing and JWT token generation.
- Developed application orchestration services for managing Kubernetes applications, including installation and uninstallation.
- Added cluster service for retrieving cluster status and health reports.
- Introduced node service for fetching node resource details and health status.
- Implemented user service for handling user authentication and management.
2025-09-02 02:54:26 +00:00

406 lines
20 KiB
HTML

<!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>