import { DeleteOutlined, EditOutlined, GlobalOutlined, KeyOutlined, PlusOutlined, UserOutlined, ApiOutlined, } from '@ant-design/icons' import { Badge, Breadcrumb, Button, Col, Divider, Form, Input, InputNumber, Modal, Popconfirm, Row, Select, Space, Switch, Table, Tag, Tabs, Typography, message, } from 'antd' import { useState } from 'react' import { useAdminUsers, useCreateAdminUser, useDeleteAdminUser, useGlobalSSHCredentials, useCreateGlobalSSHCredential, useDeleteGlobalSSHCredential, usePlugins, useUpdateAdminUser, useUpdatePlugin, type GlobalSSHCredentialItem, type UserItem, } from '../api/admin' import { useSSHKeys, useCreateSSHKey, useDeleteSSHKey, type SSHKeyItem } from '../api/credentials' import { stripEmpty } from '../utils/form' // ── Plugins Tab ─────────────────────────────────────────────────────────────── function PluginsTab() { const { data: plugins, isLoading } = usePlugins() const updatePlugin = useUpdatePlugin() const toggle = async (name: string, enabled: boolean) => { try { await updatePlugin.mutateAsync({ name, enabled }) message.success(enabled ? `Плагин «${name}» включён` : `Плагин «${name}» отключён`) } catch { message.error('Ошибка при обновлении плагина') } } const columns = [ { title: 'Название', key: 'display_name', render: (_: unknown, row: (typeof plugins)[0]) => ( {row.display_name} {row.name} ), }, { title: 'Описание', dataIndex: 'description', key: 'description', render: (v: string | null) => v ?? '—', }, { title: 'Категории', dataIndex: 'compatible_categories', key: 'categories', render: (cats: string[]) => cats.length === 0 ? ( Все ) : ( cats.map((c) => {c}) ), }, { title: 'Статус', key: 'enabled', width: 100, render: (_: unknown, row: (typeof plugins)[0]) => ( toggle(row.name, val)} loading={updatePlugin.isPending} checkedChildren="Вкл" unCheckedChildren="Откл" /> ), }, ] return ( ) } // ── Users Tab ───────────────────────────────────────────────────────────────── function UsersTab() { const { data: users, isLoading } = useAdminUsers() const createUser = useCreateAdminUser() const updateUser = useUpdateAdminUser() const deleteUser = useDeleteAdminUser() const [createModal, setCreateModal] = useState(false) const [editTarget, setEditTarget] = useState(null) const [form] = Form.useForm() const openCreate = () => { form.resetFields() setEditTarget(null) setCreateModal(true) } const openEdit = (user: UserItem) => { form.resetFields() form.setFieldsValue({ email: user.email ?? '', full_name: user.full_name ?? '', role: user.role, is_active: user.is_active, }) setEditTarget(user) setCreateModal(true) } const handleSave = async () => { const values = await form.validateFields() const payload = stripEmpty(values) try { if (editTarget) { await updateUser.mutateAsync({ id: editTarget.id, body: payload as any }) message.success('Пользователь обновлён') } else { await createUser.mutateAsync(payload as any) message.success('Пользователь создан') } setCreateModal(false) setEditTarget(null) } catch (err: any) { const detail = err?.response?.data?.detail message.error(detail ?? 'Ошибка при сохранении') } } const handleDelete = async (id: number) => { try { await deleteUser.mutateAsync(id) message.success('Пользователь удалён') } catch (err: any) { message.error(err?.response?.data?.detail ?? 'Ошибка при удалении') } } const columns = [ { title: 'Логин', dataIndex: 'username', key: 'username', render: (v: string) => {v}, }, { title: 'Email', dataIndex: 'email', key: 'email', render: (v: string | null) => v ?? '—' }, { title: 'Роль', dataIndex: 'role', key: 'role', render: (v: string) => ( {v} ), }, { title: 'Статус', dataIndex: 'is_active', key: 'is_active', render: (v: boolean) => v ? : , }, { title: '', key: 'actions', width: 100, render: (_: unknown, row: UserItem) => (
{ setCreateModal(false); setEditTarget(null) }} onOk={handleSave} confirmLoading={createUser.isPending || updateUser.isPending} okText={editTarget ? 'Сохранить' : 'Создать'} >
{!editTarget && ( <> )} {editTarget && ( )}
setModalOpen(false)} onOk={handleCreate} confirmLoading={createKey.isPending} okText="Зарегистрировать" > ) } // ── Global SSH Credentials Tab ──────────────────────────────────────────────── function GlobalCredsTab() { const { data: creds, isLoading } = useGlobalSSHCredentials() const { data: sshKeys } = useSSHKeys() const createCred = useCreateGlobalSSHCredential() const deleteCred = useDeleteGlobalSSHCredential() const [form] = Form.useForm() const [modalOpen, setModalOpen] = useState(false) const handleCreate = async () => { const values = await form.validateFields() const payload = stripEmpty(values) try { await createCred.mutateAsync(payload as any) message.success('Учётные данные добавлены') setModalOpen(false) form.resetFields() } catch (err: any) { message.error(err?.response?.data?.detail ?? 'Ошибка при создании') } } const columns = [ { title: 'Тип', dataIndex: 'type', key: 'type', width: 90, render: (v: string) => {v === 'key' ? 'SSH-ключ' : 'Пароль'}, }, { title: 'Логин', dataIndex: 'username', key: 'username', render: (v: string) => {v}, }, { title: 'SSH-ключ', dataIndex: 'ssh_key_id', key: 'ssh_key_id', render: (v: number | null) => { if (!v) return '—' const key = (sshKeys ?? []).find((k) => k.id === v) return key ? {key.name} : key_id={v} }, }, { title: 'Приоритет', dataIndex: 'priority', key: 'priority', width: 100, }, { title: 'Описание', dataIndex: 'description', key: 'description', render: (v: string | null) => v ?? '—', }, { title: '', key: 'actions', width: 60, render: (_: unknown, row: GlobalSSHCredentialItem) => ( deleteCred.mutateAsync(row.id).catch(() => message.error('Ошибка при удалении'))} >
setModalOpen(false)} onOk={handleCreate} confirmLoading={createCred.isPending} okText="Добавить" >
prev.type !== cur.type}> {({ getFieldValue }) => getFieldValue('type') === 'key' ? ( ) } // ── Main Page ───────────────────────────────────────────────────────────────── export function AdminPage() { return (
Администрирование Плагины ), children: , }, { key: 'users', label: ( Пользователи ), children: , }, { key: 'ssh-keys', label: ( SSH-ключи ), children: , }, { key: 'global-creds', label: ( Глобальные SSH-кредентиалы ), children: , }, ]} />
) }