feat(deploy): remove cluster allocation section from app creation
Drop the admin-only "cluster assignment" UI from the resources & config step of the deploy wizard and the related backend override. App placement is now always decided automatically by the allocator. - frontend: remove cluster/pool selection block, review-step cluster row, clusterMode state, public cluster/pool queries, and clusterId/ poolId from CreateApplicationDto - backend: drop clusterId/poolId override from the create DTO and simplify ApplicationsService.create to always auto-allocate; widen selectClusterForApplication param to keep the fallback path working Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,6 @@ import { useDeployProgressActions } from '@/lib/use-deploy-progress-actions';
|
||||
import { notify } from '@/lib/notify';
|
||||
import type {
|
||||
CreateApplicationDto,
|
||||
ClusterPublic,
|
||||
ClusterPoolPublic,
|
||||
DeployCostPreview,
|
||||
BillingCycle,
|
||||
PricingCatalog,
|
||||
@@ -251,7 +249,6 @@ export default function DeployPage() {
|
||||
useDeployProgressStore.getState().stopTracking(appId);
|
||||
}
|
||||
};
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const [step, setStep] = useState(0);
|
||||
const [form, setForm] = useState<CreateApplicationDto>({
|
||||
name: '',
|
||||
@@ -289,7 +286,6 @@ export default function DeployPage() {
|
||||
const [zipFile, setZipFile] = useState<File | null>(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [clusterMode, setClusterMode] = useState<'default' | 'manual' | 'pool'>('default');
|
||||
const [showDbPassword, setShowDbPassword] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [dbDumpFile, setDbDumpFile] = useState<File | null>(null);
|
||||
@@ -336,18 +332,6 @@ export default function DeployPage() {
|
||||
};
|
||||
};
|
||||
|
||||
const { data: clusters = [] } = useQuery<ClusterPublic[]>({
|
||||
queryKey: ['clusters-public'],
|
||||
queryFn: () => api.get('/clusters/public').then((r) => r.data),
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
const { data: pools = [] } = useQuery<ClusterPoolPublic[]>({
|
||||
queryKey: ['pools-public'],
|
||||
queryFn: () => api.get('/clusters/pools/public').then((r) => r.data),
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
const { data: pricingCatalog } = useQuery<PricingCatalog>({
|
||||
queryKey: ['pricing-catalog'],
|
||||
queryFn: () => api.get('/billing/pricing-catalog').then((r) => r.data),
|
||||
@@ -441,10 +425,6 @@ export default function DeployPage() {
|
||||
: {}),
|
||||
...(form.appStorageSize ? { appStorageSize: `${parseInt(form.appStorageSize, 10) || 2}Gi` } : {}),
|
||||
});
|
||||
if (!isAdmin) {
|
||||
delete payload.clusterId;
|
||||
delete payload.poolId;
|
||||
}
|
||||
const res = await api.post('/applications', payload);
|
||||
const appId = res.data.id;
|
||||
|
||||
@@ -524,10 +504,6 @@ export default function DeployPage() {
|
||||
: {}),
|
||||
...(form.appStorageSize ? { appStorageSize: `${parseInt(form.appStorageSize, 10) || 2}Gi` } : {}),
|
||||
});
|
||||
if (!isAdmin) {
|
||||
delete payload.clusterId;
|
||||
delete payload.poolId;
|
||||
}
|
||||
const res = await api.post('/applications', payload);
|
||||
const appId = res.data.id;
|
||||
|
||||
@@ -684,10 +660,6 @@ export default function DeployPage() {
|
||||
if (enableCustomDomain && customDomainInput.trim()) {
|
||||
payload.customDomain = customDomainInput.trim();
|
||||
}
|
||||
if (!isAdmin) {
|
||||
delete payload.clusterId;
|
||||
delete payload.poolId;
|
||||
}
|
||||
createMutation.mutate(sanitizePayloadForWordPressRuntime(payload));
|
||||
};
|
||||
|
||||
@@ -2101,181 +2073,6 @@ export default function DeployPage() {
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">{dw.resourcesConfig}</h2>
|
||||
|
||||
{/* Cluster Assignment Mode — Super Admin only */}
|
||||
{isAdmin && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">{dw.clusterAssignment}</label>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setClusterMode('default');
|
||||
setForm({ ...form, clusterId: undefined, poolId: undefined });
|
||||
}}
|
||||
className={`p-3 rounded-xl border-2 text-center transition-colors ${
|
||||
clusterMode === 'default'
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Home className="w-5 h-5 mx-auto text-gray-500" />
|
||||
<p className="mt-1 font-semibold text-sm text-gray-900">{dw.default}</p>
|
||||
<p className="text-xs text-gray-500">{dw.useDefaultCluster}</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setClusterMode('manual');
|
||||
setForm({ ...form, poolId: undefined });
|
||||
}}
|
||||
className={`p-3 rounded-xl border-2 text-center transition-colors ${
|
||||
clusterMode === 'manual'
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Target className="w-5 h-5 mx-auto text-gray-500" />
|
||||
<p className="mt-1 font-semibold text-sm text-gray-900">{dw.manual}</p>
|
||||
<p className="text-xs text-gray-500">{dw.pickSpecificCluster}</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setClusterMode('pool');
|
||||
setForm({ ...form, clusterId: undefined });
|
||||
}}
|
||||
className={`p-3 rounded-xl border-2 text-center transition-colors ${
|
||||
clusterMode === 'pool'
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Scale className="w-5 h-5 mx-auto text-gray-500" />
|
||||
<p className="mt-1 font-semibold text-sm text-gray-900">{dw.loadBalanced}</p>
|
||||
<p className="text-xs text-gray-500">{dw.pickClusterPool}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Manual: show cluster list */}
|
||||
{clusterMode === 'manual' && (
|
||||
<div className="space-y-2">
|
||||
{clusters.length === 0 ? (
|
||||
<p className="text-sm text-gray-400 text-center py-4">{dw.noClustersAvailable}</p>
|
||||
) : (
|
||||
clusters.map((cluster) => (
|
||||
<button
|
||||
key={cluster.id}
|
||||
type="button"
|
||||
onClick={() => setForm({ ...form, clusterId: cluster.id })}
|
||||
className={`w-full p-3 rounded-xl border-2 text-left transition-colors ${
|
||||
form.clusterId === cluster.id
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Server className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="font-semibold text-sm text-gray-900">
|
||||
{cluster.name}
|
||||
{cluster.isDefault && (
|
||||
<span className="ml-2 text-xs bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded">{dw.default}</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{[cluster.provider, cluster.region].filter(Boolean).join(' · ') || 'No region info'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
cluster.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
|
||||
}`}>
|
||||
{cluster.status}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pool: show pool list */}
|
||||
{clusterMode === 'pool' && (
|
||||
<div className="space-y-2">
|
||||
{pools.length === 0 ? (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-gray-400">{dw.noClusterPools}</p>
|
||||
<p className="text-xs text-gray-400 mt-1">{dw.askAdminPool}</p>
|
||||
</div>
|
||||
) : (
|
||||
pools.map((pool) => (
|
||||
<button
|
||||
key={pool.id}
|
||||
type="button"
|
||||
onClick={() => setForm({ ...form, poolId: pool.id })}
|
||||
className={`w-full p-3 rounded-xl border-2 text-left transition-colors ${
|
||||
form.poolId === pool.id
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Scale className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="font-semibold text-sm text-gray-900">{pool.name}</p>
|
||||
{pool.description && (
|
||||
<p className="text-xs text-gray-500">{pool.description}</p>
|
||||
)}
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<span className="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded flex items-center gap-1">
|
||||
{pool.strategy === 'weighted-resource'
|
||||
? <><BarChart3 className="w-3 h-3" />{dw.stratWeightedResource}</>
|
||||
: pool.strategy === 'least-loaded'
|
||||
? <><BarChart3 className="w-3 h-3" />{dw.stratLeastLoaded}</>
|
||||
: pool.strategy === 'weighted-round-robin'
|
||||
? <><RotateCw className="w-3 h-3" />{dw.stratWeightedRR}</>
|
||||
: pool.strategy === 'region-based'
|
||||
? <><BarChart3 className="w-3 h-3" />{dw.stratRegionBased}</>
|
||||
: pool.strategy === 'least-apps'
|
||||
? <><BarChart3 className="w-3 h-3" />{dw.stratLeastApps}</>
|
||||
: <><RotateCw className="w-3 h-3" />{dw.stratRoundRobin}</>}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{pool.clusters.length} cluster{pool.clusters.length !== 1 ? 's' : ''}:
|
||||
{' '}{pool.clusters.map((c) => c.name).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Default: info text */}
|
||||
{clusterMode === 'default' && (
|
||||
<div className="p-3 bg-gray-50 rounded-xl border border-gray-200">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Home className="w-5 h-5 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700">{dw.defaultClusterWillBeUsed}</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Your app will be deployed to the platform's default cluster
|
||||
{clusters.find((c) => c.isDefault) && (
|
||||
<> — <strong>{clusters.find((c) => c.isDefault)?.name}</strong></>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-xl border border-primary-100 bg-primary-50/40 p-4 space-y-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide">{dw.applicationWorkload}</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
@@ -2609,18 +2406,6 @@ export default function DeployPage() {
|
||||
<span className="text-sm font-medium text-green-600">{dw.tokenProvided}</span>
|
||||
</div>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-500">{dw.cluster}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{clusterMode === 'manual' && form.clusterId
|
||||
? `${clusters.find((c) => c.id === form.clusterId)?.name || form.clusterId}`
|
||||
: clusterMode === 'pool' && form.poolId
|
||||
? `${pools.find((p) => p.id === form.poolId)?.name || 'Pool'} (Load Balanced)`
|
||||
: dw.automaticAllocator}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-500">{dw.cpu}</span>
|
||||
<span className="text-sm font-medium">{form.cpuRequest} / {form.cpuLimit}</span>
|
||||
|
||||
Reference in New Issue
Block a user