redesign: apply Stitch design system — Heebo font, glass header, Material icons, editorial tables
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="he" dir="rtl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>extension-dashboard</title>
|
<title>Extension Platform</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@300;400;500;700;800;900&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+205
-116
@@ -14,26 +14,39 @@ function useFetch(url) {
|
|||||||
return { data, loading, reload }
|
return { data, loading, reload }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Stats Bar ---
|
// --- Icon helper ---
|
||||||
|
function Icon({ name, className = '' }) {
|
||||||
|
return <span className={`material-symbols-outlined ${className}`}>{name}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Stats Cards ---
|
||||||
function StatsBar() {
|
function StatsBar() {
|
||||||
const { data } = useFetch(`${API}/stats`)
|
const { data } = useFetch(`${API}/stats`)
|
||||||
if (!data) return null
|
if (!data) return null
|
||||||
const items = [
|
const items = [
|
||||||
{ label: 'הרחבות', value: data.total_extensions, color: 'bg-blue-500' },
|
{ label: 'הרחבות פעילות', value: data.total_extensions, icon: 'extension', color: 'blue' },
|
||||||
{ label: 'לקוחות', value: data.total_customers, color: 'bg-green-500' },
|
{ label: 'לקוחות', value: data.total_customers, icon: 'group', color: 'slate' },
|
||||||
{ label: 'רישיונות', value: data.total_licenses, color: 'bg-purple-500' },
|
{ label: 'רישיונות', value: data.total_licenses, icon: 'key', color: 'slate' },
|
||||||
{ label: 'פריסות מוצלחות', value: data.successful_deploys, color: 'bg-emerald-500' },
|
{ label: 'פריסות מוצלחות', value: data.successful_deploys, icon: 'check_circle', color: 'emerald', border: true },
|
||||||
{ label: 'כשלונות', value: data.failed_deploys, color: 'bg-red-500' },
|
{ label: 'כשלונות', value: data.failed_deploys, icon: 'error', color: 'red', border: true },
|
||||||
]
|
]
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-5 gap-4 mb-8">
|
<section className="grid grid-cols-1 md:grid-cols-5 gap-6">
|
||||||
{items.map(s => (
|
{items.map(s => (
|
||||||
<div key={s.label} className="bg-white rounded-lg shadow p-4 text-center">
|
<div key={s.label} className={`bg-white p-6 rounded-xl flex flex-col justify-between text-right
|
||||||
<div className={`text-3xl font-bold ${s.color.replace('bg-', 'text-')}`}>{s.value}</div>
|
hover:bg-${s.color}-50/30 transition-colors ${s.border ? `border-b-4 border-${s.color}-500/20` : ''}`}>
|
||||||
<div className="text-sm text-gray-500 mt-1">{s.label}</div>
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<div className={`w-10 h-10 rounded-xl bg-${s.color}-100 flex items-center justify-center text-${s.color}-700`}>
|
||||||
|
<Icon name={s.icon} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className={`text-3xl font-black ${s.color === 'red' ? 'text-red-600' : 'text-slate-900'}`}>{s.value}</h3>
|
||||||
|
<p className={`text-sm mt-1 ${s.color === 'emerald' ? 'text-emerald-600 font-medium' : s.color === 'red' ? 'text-red-600/70 font-bold' : 'text-slate-500'}`}>{s.label}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,30 +55,49 @@ function ExtensionsTab() {
|
|||||||
const { data: extensions } = useFetch(`${API}/extensions`)
|
const { data: extensions } = useFetch(`${API}/extensions`)
|
||||||
if (!extensions) return <Loader />
|
if (!extensions) return <Loader />
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
<section className="bg-white rounded-xl overflow-hidden">
|
||||||
<table className="w-full text-right">
|
<div className="p-8 flex justify-between items-center">
|
||||||
<thead className="bg-gray-50">
|
<div className="text-right">
|
||||||
<tr>
|
<h2 className="text-xl font-black text-slate-900">רשימת הרחבות</h2>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">שם</th>
|
<p className="text-sm text-slate-500 mt-1">כל ההרחבות הזמינות במערכת</p>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">Composer</th>
|
</div>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">גרסה אחרונה</th>
|
</div>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">לקוחות</th>
|
<div className="overflow-x-auto">
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">גרסאות</th>
|
<table className="w-full text-right border-collapse">
|
||||||
</tr>
|
<thead>
|
||||||
</thead>
|
<tr className="bg-slate-50/50">
|
||||||
<tbody className="divide-y divide-gray-200">
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">שם ההרחבה</th>
|
||||||
{extensions.map(e => (
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">Composer</th>
|
||||||
<tr key={e.id} className="hover:bg-gray-50">
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">גרסה אחרונה</th>
|
||||||
<td className="px-4 py-3 font-medium">{e.name}</td>
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">לקוחות</th>
|
||||||
<td className="px-4 py-3 text-sm text-gray-500 font-mono">{e.composer_name}</td>
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">גרסאות</th>
|
||||||
<td className="px-4 py-3"><Badge>{e.latest_version || '—'}</Badge></td>
|
|
||||||
<td className="px-4 py-3 text-center">{e.customer_count}</td>
|
|
||||||
<td className="px-4 py-3 text-center">{e.version_count}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody className="divide-y divide-slate-50">
|
||||||
</table>
|
{extensions.map(e => (
|
||||||
</div>
|
<tr key={e.id} className="hover:bg-slate-50/80 transition-all">
|
||||||
|
<td className="px-8 py-5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center text-blue-700">
|
||||||
|
<Icon name="extension" className="text-base" />
|
||||||
|
</div>
|
||||||
|
<span className="font-bold text-slate-900">{e.name}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-8 py-5">
|
||||||
|
<span className="font-mono text-xs bg-slate-100 px-2 py-1 rounded text-slate-600" dir="ltr">{e.composer_name}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-8 py-5">
|
||||||
|
<VersionBadge>{e.latest_version || '—'}</VersionBadge>
|
||||||
|
</td>
|
||||||
|
<td className="px-8 py-5 text-slate-500 font-medium">{e.customer_count}</td>
|
||||||
|
<td className="px-8 py-5 text-slate-500 font-medium">{e.version_count}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,30 +110,45 @@ function CustomersTab() {
|
|||||||
if (!customers) return <Loader />
|
if (!customers) return <Loader />
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
<div className="w-1/2">
|
<div className="w-80 shrink-0">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h3 className="text-lg font-semibold">לקוחות ({customers.length})</h3>
|
<h3 className="text-lg font-black text-slate-900">לקוחות ({customers.length})</h3>
|
||||||
<button onClick={() => setShowForm(!showForm)}
|
<button onClick={() => setShowForm(!showForm)}
|
||||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 text-sm">
|
className="deep-action-gradient text-white px-4 py-2 rounded-xl font-bold text-sm flex items-center gap-2 shadow-lg shadow-blue-500/20 active:scale-95 transition-all">
|
||||||
+ לקוח חדש
|
<Icon name="add" className="text-sm" />
|
||||||
|
<span>חדש</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{showForm && <CustomerForm onDone={() => { setShowForm(false); reload() }} />}
|
{showForm && <CustomerForm onDone={() => { setShowForm(false); reload() }} />}
|
||||||
<div className="bg-white rounded-lg shadow divide-y">
|
<div className="space-y-2">
|
||||||
{customers.map(c => (
|
{customers.map(c => (
|
||||||
<div key={c.id} onClick={() => setSelected(c.id)}
|
<div key={c.id} onClick={() => setSelected(c.id)}
|
||||||
className={`p-4 cursor-pointer hover:bg-gray-50 ${selected === c.id ? 'bg-blue-50 border-r-4 border-blue-500' : ''}`}>
|
className={`bg-white p-4 rounded-xl cursor-pointer transition-all ${selected === c.id ? 'ring-2 ring-blue-500 bg-blue-50/30' : 'hover:bg-slate-50'}`}>
|
||||||
<div className="font-medium">{c.name}</div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-sm text-gray-500">{c.espocrm_url}</div>
|
<div className="w-10 h-10 rounded-xl bg-slate-100 flex items-center justify-center text-slate-600">
|
||||||
<div className="text-xs text-gray-400 mt-1">
|
<Icon name="domain" />
|
||||||
{c.extension_count} הרחבות • {c.is_active ? '🟢 פעיל' : '🔴 לא פעיל'}
|
</div>
|
||||||
|
<div className="text-right flex-1">
|
||||||
|
<p className="font-bold text-slate-900">{c.name}</p>
|
||||||
|
<p className="text-xs text-slate-500 mt-0.5" dir="ltr">{c.espocrm_url}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 mt-3 text-xs text-slate-400">
|
||||||
|
<span>{c.extension_count} הרחבות</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span className={c.is_active ? 'text-emerald-600' : 'text-red-500'}>{c.is_active ? 'פעיל' : 'לא פעיל'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2">
|
<div className="flex-1 min-w-0">
|
||||||
{selected && <CustomerDetail customerId={selected} />}
|
{selected ? <CustomerDetail customerId={selected} /> : (
|
||||||
|
<div className="bg-white rounded-xl p-12 text-center text-slate-400">
|
||||||
|
<Icon name="touch_app" className="text-5xl mb-4 block" />
|
||||||
|
<p className="font-medium">בחר לקוח מהרשימה</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -116,17 +163,18 @@ function CustomerForm({ onDone }) {
|
|||||||
onDone()
|
onDone()
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow p-4 mb-4 space-y-3">
|
<div className="bg-white rounded-xl p-5 mb-4 space-y-3">
|
||||||
<input placeholder="שם לקוח" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })}
|
<input placeholder="שם לקוח" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })}
|
||||||
className="w-full border rounded px-3 py-2 text-right" />
|
className="w-full bg-slate-50 border-none rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-blue-500 text-right" />
|
||||||
<input placeholder="אימייל" value={form.email} onChange={e => setForm({ ...form, email: e.target.value })}
|
<input placeholder="אימייל" value={form.email} onChange={e => setForm({ ...form, email: e.target.value })}
|
||||||
className="w-full border rounded px-3 py-2 text-right" dir="ltr" />
|
className="w-full bg-slate-50 border-none rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-blue-500" dir="ltr" />
|
||||||
<input placeholder="EspoCRM URL" value={form.espocrm_url} onChange={e => setForm({ ...form, espocrm_url: e.target.value })}
|
<input placeholder="EspoCRM URL" value={form.espocrm_url} onChange={e => setForm({ ...form, espocrm_url: e.target.value })}
|
||||||
className="w-full border rounded px-3 py-2" dir="ltr" />
|
className="w-full bg-slate-50 border-none rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-blue-500" dir="ltr" />
|
||||||
<input placeholder="Espo-Authorization (base64)" value={form.espocrm_api_key}
|
<input placeholder="Espo-Authorization (base64)" value={form.espocrm_api_key}
|
||||||
onChange={e => setForm({ ...form, espocrm_api_key: e.target.value })}
|
onChange={e => setForm({ ...form, espocrm_api_key: e.target.value })}
|
||||||
className="w-full border rounded px-3 py-2 font-mono text-sm" dir="ltr" />
|
className="w-full bg-slate-50 border-none rounded-xl px-4 py-2.5 text-sm font-mono focus:ring-2 focus:ring-blue-500" dir="ltr" />
|
||||||
<button onClick={submit} className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 w-full">
|
<button onClick={submit}
|
||||||
|
className="deep-action-gradient text-white px-4 py-2.5 rounded-xl font-bold w-full shadow-lg shadow-blue-500/20 active:scale-95 transition-all">
|
||||||
שמור
|
שמור
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,48 +224,67 @@ function CustomerDetail({ customerId }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-2">{customer.name}</h3>
|
<div className="flex items-center gap-4 mb-6">
|
||||||
<div className="text-sm text-gray-500 mb-4" dir="ltr">{customer.espocrm_url}</div>
|
<div className="w-12 h-12 rounded-xl bg-blue-100 flex items-center justify-center text-blue-700">
|
||||||
|
<Icon name="domain" className="text-2xl" />
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<h3 className="text-xl font-black text-slate-900">{customer.name}</h3>
|
||||||
|
<p className="text-sm text-slate-500" dir="ltr">{customer.espocrm_url}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{deployResult && (
|
{deployResult && (
|
||||||
<div className={`p-3 rounded mb-4 text-sm ${deployResult.status === 'ok' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
<div className={`p-4 rounded-xl mb-6 text-sm font-medium flex items-center gap-3 ${deployResult.status === 'ok' ? 'bg-emerald-50 text-emerald-800' : 'bg-red-50 text-red-800'}`}>
|
||||||
|
<Icon name={deployResult.status === 'ok' ? 'check_circle' : 'error'} />
|
||||||
{deployResult.module}: {deployResult.status === 'ok' ? 'הותקן בהצלחה!' : `שגיאה: ${deployResult.message}`}
|
{deployResult.module}: {deployResult.status === 'ok' ? 'הותקן בהצלחה!' : `שגיאה: ${deployResult.message}`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
<section className="bg-white rounded-xl overflow-hidden">
|
||||||
<table className="w-full text-right">
|
<div className="p-6 flex justify-between items-center">
|
||||||
<thead className="bg-gray-50">
|
<h4 className="font-bold text-slate-900">הרחבות מורשות</h4>
|
||||||
<tr>
|
<span className="text-xs font-bold text-slate-400 uppercase tracking-widest">{(customer.extensions || []).length} הרחבות</span>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">שם ההרחבה</th>
|
</div>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">מותקנת</th>
|
<table className="w-full text-right border-collapse">
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">עדכון זמין</th>
|
<thead>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">פעולות</th>
|
<tr className="bg-slate-50/50">
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">שם ההרחבה</th>
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">מותקנת</th>
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">עדכון זמין</th>
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest text-center">פעולות</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-slate-50">
|
||||||
{(customer.extensions || []).map(e => {
|
{(customer.extensions || []).map(e => {
|
||||||
const installed = e.installed_version
|
const installed = e.installed_version
|
||||||
const hasUpdate = installed && e.latest_version && installed !== e.latest_version
|
const hasUpdate = installed && e.latest_version && installed !== e.latest_version
|
||||||
return (
|
return (
|
||||||
<tr key={e.id} className="hover:bg-gray-50">
|
<tr key={e.id} className="hover:bg-slate-50/80 transition-all">
|
||||||
<td className="px-4 py-3 font-medium">{e.name}</td>
|
<td className="px-8 py-5">
|
||||||
<td className="px-4 py-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${installed ? 'bg-blue-50 text-blue-700' : 'bg-slate-100 text-slate-400'}`}>
|
||||||
|
<Icon name="extension" className="text-base" />
|
||||||
|
</div>
|
||||||
|
<span className="font-bold text-slate-900">{e.name}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-8 py-5">
|
||||||
{installed
|
{installed
|
||||||
? <Badge>v{installed}</Badge>
|
? <VersionBadge>v{installed}</VersionBadge>
|
||||||
: <span className="text-sm text-red-500">לא מותקן</span>}
|
: <span className="text-sm text-red-500 font-medium">לא מותקן</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-8 py-5">
|
||||||
{hasUpdate
|
{hasUpdate
|
||||||
? <span className="text-sm font-medium text-amber-600">v{e.latest_version}</span>
|
? <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-amber-100 text-amber-700">v{e.latest_version}</span>
|
||||||
: <span className="text-sm text-gray-300">—</span>}
|
: <span className="text-sm text-slate-300">—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-8 py-5">
|
||||||
<div className="flex gap-2 justify-end">
|
<div className="flex items-center justify-center gap-1">
|
||||||
{!installed ? (
|
{!installed ? (
|
||||||
<button onClick={() => deploy(e.name, e.latest_version)}
|
<button onClick={() => deploy(e.name, e.latest_version)}
|
||||||
disabled={deploying === e.name}
|
disabled={deploying === e.name}
|
||||||
className="bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600 disabled:opacity-50">
|
className="deep-action-gradient text-white px-4 py-1.5 rounded-lg text-xs font-bold shadow-sm active:scale-95 transition-all disabled:opacity-50">
|
||||||
{deploying === e.name ? '...' : 'התקנה'}
|
{deploying === e.name ? '...' : 'התקנה'}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
@@ -225,13 +292,15 @@ function CustomerDetail({ customerId }) {
|
|||||||
{hasUpdate && (
|
{hasUpdate && (
|
||||||
<button onClick={() => deploy(e.name, e.latest_version)}
|
<button onClick={() => deploy(e.name, e.latest_version)}
|
||||||
disabled={deploying === e.name}
|
disabled={deploying === e.name}
|
||||||
className="bg-amber-500 text-white px-3 py-1 rounded text-sm hover:bg-amber-600 disabled:opacity-50">
|
className="p-2 text-amber-500 hover:text-amber-700 hover:bg-amber-50 rounded-lg transition-all"
|
||||||
{deploying === e.name ? '...' : 'עדכון'}
|
title="עדכון">
|
||||||
|
<Icon name="upgrade" className="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => remove(e.id)}
|
<button onClick={() => remove(e.id)}
|
||||||
className="bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600">
|
className="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-all"
|
||||||
הסרה
|
title="הסרה">
|
||||||
|
<Icon name="delete" className="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -242,13 +311,13 @@ function CustomerDetail({ customerId }) {
|
|||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{available.length > 0 && (
|
{available.length > 0 && (
|
||||||
<div className="mt-4">
|
<div className="mt-6 bg-white rounded-xl p-6">
|
||||||
<h4 className="text-sm font-medium text-gray-500 mb-2">הוסף הרחבה:</h4>
|
<h4 className="text-sm font-bold text-slate-400 uppercase tracking-widest mb-3">הוסף הרחבה</h4>
|
||||||
<select onChange={e => e.target.value && assign(parseInt(e.target.value))}
|
<select onChange={e => e.target.value && assign(parseInt(e.target.value))}
|
||||||
className="border rounded px-3 py-2 w-full">
|
className="bg-slate-50 border-none rounded-xl px-4 py-2.5 w-full text-sm focus:ring-2 focus:ring-blue-500">
|
||||||
<option value="">בחר הרחבה...</option>
|
<option value="">בחר הרחבה...</option>
|
||||||
{available.map(e => <option key={e.id} value={e.id}>{e.name} v{e.latest_version}</option>)}
|
{available.map(e => <option key={e.id} value={e.id}>{e.name} v{e.latest_version}</option>)}
|
||||||
</select>
|
</select>
|
||||||
@@ -263,73 +332,93 @@ function DeploymentsTab() {
|
|||||||
const { data: deployments } = useFetch(`${API}/deployments?limit=50`)
|
const { data: deployments } = useFetch(`${API}/deployments?limit=50`)
|
||||||
if (!deployments) return <Loader />
|
if (!deployments) return <Loader />
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
<section className="bg-white rounded-xl overflow-hidden">
|
||||||
<table className="w-full text-right">
|
<div className="p-8 flex justify-between items-center">
|
||||||
<thead className="bg-gray-50">
|
<div className="text-right">
|
||||||
<tr>
|
<h2 className="text-xl font-black text-slate-900">לוג פריסות</h2>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">לקוח</th>
|
<p className="text-sm text-slate-500 mt-1">היסטוריית כל הפריסות במערכת</p>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">הרחבה</th>
|
</div>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">גרסה</th>
|
</div>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">סטטוס</th>
|
<table className="w-full text-right border-collapse">
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">סוג</th>
|
<thead>
|
||||||
<th className="px-4 py-3 text-sm font-medium text-gray-500">זמן</th>
|
<tr className="bg-slate-50/50">
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">לקוח</th>
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">הרחבה</th>
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">גרסה</th>
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">סטטוס</th>
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">סוג</th>
|
||||||
|
<th className="px-8 py-4 text-[11px] font-bold text-slate-400 uppercase tracking-widest">זמן</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-slate-50">
|
||||||
{deployments.map(d => (
|
{deployments.map(d => (
|
||||||
<tr key={d.id} className="hover:bg-gray-50">
|
<tr key={d.id} className="hover:bg-slate-50/80 transition-all">
|
||||||
<td className="px-4 py-3">{d.customer_name}</td>
|
<td className="px-8 py-5 font-medium text-slate-900">{d.customer_name}</td>
|
||||||
<td className="px-4 py-3 font-medium">{d.extension_name}</td>
|
<td className="px-8 py-5">
|
||||||
<td className="px-4 py-3"><Badge>{d.version}</Badge></td>
|
<div className="flex items-center gap-2">
|
||||||
<td className="px-4 py-3">
|
<div className="w-6 h-6 rounded bg-blue-50 flex items-center justify-center text-blue-700">
|
||||||
<span className={`px-2 py-1 rounded text-xs ${d.status === 'ok' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
<Icon name="extension" className="text-sm" />
|
||||||
|
</div>
|
||||||
|
<span className="font-bold text-slate-900">{d.extension_name}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-8 py-5"><VersionBadge>{d.version}</VersionBadge></td>
|
||||||
|
<td className="px-8 py-5">
|
||||||
|
<span className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-bold ${d.status === 'ok' ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}>
|
||||||
|
<Icon name={d.status === 'ok' ? 'check' : 'close'} className="text-xs" />
|
||||||
{d.status === 'ok' ? 'הצלחה' : 'שגיאה'}
|
{d.status === 'ok' ? 'הצלחה' : 'שגיאה'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-gray-500">{d.trigger_type === 'auto' ? 'אוטומטי' : 'ידני'}</td>
|
<td className="px-8 py-5">
|
||||||
<td className="px-4 py-3 text-sm text-gray-400" dir="ltr">{d.started_at?.slice(0, 19)}</td>
|
<span className={`text-xs font-medium px-2 py-0.5 rounded ${d.trigger_type === 'auto' ? 'bg-blue-50 text-blue-600' : 'bg-slate-100 text-slate-600'}`}>
|
||||||
|
{d.trigger_type === 'auto' ? 'אוטומטי' : 'ידני'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-8 py-5 text-sm text-slate-400 font-mono" dir="ltr">{d.started_at?.slice(0, 16).replace('T', ' ')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Shared Components ---
|
// --- Shared Components ---
|
||||||
function Badge({ children }) {
|
function VersionBadge({ children }) {
|
||||||
return <span className="bg-gray-100 text-gray-800 px-2 py-0.5 rounded text-sm font-mono">{children}</span>
|
return <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold bg-blue-100 text-blue-700" dir="ltr">{children}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
function Loader() {
|
function Loader() {
|
||||||
return <div className="text-center py-8 text-gray-400">טוען...</div>
|
return <div className="text-center py-12 text-slate-400"><Icon name="hourglass_empty" className="text-3xl animate-spin" /></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Main App ---
|
// --- Main App ---
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [tab, setTab] = useState('extensions')
|
const [tab, setTab] = useState('extensions')
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'extensions', label: 'הרחבות' },
|
{ id: 'extensions', label: 'הרחבות', icon: 'extension' },
|
||||||
{ id: 'customers', label: 'לקוחות' },
|
{ id: 'customers', label: 'לקוחות', icon: 'group' },
|
||||||
{ id: 'deployments', label: 'פריסות' },
|
{ id: 'deployments', label: 'פריסות', icon: 'analytics' },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100" dir="rtl">
|
<div className="min-h-screen bg-[#f8f9fb] text-[#191c1e]" dir="rtl">
|
||||||
<header className="bg-white shadow-sm">
|
<header className="glass-header sticky top-0 z-50 flex justify-between items-center w-full px-8 py-4">
|
||||||
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
|
<div className="flex items-center gap-8">
|
||||||
<h1 className="text-xl font-bold text-gray-800">Extension Platform</h1>
|
<span className="text-xl font-black text-blue-700 tracking-tight">Extension Platform</span>
|
||||||
<nav className="flex gap-2">
|
<nav className="hidden md:flex items-center gap-1">
|
||||||
{tabs.map(t => (
|
{tabs.map(t => (
|
||||||
<button key={t.id} onClick={() => setTab(t.id)}
|
<button key={t.id} onClick={() => setTab(t.id)}
|
||||||
className={`px-4 py-2 rounded text-sm font-medium ${tab === t.id ? 'bg-blue-500 text-white' : 'text-gray-600 hover:bg-gray-100'}`}>
|
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-bold transition-all
|
||||||
|
${tab === t.id ? 'text-blue-700 bg-blue-50' : 'text-slate-500 hover:bg-slate-100'}`}>
|
||||||
|
<Icon name={t.icon} className="text-lg" />
|
||||||
{t.label}
|
{t.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="max-w-7xl mx-auto px-6 py-6">
|
<main className="p-8 max-w-[1400px] w-full mx-auto space-y-8">
|
||||||
<StatsBar />
|
<StatsBar />
|
||||||
{tab === 'extensions' && <ExtensionsTab />}
|
{tab === 'extensions' && <ExtensionsTab />}
|
||||||
{tab === 'customers' && <CustomersTab />}
|
{tab === 'customers' && <CustomersTab />}
|
||||||
|
|||||||
@@ -1 +1,22 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Heebo', sans-serif;
|
||||||
|
background: #f8f9fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-header {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deep-action-gradient {
|
||||||
|
background: linear-gradient(135deg, #0058be 0%, #2170e4 100%);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user