плагин задача объекты
This commit is contained in:
parent
74fc294d04
commit
9dde2252f0
2 changed files with 161 additions and 8 deletions
|
|
@ -64,11 +64,15 @@ async def _resolve_scope(scope: dict) -> list[tuple[Device, Object]]:
|
|||
query = query.where(Device.rack_id.in_(rack_ids))
|
||||
elif scope_type == "category":
|
||||
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"])
|
||||
elif scope_type == "role":
|
||||
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"])
|
||||
else:
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -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 { useState } from 'react'
|
||||
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'
|
||||
|
||||
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() {
|
||||
const navigate = useNavigate()
|
||||
const [page, setPage] = useState(1)
|
||||
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 = [
|
||||
{
|
||||
|
|
@ -46,11 +99,19 @@ export function TasksPage() {
|
|||
},
|
||||
]
|
||||
|
||||
const objectOptions = (objects ?? []).map((o) => ({ value: o.id, label: o.name }))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ marginBottom: 16 }}>
|
||||
Задачи
|
||||
</Typography.Title>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
Задачи
|
||||
</Typography.Title>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openModal}>
|
||||
Новая задача
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
dataSource={tasks ?? []}
|
||||
columns={columns}
|
||||
|
|
@ -61,13 +122,101 @@ export function TasksPage() {
|
|||
current: page,
|
||||
pageSize: PAGE_SIZE,
|
||||
onChange: setPage,
|
||||
// Backend doesn't return total count — hide size changer, show simple pagination
|
||||
showSizeChanger: false,
|
||||
simple: true,
|
||||
}}
|
||||
onRow={(row) => ({ onClick: () => navigate(`/tasks/${row.id}`) })}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue