feat: multi-cluster management with pool-based load balancing
- Add ClusterPool entity for grouping clusters into named pools - Support 3 deployment modes: manual cluster, pool load-balanced, default fallback - Pool strategies: least-apps (fewest deployed apps) and round-robin - Add pool CRUD API endpoints (admin) and public pool listing - Frontend deploy page: 3-mode cluster selector (Default/Manual/Pool) - Frontend app detail: shows assigned cluster and pool info - Admin pools management page with cluster selection and strategy picker - Application entity extended with poolId field
This commit is contained in:
@@ -4,7 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import api from '@/lib/api';
|
||||
import toast from 'react-hot-toast';
|
||||
import type { Application, Deployment, ResourceUsage } from '@/types';
|
||||
import type { Application, Deployment, ResourceUsage, ClusterPublic, ClusterPoolPublic } from '@/types';
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
@@ -85,6 +85,16 @@ export default function AppDetailPage() {
|
||||
refetchInterval: showResources ? 5000 : false,
|
||||
});
|
||||
|
||||
const { data: clusters = [] } = useQuery<ClusterPublic[]>({
|
||||
queryKey: ['clusters-public'],
|
||||
queryFn: () => api.get('/clusters/public').then((r) => r.data),
|
||||
});
|
||||
|
||||
const { data: pools = [] } = useQuery<ClusterPoolPublic[]>({
|
||||
queryKey: ['pools-public'],
|
||||
queryFn: () => api.get('/clusters/pools/public').then((r) => r.data),
|
||||
});
|
||||
|
||||
// Sync form when resource data loads
|
||||
useEffect(() => {
|
||||
if (resourceUsage?.configured) {
|
||||
@@ -378,6 +388,22 @@ export default function AppDetailPage() {
|
||||
<dt className="text-sm text-gray-500">Port</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">{app.port}</dd>
|
||||
</div>
|
||||
{app.clusterId && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Cluster</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">
|
||||
🖥️ {clusters.find((c) => c.id === app.clusterId)?.name || 'Unknown'}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{app.poolId && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Pool</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">
|
||||
⚖️ {pools.find((p) => p.id === app.poolId)?.name || 'Unknown'}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{app.latestImageTag && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Image</dt>
|
||||
|
||||
Reference in New Issue
Block a user