плагин задача объекты

This commit is contained in:
dv 2026-04-07 18:48:21 +03:00
parent 74fc294d04
commit 9dde2252f0
2 changed files with 161 additions and 8 deletions

View file

@ -64,11 +64,15 @@ async def _resolve_scope(scope: dict) -> list[tuple[Device, Object]]:
query = query.where(Device.rack_id.in_(rack_ids)) query = query.where(Device.rack_id.in_(rack_ids))
elif scope_type == "category": elif scope_type == "category":
query = query.where(Device.category == scope["value"]) query = query.where(Device.category == scope["value"])
if "object_id" in scope: if "object_ids" in scope:
query = query.where(Device.object_id.in_(scope["object_ids"]))
elif "object_id" in scope:
query = query.where(Device.object_id == scope["object_id"]) query = query.where(Device.object_id == scope["object_id"])
elif scope_type == "role": elif scope_type == "role":
query = query.where(Device.role == scope["value"]) query = query.where(Device.role == scope["value"])
if "object_id" in scope: if "object_ids" in scope:
query = query.where(Device.object_id.in_(scope["object_ids"]))
elif "object_id" in scope:
query = query.where(Device.object_id == scope["object_id"]) query = query.where(Device.object_id == scope["object_id"])
else: else:
return [] return []

View file

@ -1,16 +1,69 @@
import { Table, Typography } from 'antd' import { PlusOutlined } from '@ant-design/icons'
import { Button, Form, Input, Modal, Select, Space, Table, Typography, message } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useState } from 'react' import { useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useTasks, type Task } from '../api/tasks' import { useObjects } from '../api/objects'
import { useActions, useCreateTask, useTasks, type Task } from '../api/tasks'
import { TaskStatusBadge } from '../components/StatusBadge' import { TaskStatusBadge } from '../components/StatusBadge'
const PAGE_SIZE = 50 const PAGE_SIZE = 50
const CATEGORY_OPTIONS = [
{ value: 'server', label: 'Сервер' },
{ value: 'raspberry', label: 'Raspberry Pi' },
{ value: 'mikrotik', label: 'Mikrotik' },
{ value: 'camera', label: 'Камера' },
{ value: 'embedded', label: 'Embedded' },
{ value: 'other', label: 'Другое' },
]
export function TasksPage() { export function TasksPage() {
const navigate = useNavigate() const navigate = useNavigate()
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const { data: tasks, isLoading } = useTasks(page, PAGE_SIZE) const { data: tasks, isLoading } = useTasks(page, PAGE_SIZE)
const { data: actions } = useActions()
const { data: objects } = useObjects()
const createTask = useCreateTask()
const [modalOpen, setModalOpen] = useState(false)
const [form] = Form.useForm()
// watched form values for conditional rendering
const scopeType: string = Form.useWatch('scope_type', form)
const selectedAction: string = Form.useWatch('action', form)
const currentAction = actions?.find((a) => a.name === selectedAction)
const openModal = () => {
form.resetFields()
setModalOpen(true)
}
const handleSubmit = async () => {
const values = await form.validateFields()
const { action, scope_type, scope_category, scope_object_ids, scope_object_id, ...params } = values
let target_scope: object
if (scope_type === 'category') {
if (scope_object_ids?.length > 0) {
target_scope = { type: 'category', value: scope_category, object_ids: scope_object_ids }
} else {
target_scope = { type: 'category', value: scope_category }
}
} else {
// scope_type === 'object'
target_scope = { type: 'object', id: scope_object_id }
}
try {
const task = await createTask.mutateAsync({ type: action, target_scope, params })
message.success('Задача создана')
setModalOpen(false)
navigate(`/tasks/${task.id}`)
} catch {
message.error('Ошибка создания задачи')
}
}
const columns = [ const columns = [
{ {
@ -46,11 +99,19 @@ export function TasksPage() {
}, },
] ]
const objectOptions = (objects ?? []).map((o) => ({ value: o.id, label: o.name }))
return ( return (
<div> <div>
<Typography.Title level={4} style={{ marginBottom: 16 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<Typography.Title level={4} style={{ margin: 0 }}>
Задачи Задачи
</Typography.Title> </Typography.Title>
<Button type="primary" icon={<PlusOutlined />} onClick={openModal}>
Новая задача
</Button>
</div>
<Table <Table
dataSource={tasks ?? []} dataSource={tasks ?? []}
columns={columns} columns={columns}
@ -61,13 +122,101 @@ export function TasksPage() {
current: page, current: page,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
onChange: setPage, onChange: setPage,
// Backend doesn't return total count — hide size changer, show simple pagination
showSizeChanger: false, showSizeChanger: false,
simple: true, simple: true,
}} }}
onRow={(row) => ({ onClick: () => navigate(`/tasks/${row.id}`) })} onRow={(row) => ({ onClick: () => navigate(`/tasks/${row.id}`) })}
rowClassName={() => 'cursor-pointer'} rowClassName={() => 'cursor-pointer'}
/> />
<Modal
title="Новая задача"
open={modalOpen}
onCancel={() => setModalOpen(false)}
onOk={handleSubmit}
confirmLoading={createTask.isPending}
okText="Запустить"
width={520}
destroyOnClose
>
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
{/* Действие */}
<Form.Item name="action" label="Действие" rules={[{ required: true, message: 'Выберите действие' }]}>
<Select
placeholder="Выберите действие"
options={actions?.map((a) => ({ value: a.name, label: a.display_name }))}
showSearch
optionFilterProp="label"
/>
</Form.Item>
{/* Тип цели */}
<Form.Item name="scope_type" label="Тип цели" rules={[{ required: true, message: 'Выберите тип цели' }]}>
<Select placeholder="Выберите тип цели">
<Select.Option value="category">По категории устройств</Select.Option>
<Select.Option value="object">Весь объект</Select.Option>
</Select>
</Form.Item>
{/* По категории */}
{scopeType === 'category' && (
<Space direction="vertical" style={{ width: '100%' }}>
<Form.Item
name="scope_category"
label="Категория"
rules={[{ required: true, message: 'Выберите категорию' }]}
style={{ marginBottom: 0 }}
>
<Select placeholder="Выберите категорию" options={CATEGORY_OPTIONS} />
</Form.Item>
<Form.Item
name="scope_object_ids"
label="Объекты"
extra="Оставьте пустым — задача запустится на всех объектах"
style={{ marginBottom: 0 }}
>
<Select
mode="multiple"
placeholder="Все объекты"
options={objectOptions}
allowClear
showSearch
optionFilterProp="label"
/>
</Form.Item>
</Space>
)}
{/* Весь объект */}
{scopeType === 'object' && (
<Form.Item
name="scope_object_id"
label="Объект"
rules={[{ required: true, message: 'Выберите объект' }]}
>
<Select
placeholder="Выберите объект"
options={objectOptions}
showSearch
optionFilterProp="label"
/>
</Form.Item>
)}
{/* Динамические параметры действия */}
{currentAction?.params_schema.map((p) => (
<Form.Item
key={p.name}
name={p.name}
label={p.label}
rules={p.required ? [{ required: true, message: `Введите ${p.label}` }] : []}
>
<Input placeholder={p.label} />
</Form.Item>
))}
</Form>
</Modal>
</div> </div>
) )
} }