правка настреок

This commit is contained in:
dv 2026-05-05 12:33:27 +03:00
parent ecb3f6b524
commit 51ea278339

View file

@ -1,36 +1,21 @@
import { ArrowLeftOutlined } from '@ant-design/icons' import { ArrowLeftOutlined } from '@ant-design/icons'
import { Alert, Button, Card, Col, Divider, InputNumber, Row, Select, Space, Switch, Table, Tag, Typography, message } from 'antd' import { Alert, Button, Card, Checkbox, Collapse, Row, Col, Space, Tag, Typography, message } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useEffect, useMemo, useState } from 'react' import { useMemo } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useObjects } from '../api/objects' import { useObjects } from '../api/objects'
import { import {
useDashboardChecks,
useDashboardMonitorStatus, useDashboardMonitorStatus,
useDashboardPolicy, useDashboardPolicy,
useDashboardRunNow, useDashboardRunNow,
usePatchDashboardChecks,
usePatchDashboardObjectOverrides,
usePatchDashboardObjectPolicy, usePatchDashboardObjectPolicy,
usePatchDashboardTagPolicy, usePatchDashboardTagPolicy,
type DashboardObjectOverride,
} from '../api/dashboard' } from '../api/dashboard'
import { useTags } from '../api/tags' import { useTags } from '../api/tags'
const CATEGORY_OPTIONS = [ function cityKey(city: string | null): string {
'main_server', return city && city.trim() ? city : 'Без города'
'vm', }
'router',
'embedded',
'camera',
'io_board',
'bank_terminal',
'cash_register',
'other',
]
type OverrideEnabledMode = 'inherit' | 'enabled' | 'disabled'
type OverrideCategoriesMode = 'inherit' | 'custom'
export function HomeSettingsPage() { export function HomeSettingsPage() {
const navigate = useNavigate() const navigate = useNavigate()
@ -38,27 +23,13 @@ export function HomeSettingsPage() {
const { data: objects } = useObjects() const { data: objects } = useObjects()
const { data: tags } = useTags() const { data: tags } = useTags()
const { data: policy } = useDashboardPolicy() const { data: policy } = useDashboardPolicy()
const { data: checksData } = useDashboardChecks()
const { data: monitorStatus } = useDashboardMonitorStatus() const { data: monitorStatus } = useDashboardMonitorStatus()
const runNow = useDashboardRunNow() const runNow = useDashboardRunNow()
const patchTagPolicy = usePatchDashboardTagPolicy() const patchTagPolicy = usePatchDashboardTagPolicy()
const patchObjectPolicy = usePatchDashboardObjectPolicy() const patchObjectPolicy = usePatchDashboardObjectPolicy()
const patchChecks = usePatchDashboardChecks()
const patchOverrides = usePatchDashboardObjectOverrides()
const [overrideObjectId, setOverrideObjectId] = useState<number | undefined>() const busy = runNow.isPending || patchTagPolicy.isPending || patchObjectPolicy.isPending
const [overrideCheckKey, setOverrideCheckKey] = useState<string | undefined>()
const [overrideInterval, setOverrideInterval] = useState<number | undefined>()
const [overrideEnabledMode, setOverrideEnabledMode] = useState<OverrideEnabledMode>('inherit')
const [overrideCategoriesMode, setOverrideCategoriesMode] = useState<OverrideCategoriesMode>('inherit')
const [overrideCategories, setOverrideCategories] = useState<string[]>([])
const tagPolicyMap = useMemo(() => {
const map = new Map<number, boolean>()
for (const row of policy?.tag_policies ?? []) map.set(row.tag_id, row.enabled)
return map
}, [policy])
const objectPolicyMap = useMemo(() => { const objectPolicyMap = useMemo(() => {
const map = new Map<number, 'inherit' | 'include' | 'exclude'>() const map = new Map<number, 'inherit' | 'include' | 'exclude'>()
@ -66,46 +37,88 @@ export function HomeSettingsPage() {
return map return map
}, [policy]) }, [policy])
const overrideMap = useMemo(() => { const tagPolicyMap = useMemo(() => {
const map = new Map<string, DashboardObjectOverride>() const map = new Map<number, boolean>()
for (const row of checksData?.object_overrides ?? []) { for (const row of policy?.tag_policies ?? []) map.set(row.tag_id, row.enabled)
map.set(`${row.object_id}:${row.check_key}`, row)
}
return map return map
}, [checksData]) }, [policy])
useEffect(() => { const objectsByCity = useMemo(() => {
if (!overrideObjectId || !overrideCheckKey) { const map = new Map<string, NonNullable<typeof objects>>()
setOverrideEnabledMode('inherit') for (const obj of objects ?? []) {
setOverrideInterval(undefined) const key = cityKey(obj.city)
setOverrideCategoriesMode('inherit') if (!map.has(key)) map.set(key, [])
setOverrideCategories([]) map.get(key)!.push(obj)
return
} }
const current = overrideMap.get(`${overrideObjectId}:${overrideCheckKey}`) return Array.from(map.entries())
if (!current) { .sort((a, b) => a[0].localeCompare(b[0]))
setOverrideEnabledMode('inherit') .map(([city, rows]) => ({
setOverrideInterval(undefined) city,
setOverrideCategoriesMode('inherit') rows: rows.sort((x, y) => x.name.localeCompare(y.name)),
setOverrideCategories([]) }))
return }, [objects])
const tagsByType = useMemo(() => {
const map = new Map<string, NonNullable<typeof tags>>()
for (const tag of tags ?? []) {
const key = tag.tag_type && tag.tag_type.trim() ? tag.tag_type : 'other'
if (!map.has(key)) map.set(key, [])
map.get(key)!.push(tag)
} }
return Array.from(map.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([tagType, rows]) => ({
tagType,
rows: rows.sort((x, y) => x.name.localeCompare(y.name)),
}))
}, [tags])
if (current.enabled_override === true) setOverrideEnabledMode('enabled') const isObjectEnabled = (objectId: number): boolean => objectPolicyMap.get(objectId) !== 'exclude'
else if (current.enabled_override === false) setOverrideEnabledMode('disabled') const isTagEnabled = (tagId: number): boolean => tagPolicyMap.get(tagId) ?? true
else setOverrideEnabledMode('inherit')
setOverrideInterval(current.interval_sec_override ?? undefined) const setObjectEnabled = async (objectId: number, enabled: boolean) => {
try {
if (current.categories_override && current.categories_override.length > 0) { await patchObjectPolicy.mutateAsync([
setOverrideCategoriesMode('custom') { object_id: objectId, mode: enabled ? 'include' : 'exclude' },
setOverrideCategories(current.categories_override) ])
} else { } catch {
setOverrideCategoriesMode('inherit') message.error('Не удалось обновить объект')
setOverrideCategories([])
} }
}, [overrideObjectId, overrideCheckKey, overrideMap]) }
const setObjectsEnabled = async (objectIds: number[], enabled: boolean) => {
if (objectIds.length === 0) return
try {
await patchObjectPolicy.mutateAsync(
objectIds.map((objectId) => ({
object_id: objectId,
mode: enabled ? 'include' : 'exclude',
}))
)
message.success(`Обновлено объектов: ${objectIds.length}`)
} catch {
message.error('Не удалось обновить объекты')
}
}
const setTagEnabled = async (tagId: number, enabled: boolean) => {
try {
await patchTagPolicy.mutateAsync([{ tag_id: tagId, enabled }])
} catch {
message.error('Не удалось обновить тег')
}
}
const setTagsEnabled = async (tagIds: number[], enabled: boolean) => {
if (tagIds.length === 0) return
try {
await patchTagPolicy.mutateAsync(tagIds.map((tagId) => ({ tag_id: tagId, enabled })))
message.success(`Обновлено тегов: ${tagIds.length}`)
} catch {
message.error('Не удалось обновить теги')
}
}
const handleRunNow = async () => { const handleRunNow = async () => {
try { try {
@ -116,6 +129,10 @@ export function HomeSettingsPage() {
} }
} }
const allObjectIds = (objects ?? []).map((o) => o.id)
const allObjectsEnabled = allObjectIds.length > 0 && allObjectIds.every((id) => isObjectEnabled(id))
const someObjectsEnabled = allObjectIds.some((id) => isObjectEnabled(id))
return ( return (
<div> <div>
<Row justify="space-between" align="middle" gutter={[8, 8]} style={{ marginBottom: 12 }}> <Row justify="space-between" align="middle" gutter={[8, 8]} style={{ marginBottom: 12 }}>
@ -130,9 +147,11 @@ export function HomeSettingsPage() {
</Space> </Space>
</Col> </Col>
<Col> <Col>
<Button type="primary" onClick={handleRunNow} loading={runNow.isPending}> <Space wrap>
Запустить сейчас <Button type="primary" onClick={handleRunNow} loading={runNow.isPending}>
</Button> Запустить сейчас
</Button>
</Space>
</Col> </Col>
</Row> </Row>
@ -147,239 +166,110 @@ export function HomeSettingsPage() {
<Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} /> <Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} />
)} )}
<Card title="Расписание проверок" style={{ marginBottom: 16 }}> <Card title="Объекты" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }} size={12}>
{(checksData?.checks ?? []).map((check) => ( <Checkbox
<Row key={check.check_key} gutter={[12, 12]} align="middle"> disabled={busy || allObjectIds.length === 0}
<Col xs={24} md={6}><Typography.Text strong>{check.check_key}</Typography.Text></Col> indeterminate={!allObjectsEnabled && someObjectsEnabled}
<Col xs={24} md={4}> checked={allObjectsEnabled}
<Switch onChange={(e) => void setObjectsEnabled(allObjectIds, e.target.checked)}
checked={check.enabled} >
onChange={async (enabled) => { Все объекты
await patchChecks.mutateAsync([{ check_key: check.check_key, enabled }]) </Checkbox>
}}
/> <Collapse
</Col> items={objectsByCity.map((group) => {
<Col xs={24} md={6}> const ids = group.rows.map((o) => o.id)
<InputNumber const all = ids.length > 0 && ids.every((id) => isObjectEnabled(id))
min={30} const some = ids.some((id) => isObjectEnabled(id))
value={check.interval_sec_default}
addonAfter="sec" return {
onBlur={async (e) => { key: group.city,
const val = Number((e.target as HTMLInputElement).value) label: (
if (!Number.isFinite(val) || val <= 0) return <Space>
await patchChecks.mutateAsync([{ check_key: check.check_key, interval_sec_default: val }]) <Checkbox
}} disabled={busy}
/> checked={all}
</Col> indeterminate={!all && some}
<Col xs={24} md={8}> onChange={(e) => {
<Select e.stopPropagation()
mode="multiple" void setObjectsEnabled(ids, e.target.checked)
allowClear }}
style={{ width: '100%' }} onClick={(e) => e.stopPropagation()}
value={check.categories} />
options={CATEGORY_OPTIONS.map((c) => ({ value: c, label: c }))} <Typography.Text strong>{group.city}</Typography.Text>
onChange={async (categories) => { <Typography.Text type="secondary">({ids.length})</Typography.Text>
await patchChecks.mutateAsync([{ check_key: check.check_key, categories }]) </Space>
}} ),
/> children: (
</Col> <Space direction="vertical" style={{ width: '100%' }}>
</Row> {group.rows.map((obj) => (
))} <Row key={obj.id} justify="space-between" align="middle">
<Col>
<Space>
<Checkbox
disabled={busy}
checked={isObjectEnabled(obj.id)}
onChange={(e) => void setObjectEnabled(obj.id, e.target.checked)}
/>
<Typography.Text>{obj.name}</Typography.Text>
{obj.client ? <Tag color="blue">{obj.client}</Tag> : null}
</Space>
</Col>
<Col>
<Typography.Text type="secondary">{obj.device_count} устр.</Typography.Text>
</Col>
</Row>
))}
</Space>
),
}
})}
/>
</Space> </Space>
</Card> </Card>
<Card title="Политика по тегам" style={{ marginBottom: 16 }}> <Card title="Категории (теги)">
<Row gutter={[12, 8]}> <Collapse
{(tags ?? []).map((tag) => ( items={tagsByType.map((group) => {
<Col xs={24} md={12} xl={8} key={tag.id}> const ids = group.rows.map((t) => t.id)
<Space> const all = ids.length > 0 && ids.every((id) => isTagEnabled(id))
<Tag color={tag.color}>{tag.name}</Tag> const some = ids.some((id) => isTagEnabled(id))
<Switch
size="small"
checked={tagPolicyMap.get(tag.id) ?? true}
onChange={async (enabled) => {
await patchTagPolicy.mutateAsync([{ tag_id: tag.id, enabled }])
}}
/>
</Space>
</Col>
))}
</Row>
</Card>
<Card title="Политика объектов и overrides" style={{ marginBottom: 16 }}> return {
<Table key: group.tagType,
size="small" label: (
rowKey="id" <Space>
pagination={{ pageSize: 8 }} <Checkbox
dataSource={objects ?? []} disabled={busy}
scroll={{ x: 650 }} checked={all}
columns={[ indeterminate={!all && some}
{ title: 'Объект', dataIndex: 'name', key: 'name' }, onChange={(e) => {
{ e.stopPropagation()
title: 'Режим', void setTagsEnabled(ids, e.target.checked)
key: 'mode', }}
render: (_: unknown, row: any) => ( onClick={(e) => e.stopPropagation()}
<Select />
value={objectPolicyMap.get(row.id) ?? 'inherit'} <Typography.Text strong>{group.tagType}</Typography.Text>
style={{ width: 150 }} <Typography.Text type="secondary">({ids.length})</Typography.Text>
options={[ </Space>
{ value: 'inherit', label: 'inherit' },
{ value: 'include', label: 'include' },
{ value: 'exclude', label: 'exclude' },
]}
onChange={async (mode) => {
await patchObjectPolicy.mutateAsync([{ object_id: row.id, mode }])
}}
/>
), ),
}, children: (
]} <Space direction="vertical" style={{ width: '100%' }}>
/> {group.rows.map((tag) => (
<Space key={tag.id}>
<Divider style={{ margin: '12px 0' }} /> <Checkbox
disabled={busy}
<Space wrap> checked={isTagEnabled(tag.id)}
<Select onChange={(e) => void setTagEnabled(tag.id, e.target.checked)}
placeholder="Объект" />
style={{ width: 220 }} <Tag color={tag.color}>{tag.name}</Tag>
value={overrideObjectId} </Space>
onChange={setOverrideObjectId} ))}
options={(objects ?? []).map((o) => ({ value: o.id, label: o.name }))} </Space>
/> ),
<Select }
placeholder="Проверка" })}
style={{ width: 160 }}
value={overrideCheckKey}
onChange={setOverrideCheckKey}
options={(checksData?.checks ?? []).map((c) => ({ value: c.check_key, label: c.check_key }))}
/>
<Select
placeholder="Enabled override"
style={{ width: 170 }}
value={overrideEnabledMode}
onChange={(v) => setOverrideEnabledMode(v as OverrideEnabledMode)}
options={[
{ value: 'inherit', label: 'inherit' },
{ value: 'enabled', label: 'enabled' },
{ value: 'disabled', label: 'disabled' },
]}
/>
<InputNumber
placeholder="Интервал (сек)"
min={30}
value={overrideInterval}
onChange={(v) => setOverrideInterval(typeof v === 'number' ? v : undefined)}
/>
<Select
placeholder="Режим категорий"
style={{ width: 170 }}
value={overrideCategoriesMode}
onChange={(v) => setOverrideCategoriesMode(v as OverrideCategoriesMode)}
options={[
{ value: 'inherit', label: 'inherit' },
{ value: 'custom', label: 'custom' },
]}
/>
<Select
mode="multiple"
allowClear
disabled={overrideCategoriesMode !== 'custom'}
placeholder="Категории override"
style={{ width: 280 }}
value={overrideCategories}
onChange={setOverrideCategories}
options={CATEGORY_OPTIONS.map((c) => ({ value: c, label: c }))}
/>
<Button
type="primary"
onClick={async () => {
if (!overrideObjectId || !overrideCheckKey) {
message.warning('Выберите объект и проверку')
return
}
await patchOverrides.mutateAsync([
{
object_id: overrideObjectId,
check_key: overrideCheckKey,
enabled_override:
overrideEnabledMode === 'inherit'
? null
: overrideEnabledMode === 'enabled',
interval_sec_override: overrideInterval ?? null,
categories_override:
overrideCategoriesMode === 'custom'
? overrideCategories
: null,
},
])
message.success('Override обновлен')
}}
>
Сохранить override
</Button>
<Button
onClick={async () => {
if (!overrideObjectId || !overrideCheckKey) {
message.warning('Выберите объект и проверку')
return
}
await patchOverrides.mutateAsync([
{
object_id: overrideObjectId,
check_key: overrideCheckKey,
enabled_override: null,
interval_sec_override: null,
categories_override: null,
},
])
message.success('Override очищен')
}}
>
Очистить override
</Button>
</Space>
<Divider style={{ margin: '12px 0' }} />
<Table
size="small"
rowKey={(row) => `${row.object_id}:${row.check_key}`}
pagination={{ pageSize: 6 }}
dataSource={checksData?.object_overrides ?? []}
scroll={{ x: 900 }}
columns={[
{
title: 'Объект',
key: 'object',
render: (_: unknown, row: DashboardObjectOverride) =>
objects?.find((o) => o.id === row.object_id)?.name ?? row.object_id,
},
{ title: 'Проверка', dataIndex: 'check_key', key: 'check_key' },
{
title: 'Enabled',
key: 'enabled_override',
render: (_: unknown, row: DashboardObjectOverride) =>
row.enabled_override == null ? 'inherit' : row.enabled_override ? 'enabled' : 'disabled',
},
{
title: 'Интервал',
key: 'interval_sec_override',
render: (_: unknown, row: DashboardObjectOverride) =>
row.interval_sec_override == null ? 'inherit' : `${row.interval_sec_override}s`,
},
{
title: 'Категории',
key: 'categories_override',
render: (_: unknown, row: DashboardObjectOverride) =>
row.categories_override == null
? 'inherit'
: row.categories_override.length === 0
? 'all'
: row.categories_override.join(', '),
},
]}
/> />
</Card> </Card>
</div> </div>