From 22b05617d48894f40d7a444586d79a1a8964541f Mon Sep 17 00:00:00 2001 From: Chaim Marcus Date: Thu, 26 Mar 2026 18:44:56 +0000 Subject: [PATCH] feat: install order, dependency blocking, batch install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sort extensions by install order (HebrewLanguage first, then deps) - Grey out extensions with missing dependencies (show lock icon + "נדרש: X") - Checkbox selection for batch install with progress indicator - "בחר הכל" button to select all installable extensions - Server returns dependencies + install_order in customer API Co-Authored-By: Claude Opus 4.6 (1M context) --- dashboard/src/App.jsx | 124 +++++++++++++++++++++++++++++++++++++----- server.py | 35 ++++++++++++ 2 files changed, 146 insertions(+), 13 deletions(-) diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx index 35aef32..d6df477 100644 --- a/dashboard/src/App.jsx +++ b/dashboard/src/App.jsx @@ -201,11 +201,41 @@ function CustomerDetail({ customerId }) { const [deploying, setDeploying] = useState(null) const [deployResult, setDeployResult] = useState(null) const [editing, setEditing] = useState(false) + const [selected, setSelected] = useState(new Set()) + const [batchDeploying, setBatchDeploying] = useState(false) + const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, module: '' }) if (!customer) return const assignedIds = new Set((customer.extensions || []).map(e => e.id)) const available = (allExtensions || []).filter(e => !assignedIds.has(e.id)) + const order = customer.install_order || [] + const deps = customer.dependencies || {} + const exts = customer.extensions || [] + const installedNames = new Set(exts.filter(e => e.installed_version).map(e => e.name)) + + const sorted = [...exts].sort((a, b) => { + const ia = order.indexOf(a.name) + const ib = order.indexOf(b.name) + return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib) + }) + + const toggleSelect = (id) => { + setSelected(prev => { + const next = new Set(prev) + next.has(id) ? next.delete(id) : next.add(id) + return next + }) + } + + const selectAll = () => { + const installable = sorted.filter(e => { + if (e.installed_version) return false + const missing = (deps[e.name] || []).filter(d => !installedNames.has(d)) + return missing.length === 0 + }) + setSelected(new Set(installable.map(e => e.id))) + } const deploy = async (module, version) => { setDeploying(module) @@ -224,6 +254,34 @@ function CustomerDetail({ customerId }) { setDeploying(null) } + const batchDeploy = async () => { + const toDeploy = sorted.filter(e => selected.has(e.id) && !e.installed_version) + setBatchDeploying(true) + setBatchProgress({ current: 0, total: toDeploy.length, module: '' }) + const results = [] + for (let i = 0; i < toDeploy.length; i++) { + const e = toDeploy[i] + setBatchProgress({ current: i + 1, total: toDeploy.length, module: e.name }) + try { + const r = await fetch(`${API}/deploy`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ customer_id: customerId, module: e.name, version: e.latest_version }) + }) + const result = await r.json() + results.push({ module: e.name, ...result }) + } catch (err) { + results.push({ module: e.name, status: 'error', message: err.message }) + } + } + const ok = results.filter(r => r.status === 'ok').length + const fail = results.filter(r => r.status !== 'ok').length + setDeployResult({ module: `${ok} הרחבות`, status: fail === 0 ? 'ok' : 'error', + message: fail > 0 ? `${ok} הצליחו, ${fail} נכשלו` : undefined }) + setBatchDeploying(false) + setSelected(new Set()) + reload() + } + const remove = async (extId) => { await fetch(`${API}/customers/${customerId}/extensions/${extId}`, { method: 'DELETE' }) reload() @@ -265,38 +323,76 @@ function CustomerDetail({ customerId }) { )} + {batchDeploying && ( +
+ + מתקין {batchProgress.current}/{batchProgress.total}: {batchProgress.module}... +
+ )} +
-

הרחבות מורשות

- {(customer.extensions || []).length} הרחבות +
+

הרחבות מורשות

+ {selected.size > 0 && ( + + )} +
+
+ {sorted.some(e => !e.installed_version && (deps[e.name] || []).every(d => installedNames.has(d))) && ( + + )} + {exts.length} הרחבות +
- + + - {(customer.extensions || []).map(e => { + {sorted.map(e => { const installed = e.installed_version const hasUpdate = installed && e.latest_version && installed !== e.latest_version + const missingDeps = (deps[e.name] || []).filter(d => !installedNames.has(d)) + const blocked = !installed && missingDeps.length > 0 return ( - - + +
שם ההרחבהשם ההרחבה מותקנת עדכון זמין פעולות
+
+ {!installed && !blocked && ( + toggleSelect(e.id)} disabled={batchDeploying} + className="rounded border-slate-300 text-blue-600 focus:ring-blue-500 cursor-pointer" /> + )} +
-
- +
+ +
+
+ {e.name} + {blocked && ( +

נדרש: {missingDeps.join(', ')}

+ )}
- {e.name}
{installed ? v{installed} - : לא מותקן} + : לא מותקן} {hasUpdate @@ -305,9 +401,11 @@ function CustomerDetail({ customerId }) {
- {!installed ? ( + {blocked ? ( + + ) : !installed ? ( @@ -315,7 +413,7 @@ function CustomerDetail({ customerId }) { <> {hasUpdate && (