Merge pull request 'fix: Show only git SHA in header' (#34) from feat/worker-scaling into master
Reviewed-on: https://code.cannabrands.app/Creationshop/dispensary-scraper/pulls/34
This commit is contained in:
@@ -131,7 +131,7 @@ export function Layout({ children }: LayoutProps) {
|
||||
<span className="text-lg font-bold text-gray-900">CannaIQ</span>
|
||||
{versionInfo && (
|
||||
<p className="text-xs text-gray-400">
|
||||
v{versionInfo.version} ({versionInfo.git_sha}) {versionInfo.build_time !== 'unknown' && `- ${new Date(versionInfo.build_time).toLocaleDateString()}`}
|
||||
{versionInfo.git_sha || 'dev'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '../lib/api';
|
||||
import { Layout } from '../components/Layout';
|
||||
import { PasswordConfirmModal } from '../components/PasswordConfirmModal';
|
||||
import {
|
||||
ListChecks,
|
||||
Clock,
|
||||
@@ -17,9 +16,6 @@ import {
|
||||
Users,
|
||||
Play,
|
||||
Square,
|
||||
Plus,
|
||||
Minus,
|
||||
Server,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface Task {
|
||||
@@ -144,16 +140,6 @@ export default function TasksDashboard() {
|
||||
const [poolPaused, setPoolPaused] = 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
|
||||
const [roleFilter, setRoleFilter] = useState<string>('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||
@@ -163,7 +149,7 @@ export default function TasksDashboard() {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [tasksRes, countsRes, capacityRes, poolStatus, k8sRes] = await Promise.all([
|
||||
const [tasksRes, countsRes, capacityRes, poolStatus] = await Promise.all([
|
||||
api.getTasks({
|
||||
role: roleFilter || undefined,
|
||||
status: statusFilter || undefined,
|
||||
@@ -172,16 +158,12 @@ export default function TasksDashboard() {
|
||||
api.getTaskCounts(),
|
||||
api.getTaskCapacity(),
|
||||
api.getTaskPoolStatus(),
|
||||
api.getK8sWorkers(),
|
||||
]);
|
||||
|
||||
setTasks(tasksRes.tasks || []);
|
||||
setCounts(countsRes);
|
||||
setCapacity(capacityRes.metrics || []);
|
||||
setPoolPaused(poolStatus.paused);
|
||||
setK8sAvailable(k8sRes.available);
|
||||
setWorkerReplicas(k8sRes.replicas);
|
||||
setWorkerReady(k8sRes.readyReplicas);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
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 () => {
|
||||
setPoolLoading(true);
|
||||
try {
|
||||
@@ -285,34 +238,8 @@ export default function TasksDashboard() {
|
||||
</div>
|
||||
|
||||
<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 */}
|
||||
<button
|
||||
<button
|
||||
onClick={togglePool}
|
||||
disabled={poolLoading}
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Layout } from '../components/Layout';
|
||||
import { PasswordConfirmModal } from '../components/PasswordConfirmModal';
|
||||
import { api } from '../lib/api';
|
||||
import {
|
||||
Users,
|
||||
@@ -232,6 +233,10 @@ export function WorkersDashboard() {
|
||||
const [scaling, setScaling] = useState(false);
|
||||
const [targetReplicas, setTargetReplicas] = useState<number | null>(null);
|
||||
|
||||
// Password confirmation for scaling
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [pendingReplicas, setPendingReplicas] = useState<number | null>(null);
|
||||
|
||||
// Pagination
|
||||
const [page, setPage] = useState(0);
|
||||
const workersPerPage = 15;
|
||||
@@ -254,14 +259,21 @@ export function WorkersDashboard() {
|
||||
}
|
||||
}, [targetReplicas]);
|
||||
|
||||
// Scale workers (added 2024-12-10)
|
||||
const handleScale = useCallback(async (replicas: number) => {
|
||||
// Request scale - shows confirmation modal first
|
||||
const requestScale = useCallback((replicas: number) => {
|
||||
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);
|
||||
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) {
|
||||
setTargetReplicas(replicas);
|
||||
setTargetReplicas(pendingReplicas);
|
||||
// Refresh after a short delay to see the change
|
||||
setTimeout(fetchK8sReplicas, 1000);
|
||||
}
|
||||
@@ -270,8 +282,9 @@ export function WorkersDashboard() {
|
||||
setK8sError(err.response?.data?.error || 'Failed to scale');
|
||||
} finally {
|
||||
setScaling(false);
|
||||
setPendingReplicas(null);
|
||||
}
|
||||
}, [fetchK8sReplicas]);
|
||||
}, [fetchK8sReplicas, pendingReplicas]);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
@@ -389,7 +402,7 @@ export function WorkersDashboard() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleScale((targetReplicas || k8sReplicas.desired) - 1)}
|
||||
onClick={() => requestScale((targetReplicas || k8sReplicas.desired) - 1)}
|
||||
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"
|
||||
title="Scale down"
|
||||
@@ -404,18 +417,18 @@ export function WorkersDashboard() {
|
||||
onChange={(e) => setTargetReplicas(Math.max(0, Math.min(20, parseInt(e.target.value) || 0)))}
|
||||
onBlur={() => {
|
||||
if (targetReplicas !== null && targetReplicas !== k8sReplicas.desired) {
|
||||
handleScale(targetReplicas);
|
||||
requestScale(targetReplicas);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleScale((targetReplicas || k8sReplicas.desired) + 1)}
|
||||
onClick={() => requestScale((targetReplicas || k8sReplicas.desired) + 1)}
|
||||
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"
|
||||
title="Scale up"
|
||||
@@ -685,6 +698,18 @@ export function WorkersDashboard() {
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user