(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) => (
} onClick={() => openEdit(row)} />
handleDelete(row.id)}
>
} />
),
},
]
return (
<>
} onClick={openCreate}>
Создать пользователя
{ setCreateModal(false); setEditTarget(null) }}
onOk={handleSave}
confirmLoading={createUser.isPending || updateUser.isPending}
okText={editTarget ? 'Сохранить' : 'Создать'}
>
>
)}
{editTarget && (
)}
{editTarget && (
)}
>
)
}
// ── SSH Keys Tab ──────────────────────────────────────────────────────────────
function SSHKeysTab() {
const { data: keys, isLoading } = useSSHKeys()
const createKey = useCreateSSHKey()
const deleteKey = useDeleteSSHKey()
const [form] = Form.useForm()
const [modalOpen, setModalOpen] = useState(false)
const handleCreate = async () => {
const values = await form.validateFields()
try {
await createKey.mutateAsync(values)
message.success('Ключ зарегистрирован')
setModalOpen(false)
form.resetFields()
} catch (err: any) {
message.error(err?.response?.data?.detail ?? 'Ошибка при создании')
}
}
const columns = [
{ title: 'Название', dataIndex: 'name', key: 'name' },
{ title: 'Путь на сервере', dataIndex: 'path', key: 'path', render: (v: string) => {v} },
{ title: 'Описание', dataIndex: 'description', key: 'description' },
{
title: '',
key: 'actions',
width: 60,
render: (_: unknown, row: SSHKeyItem) => (
deleteKey.mutateAsync(row.id).catch(() => message.error('Ошибка при удалении'))}
>
} />
),
},
]
return (
<>
} onClick={() => { form.resetFields(); setModalOpen(true) }}>
Зарегистрировать ключ
Файлы SSH-ключей должны находиться на сервере. Укажите путь к существующему файлу.
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('Ошибка при удалении'))}
>
} />
),
},
]
return (
<>
Глобальные SSH-кредентиалы используются как последний резерв при подключении к устройствам,
если ни объектные, ни device-level настройки не подошли.
} onClick={() => { form.resetFields(); setModalOpen(true) }}>
Добавить
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: ,
},
]}
/>
)
}