mirror of
https://github.com/mudler/LocalAI.git
synced 2026-04-16 12:59:33 -04:00
feat(ui): add backend version display and upgrade support
- Add upgrade check/trigger API endpoints to config and api module - Backends page: version badge, upgrade indicator, upgrade button - Manage page: version in metadata, context-aware upgrade/reinstall button - Settings page: auto-upgrade backends toggle
This commit is contained in:
@@ -26,6 +26,7 @@ export default function Backends() {
|
||||
const [expandedRow, setExpandedRow] = useState(null)
|
||||
const [confirmDialog, setConfirmDialog] = useState(null)
|
||||
const [allBackends, setAllBackends] = useState([])
|
||||
const [upgrades, setUpgrades] = useState({})
|
||||
|
||||
const fetchBackends = useCallback(async () => {
|
||||
try {
|
||||
@@ -52,6 +53,13 @@ export default function Backends() {
|
||||
if (!loading) fetchBackends()
|
||||
}, [operations.length])
|
||||
|
||||
// Fetch available upgrades
|
||||
useEffect(() => {
|
||||
backendsApi.checkUpgrades()
|
||||
.then(data => setUpgrades(data || {}))
|
||||
.catch(() => {})
|
||||
}, [operations.length])
|
||||
|
||||
// Client-side filtering by tag
|
||||
const filteredBackends = filter
|
||||
? allBackends.filter(b => {
|
||||
@@ -114,6 +122,15 @@ export default function Backends() {
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpgrade = async (id) => {
|
||||
try {
|
||||
await backendsApi.upgrade(id)
|
||||
addToast(`Upgrading ${id}...`, 'info')
|
||||
} catch (err) {
|
||||
addToast(`Upgrade failed: ${err.message}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleManualInstall = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!manualUri.trim()) { addToast('Please enter a URI', 'warning'); return }
|
||||
@@ -179,6 +196,14 @@ export default function Backends() {
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>Installed</div>
|
||||
</a>
|
||||
</div>
|
||||
{Object.keys(upgrades).length > 0 && (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-warning)' }}>
|
||||
{Object.keys(upgrades).length}
|
||||
</div>
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>Updates</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<a className="btn btn-secondary btn-sm" href="https://localai.io/docs/getting-started/manual/" target="_blank" rel="noopener noreferrer">
|
||||
<i className="fas fa-book" /> Docs
|
||||
@@ -300,6 +325,11 @@ export default function Backends() {
|
||||
{/* Name */}
|
||||
<td>
|
||||
<span style={{ fontWeight: 500 }}>{b.name || b.id}</span>
|
||||
{b.version && (
|
||||
<span className="badge" style={{ fontSize: '0.625rem', marginLeft: 4, background: 'var(--color-bg-tertiary)', color: 'var(--color-text-secondary)' }}>
|
||||
v{b.version}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Description */}
|
||||
@@ -346,9 +376,17 @@ export default function Backends() {
|
||||
</span>
|
||||
</div>
|
||||
) : b.installed ? (
|
||||
<span className="badge badge-success">
|
||||
<i className="fas fa-check" style={{ fontSize: '0.5rem', marginRight: 2 }} /> Installed
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<span className="badge badge-success">
|
||||
<i className="fas fa-check" style={{ fontSize: '0.5rem', marginRight: 2 }} /> Installed
|
||||
</span>
|
||||
{upgrades[b.name] && (
|
||||
<span className="badge" style={{ fontSize: '0.625rem', background: '#fef3cd', color: '#856404' }}>
|
||||
<i className="fas fa-arrow-up" style={{ fontSize: '0.5rem', marginRight: 2 }} />
|
||||
{upgrades[b.name].available_version ? `v${upgrades[b.name].available_version}` : 'Update'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="badge" style={{ background: 'var(--color-bg-tertiary)', color: 'var(--color-text-muted)' }}>
|
||||
<i className="fas fa-circle" style={{ fontSize: '0.5rem', marginRight: 2 }} /> Not Installed
|
||||
@@ -361,9 +399,15 @@ export default function Backends() {
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-xs)', justifyContent: 'flex-end' }} onClick={e => e.stopPropagation()}>
|
||||
{b.installed ? (
|
||||
<>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleInstall(b.name || b.id)} title="Reinstall" disabled={isProcessing}>
|
||||
<i className={`fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-rotate'}`} />
|
||||
</button>
|
||||
{upgrades[b.name] ? (
|
||||
<button className="btn btn-primary btn-sm" onClick={() => handleUpgrade(b.name || b.id)} title={`Upgrade to ${upgrades[b.name]?.available_version ? 'v' + upgrades[b.name].available_version : 'latest'}`} disabled={isProcessing}>
|
||||
<i className={`fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-arrow-up'}`} />
|
||||
</button>
|
||||
) : (
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => handleInstall(b.name || b.id)} title="Reinstall" disabled={isProcessing}>
|
||||
<i className={`fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-rotate'}`} />
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(b.name || b.id)} title="Delete" disabled={isProcessing}>
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function Manage() {
|
||||
const [backendsLoading, setBackendsLoading] = useState(true)
|
||||
const [reloading, setReloading] = useState(false)
|
||||
const [reinstallingBackends, setReinstallingBackends] = useState(new Set())
|
||||
const [upgrades, setUpgrades] = useState({})
|
||||
const [confirmDialog, setConfirmDialog] = useState(null)
|
||||
const [distributedMode, setDistributedMode] = useState(false)
|
||||
const [togglingModels, setTogglingModels] = useState(new Set())
|
||||
@@ -62,6 +63,15 @@ export default function Manage() {
|
||||
nodesApi.list().then(() => setDistributedMode(true)).catch(() => {})
|
||||
}, [fetchLoadedModels, fetchBackends])
|
||||
|
||||
// Fetch available backend upgrades
|
||||
useEffect(() => {
|
||||
if (activeTab === 'backends') {
|
||||
backendsApi.checkUpgrades()
|
||||
.then(data => setUpgrades(data || {}))
|
||||
.catch(() => {})
|
||||
}
|
||||
}, [activeTab])
|
||||
|
||||
const handleStopModel = (modelName) => {
|
||||
setConfirmDialog({
|
||||
title: 'Stop Model',
|
||||
@@ -169,6 +179,22 @@ export default function Manage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpgradeBackend = async (name) => {
|
||||
try {
|
||||
setReinstallingBackends(prev => new Set(prev).add(name))
|
||||
await backendsApi.upgrade(name)
|
||||
addToast(`Upgrading ${name}...`, 'info')
|
||||
} catch (err) {
|
||||
addToast(`Failed to upgrade: ${err.message}`, 'error')
|
||||
} finally {
|
||||
setReinstallingBackends(prev => {
|
||||
const next = new Set(prev)
|
||||
next.delete(name)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteBackend = (name) => {
|
||||
setConfirmDialog({
|
||||
title: 'Delete Backend',
|
||||
@@ -471,6 +497,17 @@ export default function Manage() {
|
||||
For: <span style={{ color: 'var(--color-accent)' }}>{backend.Metadata.meta_backend_for}</span>
|
||||
</span>
|
||||
)}
|
||||
{backend.Metadata?.version && (
|
||||
<span>
|
||||
<i className="fas fa-code-branch" style={{ fontSize: '0.5rem', marginRight: 4 }} />
|
||||
Version: <span style={{ color: 'var(--color-text-primary)' }}>v{backend.Metadata.version}</span>
|
||||
{upgrades[backend.Name] && (
|
||||
<span style={{ color: '#856404', marginLeft: 4 }}>
|
||||
→ v{upgrades[backend.Name].available_version}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{backend.Metadata?.installed_at && (
|
||||
<span>
|
||||
<i className="fas fa-calendar" style={{ fontSize: '0.5rem', marginRight: 4 }} />
|
||||
@@ -485,12 +522,12 @@ export default function Manage() {
|
||||
{!backend.IsSystem ? (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={() => handleReinstallBackend(backend.Name)}
|
||||
className={`btn ${upgrades[backend.Name] ? 'btn-primary' : 'btn-secondary'} btn-sm`}
|
||||
onClick={() => upgrades[backend.Name] ? handleUpgradeBackend(backend.Name) : handleReinstallBackend(backend.Name)}
|
||||
disabled={reinstallingBackends.has(backend.Name)}
|
||||
title="Reinstall"
|
||||
title={upgrades[backend.Name] ? `Upgrade to v${upgrades[backend.Name]?.available_version || 'latest'}` : 'Reinstall'}
|
||||
>
|
||||
<i className={`fas ${reinstallingBackends.has(backend.Name) ? 'fa-spinner fa-spin' : 'fa-rotate'}`} />
|
||||
<i className={`fas ${reinstallingBackends.has(backend.Name) ? 'fa-spinner fa-spin' : upgrades[backend.Name] ? 'fa-arrow-up' : 'fa-rotate'}`} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
|
||||
@@ -266,6 +266,9 @@ export default function Settings() {
|
||||
<SettingRow label="Max Active Backends" description="Maximum models to keep loaded simultaneously (0 = unlimited)">
|
||||
<input className="input" type="number" style={{ width: 120 }} value={settings.max_active_backends ?? ''} onChange={(e) => update('max_active_backends', parseInt(e.target.value) || 0)} placeholder="0" />
|
||||
</SettingRow>
|
||||
<SettingRow label="Auto-upgrade Backends" description="Automatically upgrade backends when new versions are detected">
|
||||
<Toggle checked={settings.auto_upgrade_backends} onChange={(v) => update('auto_upgrade_backends', v)} />
|
||||
</SettingRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
3
core/http/react-ui/src/utils/api.js
vendored
3
core/http/react-ui/src/utils/api.js
vendored
@@ -120,6 +120,9 @@ export const backendsApi = {
|
||||
installExternal: (body) => postJSON(API_CONFIG.endpoints.installExternalBackend, body),
|
||||
getJob: (uid) => fetchJSON(API_CONFIG.endpoints.backendJob(uid)),
|
||||
deleteInstalled: (name) => postJSON(API_CONFIG.endpoints.deleteInstalledBackend(name), {}),
|
||||
checkUpgrades: () => fetchJSON(API_CONFIG.endpoints.backendsUpgrades),
|
||||
forceCheckUpgrades: () => postJSON(API_CONFIG.endpoints.backendsUpgradesCheck, {}),
|
||||
upgrade: (name) => postJSON(API_CONFIG.endpoints.upgradeBackend(name), {}),
|
||||
}
|
||||
|
||||
// Chat API (non-streaming)
|
||||
|
||||
3
core/http/react-ui/src/utils/config.js
vendored
3
core/http/react-ui/src/utils/config.js
vendored
@@ -23,6 +23,9 @@ export const API_CONFIG = {
|
||||
installExternalBackend: '/api/backends/install-external',
|
||||
backendJob: (uid) => `/api/backends/job/${uid}`,
|
||||
deleteInstalledBackend: (name) => `/api/backends/system/delete/${name}`,
|
||||
backendsUpgrades: '/api/backends/upgrades',
|
||||
backendsUpgradesCheck: '/api/backends/upgrades/check',
|
||||
upgradeBackend: (name) => `/api/backends/upgrade/${name}`,
|
||||
|
||||
// Resources
|
||||
resources: '/api/resources',
|
||||
|
||||
Reference in New Issue
Block a user