mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-04 23:06:22 -04:00
feat(ui): add Cluster page composing distributed and swarm sections
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
27
core/http/react-ui/src/components/ClusterSection.jsx
Normal file
27
core/http/react-ui/src/components/ClusterSection.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
// ClusterSection is a collapsible, titled container for one capability area of
|
||||
// the Cluster page (Distributed / Swarm). Default expanded.
|
||||
export default function ClusterSection({ icon, title, subtitle, defaultOpen = true, children }) {
|
||||
const [open, setOpen] = useState(defaultOpen)
|
||||
return (
|
||||
<section className="card" style={{ marginBottom: 'var(--spacing-lg)' }}>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)',
|
||||
width: '100%', padding: 'var(--spacing-md)', background: 'none',
|
||||
border: 'none', cursor: 'pointer', textAlign: 'left', color: 'inherit',
|
||||
}}
|
||||
>
|
||||
<i className={`fas fa-chevron-${open ? 'down' : 'right'}`} style={{ width: '1rem', color: 'var(--color-text-muted)' }} />
|
||||
{icon && <i className={icon} style={{ color: 'var(--color-primary)' }} />}
|
||||
<span style={{ fontWeight: 600 }}>{title}</span>
|
||||
{subtitle && <span style={{ marginLeft: 'auto', color: 'var(--color-text-muted)', fontSize: '0.875rem' }}>{subtitle}</span>}
|
||||
</button>
|
||||
{open && <div style={{ padding: '0 var(--spacing-md) var(--spacing-md)' }}>{children}</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
44
core/http/react-ui/src/components/ClusterSummary.jsx
Normal file
44
core/http/react-ui/src/components/ClusterSummary.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import StatCard from './StatCard'
|
||||
import { nodesApi, p2pApi } from '../utils/api'
|
||||
|
||||
// ClusterSummary shows merged totals across both transports. Self-contained
|
||||
// (own lightweight fetch) so the page composes without lifting state out of the
|
||||
// two large section components.
|
||||
export default function ClusterSummary({ distributedEnabled, p2pEnabled }) {
|
||||
const { t } = useTranslation('admin')
|
||||
const [nats, setNats] = useState({ nodes: 0, inFlight: 0 })
|
||||
const [swarm, setSwarm] = useState({ online: 0, total: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
async function load() {
|
||||
if (distributedEnabled) {
|
||||
try {
|
||||
const list = await nodesApi.list()
|
||||
const nodes = Array.isArray(list) ? list : (list?.nodes ?? [])
|
||||
if (active) setNats({ nodes: nodes.length, inFlight: nodes.reduce((a, n) => a + (n.in_flight_count || 0), 0) })
|
||||
} catch { /* leave zeros */ }
|
||||
}
|
||||
if (p2pEnabled) {
|
||||
try {
|
||||
const stats = await p2pApi.getStats()
|
||||
const online = (stats?.federated?.online || 0) + (stats?.llama_cpp_workers?.online || 0) + (stats?.mlx_workers?.online || 0)
|
||||
const total = (stats?.federated?.total || 0) + (stats?.llama_cpp_workers?.total || 0) + (stats?.mlx_workers?.total || 0)
|
||||
if (active) setSwarm({ online, total })
|
||||
} catch { /* leave zeros */ }
|
||||
}
|
||||
}
|
||||
load()
|
||||
return () => { active = false }
|
||||
}, [distributedEnabled, p2pEnabled])
|
||||
|
||||
return (
|
||||
<div className="stat-grid" style={{ marginBottom: 'var(--spacing-lg)' }}>
|
||||
{distributedEnabled && <StatCard icon="fas fa-network-wired" label={t('cluster.summary.nodes', 'Distributed nodes')} value={nats.nodes} />}
|
||||
{distributedEnabled && <StatCard icon="fas fa-bolt" label={t('cluster.summary.inFlight', 'In-flight requests')} value={nats.inFlight} />}
|
||||
{p2pEnabled && <StatCard icon="fas fa-circle-nodes" label={t('cluster.summary.peers', 'Swarm peers online')} value={`${swarm.online}/${swarm.total}`} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
45
core/http/react-ui/src/pages/Cluster.jsx
Normal file
45
core/http/react-ui/src/pages/Cluster.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDistributedMode } from '../hooks/useDistributedMode'
|
||||
import { useP2PMode } from '../hooks/useP2PMode'
|
||||
import ClusterSection from '../components/ClusterSection'
|
||||
import ClusterSummary from '../components/ClusterSummary'
|
||||
import Nodes from './Nodes'
|
||||
import P2P from './P2P'
|
||||
|
||||
export default function Cluster() {
|
||||
const { t } = useTranslation('admin')
|
||||
const distributed = useDistributedMode()
|
||||
const p2p = useP2PMode()
|
||||
|
||||
const loading = distributed.loading || p2p.loading
|
||||
const nothingEnabled = !loading && !distributed.enabled && !p2p.enabled
|
||||
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-network-wired" /> {t('cluster.title', 'Cluster')}</h1>
|
||||
<p className="page-subtitle">{t('cluster.subtitle', 'Distributed and peer-to-peer nodes serving this instance')}</p>
|
||||
</div>
|
||||
|
||||
{!loading && <ClusterSummary distributedEnabled={distributed.enabled} p2pEnabled={p2p.enabled} />}
|
||||
|
||||
{distributed.enabled && (
|
||||
<ClusterSection icon="fas fa-network-wired" title={t('cluster.distributed.title', 'Distributed (NATS)')} defaultOpen>
|
||||
<Nodes embedded />
|
||||
</ClusterSection>
|
||||
)}
|
||||
|
||||
{p2p.enabled && (
|
||||
<ClusterSection icon="fas fa-circle-nodes" title={t('cluster.swarm.title', 'Swarm (p2p)')} defaultOpen={!distributed.enabled}>
|
||||
<P2P embedded />
|
||||
</ClusterSection>
|
||||
)}
|
||||
|
||||
{nothingEnabled && (
|
||||
<div className="card" style={{ padding: 'var(--spacing-lg)', textAlign: 'center', color: 'var(--color-text-muted)' }}>
|
||||
{t('cluster.empty', 'No distributed or p2p clustering is enabled. Start LocalAI in distributed or federated/p2p mode to manage cluster nodes here.')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user