плагин задача объекты
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))
|
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 []
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue