фронт для дискавери отображения

This commit is contained in:
dv 2026-04-30 16:31:33 +03:00
parent e355c12ffa
commit 23da2fab55
4 changed files with 85 additions and 6 deletions

View file

@ -26,6 +26,7 @@ from app.schemas.object import (
ObjectUpdate,
SyncResult,
DiscoveryStarted,
DiscoveryStatus,
)
from app.services import crypto
from app.services.database import AsyncSessionLocal
@ -36,6 +37,22 @@ from app.services.sse import sse_manager, SSEEvent
router = APIRouter(prefix="/api/v1/objects", tags=["objects"])
_discovery_tasks: dict[int, asyncio.Task] = {}
def _discovery_task_id(obj_id: int) -> str:
return f"disc-{obj_id}"
def _is_discovery_running(obj_id: int) -> bool:
task = _discovery_tasks.get(obj_id)
return task is not None and not task.done()
def _forget_discovery_task(obj_id: int, task: asyncio.Task) -> None:
if _discovery_tasks.get(obj_id) is task:
_discovery_tasks.pop(obj_id, None)
def _apply_credentials(obj: Object, db_password: str | None, ssh_password: str | None) -> None:
if db_password is not None:
@ -300,9 +317,24 @@ async def discover_devices(
):
"""SSH into each rack PC, parse caps config, discover network devices."""
await _load_object(db, obj_id)
task_id = f"disc-{obj_id}"
asyncio.create_task(_run_discovery_background(obj_id, task_id))
return DiscoveryStarted(task_id=task_id)
task_id = _discovery_task_id(obj_id)
if _is_discovery_running(obj_id):
return DiscoveryStarted(task_id=task_id, running=True, already_running=True)
task = asyncio.create_task(_run_discovery_background(obj_id, task_id))
_discovery_tasks[obj_id] = task
task.add_done_callback(lambda done_task: _forget_discovery_task(obj_id, done_task))
return DiscoveryStarted(task_id=task_id, running=True, already_running=False)
@router.get("/{obj_id}/discover/status", response_model=DiscoveryStatus)
async def discovery_status(
obj_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
await _load_object(db, obj_id)
return DiscoveryStatus(task_id=_discovery_task_id(obj_id), running=_is_discovery_running(obj_id))
@router.post("/import/preview", response_model=ObjectImportPreview)

View file

@ -145,3 +145,10 @@ class SyncResult(BaseModel):
class DiscoveryStarted(BaseModel):
task_id: str
running: bool = True
already_running: bool = False
class DiscoveryStatus(BaseModel):
task_id: str
running: bool

View file

@ -250,6 +250,13 @@ export const useSyncObject = (objId: number) => {
export interface DiscoveryStarted {
task_id: string
running: boolean
already_running: boolean
}
export interface DiscoveryStatus {
task_id: string
running: boolean
}
export const useDiscoverObject = (objId: number) =>
@ -258,6 +265,15 @@ export const useDiscoverObject = (objId: number) =>
api.post<DiscoveryStarted>(`/api/v1/objects/${objId}/discover`).then((r) => r.data),
})
export const useDiscoveryStatus = (objId: number) =>
useQuery({
queryKey: ['discovery-status', objId],
queryFn: () =>
api.get<DiscoveryStatus>(`/api/v1/objects/${objId}/discover/status`).then((r) => r.data),
enabled: !!objId,
refetchInterval: (query) => (query.state.data?.running ? 2_000 : false),
})
export interface SheetsSyncPayload {
spreadsheet_id: string
sheet_name?: string

View file

@ -38,6 +38,7 @@ import {
useDeleteDevice,
useDeleteAllDevices,
useDiscoverObject,
useDiscoveryStatus,
useDevices,
useObject,
type DeviceItem,
@ -148,6 +149,7 @@ export function ObjectDetailPage({ defaultMode = 'inventory' }: { defaultMode?:
const { data: actions } = useActions()
const discoverMutation = useDiscoverObject(objId)
const discoveryStatus = useDiscoveryStatus(objId)
const deleteDevice = useDeleteDevice(objId)
const createRack = useCreateRack(objId)
const updateRack = useUpdateRack(objId)
@ -173,12 +175,31 @@ export function ObjectDetailPage({ defaultMode = 'inventory' }: { defaultMode?:
const [editRackForm] = Form.useForm()
const currentAction = actions?.find((a) => a.name === selectedAction)
const discoveryRunning = discoverMutation.isPending || !!discoveryTaskId || !!discoveryStatus.data?.running
useEffect(() => {
if (discoveryStatus.data?.running) {
setDiscoveryTaskId(discoveryStatus.data.task_id)
} else if (discoveryStatus.data && discoveryTaskId === discoveryStatus.data.task_id) {
setDiscoveryTaskId(null)
}
}, [discoveryStatus.data, discoveryTaskId])
const handleDiscover = async () => {
if (discoveryRunning) {
setDiscoveryTaskId(discoveryTaskId ?? discoveryStatus.data?.task_id ?? `disc-${objId}`)
setDiscoveryOpen(true)
return
}
try {
const { task_id } = await discoverMutation.mutateAsync()
const { task_id, already_running } = await discoverMutation.mutateAsync()
setDiscoveryTaskId(task_id)
setDiscoveryOpen(true)
queryClient.invalidateQueries({ queryKey: ['discovery-status', objId] })
if (already_running) {
message.info('SSH discovery уже выполняется')
}
} catch {
message.error('Ошибка SSH discovery')
}
@ -186,6 +207,8 @@ export function ObjectDetailPage({ defaultMode = 'inventory' }: { defaultMode?:
const handleDiscoveryComplete = () => {
queryClient.invalidateQueries({ queryKey: ['devices', objId] })
queryClient.invalidateQueries({ queryKey: ['discovery-status', objId] })
setDiscoveryTaskId(null)
}
const handleDeleteDevice = async (row: DeviceItem) => {
@ -630,9 +653,10 @@ export function ObjectDetailPage({ defaultMode = 'inventory' }: { defaultMode?:
<Col>
{mode === 'inventory' ? (
<Space wrap>
<Button icon={<ApiOutlined />} loading={discoverMutation.isPending} onClick={handleDiscover}>
SSH Discovery
<Button icon={<ApiOutlined />} loading={discoveryRunning} onClick={handleDiscover}>
{discoveryRunning ? 'SSH Discovery...' : 'SSH Discovery'}
</Button>
{discoveryRunning && <Tag color="processing">выполняется</Tag>}
<Button icon={<PlusOutlined />} type="primary" onClick={() => { setEditDevice(null); setDeviceModal(true) }}>
Добавить устройство
</Button>