refactor: Move worker scaling to Workers page with password confirmation
- Worker scaling controls now on /workers page only (removed from /tasks) - Password confirmation required before scaling - Show only git SHA in header (removed version number) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import { Layout } from '../components/Layout';
|
import { Layout } from '../components/Layout';
|
||||||
import { PasswordConfirmModal } from '../components/PasswordConfirmModal';
|
|
||||||
import {
|
import {
|
||||||
ListChecks,
|
ListChecks,
|
||||||
Clock,
|
Clock,
|
||||||
@@ -17,9 +16,6 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
Play,
|
Play,
|
||||||
Square,
|
Square,
|
||||||
Plus,
|
|
||||||
Minus,
|
|
||||||
Server,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface Task {
|
interface Task {
|
||||||
@@ -144,16 +140,6 @@ export default function TasksDashboard() {
|
|||||||
const [poolPaused, setPoolPaused] = useState(false);
|
const [poolPaused, setPoolPaused] = useState(false);
|
||||||
const [poolLoading, setPoolLoading] = useState(false);
|
const [poolLoading, setPoolLoading] = useState(false);
|
||||||
|
|
||||||
// K8s worker state
|
|
||||||
const [k8sAvailable, setK8sAvailable] = useState(false);
|
|
||||||
const [workerReplicas, setWorkerReplicas] = useState(0);
|
|
||||||
const [workerReady, setWorkerReady] = useState(0);
|
|
||||||
const [scalingWorkers, setScalingWorkers] = useState(false);
|
|
||||||
|
|
||||||
// Password confirmation for scaling
|
|
||||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
|
||||||
const [pendingScaleDelta, setPendingScaleDelta] = useState(0);
|
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
const [roleFilter, setRoleFilter] = useState<string>('');
|
const [roleFilter, setRoleFilter] = useState<string>('');
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||||
@@ -163,7 +149,7 @@ export default function TasksDashboard() {
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const [tasksRes, countsRes, capacityRes, poolStatus, k8sRes] = await Promise.all([
|
const [tasksRes, countsRes, capacityRes, poolStatus] = await Promise.all([
|
||||||
api.getTasks({
|
api.getTasks({
|
||||||
role: roleFilter || undefined,
|
role: roleFilter || undefined,
|
||||||
status: statusFilter || undefined,
|
status: statusFilter || undefined,
|
||||||
@@ -172,16 +158,12 @@ export default function TasksDashboard() {
|
|||||||
api.getTaskCounts(),
|
api.getTaskCounts(),
|
||||||
api.getTaskCapacity(),
|
api.getTaskCapacity(),
|
||||||
api.getTaskPoolStatus(),
|
api.getTaskPoolStatus(),
|
||||||
api.getK8sWorkers(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setTasks(tasksRes.tasks || []);
|
setTasks(tasksRes.tasks || []);
|
||||||
setCounts(countsRes);
|
setCounts(countsRes);
|
||||||
setCapacity(capacityRes.metrics || []);
|
setCapacity(capacityRes.metrics || []);
|
||||||
setPoolPaused(poolStatus.paused);
|
setPoolPaused(poolStatus.paused);
|
||||||
setK8sAvailable(k8sRes.available);
|
|
||||||
setWorkerReplicas(k8sRes.replicas);
|
|
||||||
setWorkerReady(k8sRes.readyReplicas);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to load tasks');
|
setError(err.message || 'Failed to load tasks');
|
||||||
@@ -190,35 +172,6 @@ export default function TasksDashboard() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Request to scale workers - shows confirmation modal first
|
|
||||||
const requestScaleWorkers = (delta: number) => {
|
|
||||||
const newReplicas = Math.max(0, Math.min(50, workerReplicas + delta));
|
|
||||||
if (newReplicas === workerReplicas) return;
|
|
||||||
|
|
||||||
setPendingScaleDelta(delta);
|
|
||||||
setShowConfirmModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Actually scale workers after password confirmation
|
|
||||||
const executeScaleWorkers = async () => {
|
|
||||||
const newReplicas = Math.max(0, Math.min(50, workerReplicas + pendingScaleDelta));
|
|
||||||
|
|
||||||
setScalingWorkers(true);
|
|
||||||
try {
|
|
||||||
const res = await api.scaleK8sWorkers(newReplicas);
|
|
||||||
if (res.success) {
|
|
||||||
setWorkerReplicas(res.replicas);
|
|
||||||
} else {
|
|
||||||
setError(res.error || 'Failed to scale workers');
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err.message || 'Failed to scale workers');
|
|
||||||
} finally {
|
|
||||||
setScalingWorkers(false);
|
|
||||||
setPendingScaleDelta(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const togglePool = async () => {
|
const togglePool = async () => {
|
||||||
setPoolLoading(true);
|
setPoolLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -285,34 +238,8 @@ export default function TasksDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Worker Scaling */}
|
|
||||||
{k8sAvailable && (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 bg-gray-100 rounded-lg">
|
|
||||||
<Server className="w-4 h-4 text-gray-500" />
|
|
||||||
<button
|
|
||||||
onClick={() => requestScaleWorkers(-5)}
|
|
||||||
disabled={scalingWorkers || workerReplicas === 0}
|
|
||||||
className="p-1 hover:bg-gray-200 rounded disabled:opacity-50"
|
|
||||||
title="Remove 5 workers"
|
|
||||||
>
|
|
||||||
<Minus className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<span className={`min-w-[60px] text-center font-mono ${scalingWorkers ? 'animate-pulse' : ''}`}>
|
|
||||||
{workerReady}/{workerReplicas}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => requestScaleWorkers(5)}
|
|
||||||
disabled={scalingWorkers || workerReplicas >= 50}
|
|
||||||
className="p-1 hover:bg-gray-200 rounded disabled:opacity-50"
|
|
||||||
title="Add 5 workers"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pool Toggle */}
|
{/* Pool Toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={togglePool}
|
onClick={togglePool}
|
||||||
disabled={poolLoading}
|
disabled={poolLoading}
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors ${
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
@@ -593,18 +520,6 @@ export default function TasksDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password Confirmation Modal for Worker Scaling */}
|
|
||||||
<PasswordConfirmModal
|
|
||||||
isOpen={showConfirmModal}
|
|
||||||
onClose={() => {
|
|
||||||
setShowConfirmModal(false);
|
|
||||||
setPendingScaleDelta(0);
|
|
||||||
}}
|
|
||||||
onConfirm={executeScaleWorkers}
|
|
||||||
title="Confirm Worker Scaling"
|
|
||||||
description={`You are about to ${pendingScaleDelta > 0 ? 'add' : 'remove'} ${Math.abs(pendingScaleDelta)} workers (${workerReplicas} → ${Math.max(0, Math.min(50, workerReplicas + pendingScaleDelta))}). This action affects production infrastructure.`}
|
|
||||||
/>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Layout } from '../components/Layout';
|
import { Layout } from '../components/Layout';
|
||||||
|
import { PasswordConfirmModal } from '../components/PasswordConfirmModal';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import {
|
import {
|
||||||
Users,
|
Users,
|
||||||
@@ -232,6 +233,10 @@ export function WorkersDashboard() {
|
|||||||
const [scaling, setScaling] = useState(false);
|
const [scaling, setScaling] = useState(false);
|
||||||
const [targetReplicas, setTargetReplicas] = useState<number | null>(null);
|
const [targetReplicas, setTargetReplicas] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Password confirmation for scaling
|
||||||
|
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||||
|
const [pendingReplicas, setPendingReplicas] = useState<number | null>(null);
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const workersPerPage = 15;
|
const workersPerPage = 15;
|
||||||
@@ -254,14 +259,21 @@ export function WorkersDashboard() {
|
|||||||
}
|
}
|
||||||
}, [targetReplicas]);
|
}, [targetReplicas]);
|
||||||
|
|
||||||
// Scale workers (added 2024-12-10)
|
// Request scale - shows confirmation modal first
|
||||||
const handleScale = useCallback(async (replicas: number) => {
|
const requestScale = useCallback((replicas: number) => {
|
||||||
if (replicas < 0 || replicas > 20) return;
|
if (replicas < 0 || replicas > 20) return;
|
||||||
|
setPendingReplicas(replicas);
|
||||||
|
setShowConfirmModal(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Execute scale after password confirmation
|
||||||
|
const executeScale = useCallback(async () => {
|
||||||
|
if (pendingReplicas === null) return;
|
||||||
setScaling(true);
|
setScaling(true);
|
||||||
try {
|
try {
|
||||||
const res = await api.post('/api/workers/k8s/scale', { replicas });
|
const res = await api.post('/api/workers/k8s/scale', { replicas: pendingReplicas });
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
setTargetReplicas(replicas);
|
setTargetReplicas(pendingReplicas);
|
||||||
// Refresh after a short delay to see the change
|
// Refresh after a short delay to see the change
|
||||||
setTimeout(fetchK8sReplicas, 1000);
|
setTimeout(fetchK8sReplicas, 1000);
|
||||||
}
|
}
|
||||||
@@ -270,8 +282,9 @@ export function WorkersDashboard() {
|
|||||||
setK8sError(err.response?.data?.error || 'Failed to scale');
|
setK8sError(err.response?.data?.error || 'Failed to scale');
|
||||||
} finally {
|
} finally {
|
||||||
setScaling(false);
|
setScaling(false);
|
||||||
|
setPendingReplicas(null);
|
||||||
}
|
}
|
||||||
}, [fetchK8sReplicas]);
|
}, [fetchK8sReplicas, pendingReplicas]);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -389,7 +402,7 @@ export function WorkersDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleScale((targetReplicas || k8sReplicas.desired) - 1)}
|
onClick={() => requestScale((targetReplicas || k8sReplicas.desired) - 1)}
|
||||||
disabled={scaling || (targetReplicas || k8sReplicas.desired) <= 0}
|
disabled={scaling || (targetReplicas || k8sReplicas.desired) <= 0}
|
||||||
className="w-8 h-8 flex items-center justify-center bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="w-8 h-8 flex items-center justify-center bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
title="Scale down"
|
title="Scale down"
|
||||||
@@ -404,18 +417,18 @@ export function WorkersDashboard() {
|
|||||||
onChange={(e) => setTargetReplicas(Math.max(0, Math.min(20, parseInt(e.target.value) || 0)))}
|
onChange={(e) => setTargetReplicas(Math.max(0, Math.min(20, parseInt(e.target.value) || 0)))}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (targetReplicas !== null && targetReplicas !== k8sReplicas.desired) {
|
if (targetReplicas !== null && targetReplicas !== k8sReplicas.desired) {
|
||||||
handleScale(targetReplicas);
|
requestScale(targetReplicas);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && targetReplicas !== null && targetReplicas !== k8sReplicas.desired) {
|
if (e.key === 'Enter' && targetReplicas !== null && targetReplicas !== k8sReplicas.desired) {
|
||||||
handleScale(targetReplicas);
|
requestScale(targetReplicas);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-16 text-center border border-gray-300 rounded-lg px-2 py-1 text-lg font-semibold"
|
className="w-16 text-center border border-gray-300 rounded-lg px-2 py-1 text-lg font-semibold"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleScale((targetReplicas || k8sReplicas.desired) + 1)}
|
onClick={() => requestScale((targetReplicas || k8sReplicas.desired) + 1)}
|
||||||
disabled={scaling || (targetReplicas || k8sReplicas.desired) >= 20}
|
disabled={scaling || (targetReplicas || k8sReplicas.desired) >= 20}
|
||||||
className="w-8 h-8 flex items-center justify-center bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
className="w-8 h-8 flex items-center justify-center bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
title="Scale up"
|
title="Scale up"
|
||||||
@@ -685,6 +698,18 @@ export function WorkersDashboard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Password Confirmation Modal for Scaling */}
|
||||||
|
<PasswordConfirmModal
|
||||||
|
isOpen={showConfirmModal}
|
||||||
|
onClose={() => {
|
||||||
|
setShowConfirmModal(false);
|
||||||
|
setPendingReplicas(null);
|
||||||
|
}}
|
||||||
|
onConfirm={executeScale}
|
||||||
|
title="Confirm Worker Scaling"
|
||||||
|
description={`You are about to scale workers to ${pendingReplicas} replicas${k8sReplicas ? ` (currently ${k8sReplicas.desired})` : ''}. This action affects production infrastructure.`}
|
||||||
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user