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

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])
const isObjectEnabled = (objectId: number): boolean => objectPolicyMap.get(objectId) !== 'exclude'
const isTagEnabled = (tagId: number): boolean => tagPolicyMap.get(tagId) ?? true
const setObjectEnabled = async (objectId: number, enabled: boolean) => {
try {
await patchObjectPolicy.mutateAsync([
{ object_id: objectId, mode: enabled ? 'include' : 'exclude' },
])
} catch {
message.error('Не удалось обновить объект')
}
} }
if (current.enabled_override === true) setOverrideEnabledMode('enabled') const setObjectsEnabled = async (objectIds: number[], enabled: boolean) => {
else if (current.enabled_override === false) setOverrideEnabledMode('disabled') if (objectIds.length === 0) return
else setOverrideEnabledMode('inherit') try {
await patchObjectPolicy.mutateAsync(
setOverrideInterval(current.interval_sec_override ?? undefined) objectIds.map((objectId) => ({
object_id: objectId,
if (current.categories_override && current.categories_override.length > 0) { mode: enabled ? 'include' : 'exclude',
setOverrideCategoriesMode('custom') }))
setOverrideCategories(current.categories_override) )
} else { message.success(`Обновлено объектов: ${objectIds.length}`)
setOverrideCategoriesMode('inherit') } catch {
setOverrideCategories([]) 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('Не удалось обновить теги')
}
} }
}, [overrideObjectId, overrideCheckKey, overrideMap])
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>
<Space wrap>
<Button type="primary" onClick={handleRunNow} loading={runNow.isPending}> <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>
}}
/>
</Col>
<Col xs={24} md={6}>
<InputNumber
min={30}
value={check.interval_sec_default}
addonAfter="sec"
onBlur={async (e) => {
const val = Number((e.target as HTMLInputElement).value)
if (!Number.isFinite(val) || val <= 0) return
await patchChecks.mutateAsync([{ check_key: check.check_key, interval_sec_default: val }])
}}
/>
</Col>
<Col xs={24} md={8}>
<Select
mode="multiple"
allowClear
style={{ width: '100%' }}
value={check.categories}
options={CATEGORY_OPTIONS.map((c) => ({ value: c, label: c }))}
onChange={async (categories) => {
await patchChecks.mutateAsync([{ check_key: check.check_key, categories }])
}}
/>
</Col>
</Row>
))}
</Space>
</Card>
<Card title="Политика по тегам" style={{ marginBottom: 16 }}> <Collapse
<Row gutter={[12, 8]}> items={objectsByCity.map((group) => {
{(tags ?? []).map((tag) => ( const ids = group.rows.map((o) => o.id)
<Col xs={24} md={12} xl={8} key={tag.id}> const all = ids.length > 0 && ids.every((id) => isObjectEnabled(id))
const some = ids.some((id) => isObjectEnabled(id))
return {
key: group.city,
label: (
<Space> <Space>
<Tag color={tag.color}>{tag.name}</Tag> <Checkbox
<Switch disabled={busy}
size="small" checked={all}
checked={tagPolicyMap.get(tag.id) ?? true} indeterminate={!all && some}
onChange={async (enabled) => { onChange={(e) => {
await patchTagPolicy.mutateAsync([{ tag_id: tag.id, enabled }]) e.stopPropagation()
void setObjectsEnabled(ids, e.target.checked)
}} }}
onClick={(e) => e.stopPropagation()}
/> />
<Typography.Text strong>{group.city}</Typography.Text>
<Typography.Text type="secondary">({ids.length})</Typography.Text>
</Space>
),
children: (
<Space direction="vertical" style={{ width: '100%' }}>
{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> </Space>
</Col> </Col>
))} <Col>
<Typography.Text type="secondary">{obj.device_count} устр.</Typography.Text>
</Col>
</Row> </Row>
))}
</Space>
),
}
})}
/>
</Space>
</Card> </Card>
<Card title="Политика объектов и overrides" style={{ marginBottom: 16 }}> <Card title="Категории (теги)">
<Table <Collapse
size="small" items={tagsByType.map((group) => {
rowKey="id" const ids = group.rows.map((t) => t.id)
pagination={{ pageSize: 8 }} const all = ids.length > 0 && ids.every((id) => isTagEnabled(id))
dataSource={objects ?? []} const some = ids.some((id) => isTagEnabled(id))
scroll={{ x: 650 }}
columns={[
{ title: 'Объект', dataIndex: 'name', key: 'name' },
{
title: 'Режим',
key: 'mode',
render: (_: unknown, row: any) => (
<Select
value={objectPolicyMap.get(row.id) ?? 'inherit'}
style={{ width: 150 }}
options={[
{ value: 'inherit', label: 'inherit' },
{ value: 'include', label: 'include' },
{ value: 'exclude', label: 'exclude' },
]}
onChange={async (mode) => {
await patchObjectPolicy.mutateAsync([{ object_id: row.id, mode }])
}}
/>
),
},
]}
/>
<Divider style={{ margin: '12px 0' }} /> return {
key: group.tagType,
<Space wrap> label: (
<Select <Space>
placeholder="Объект" <Checkbox
style={{ width: 220 }} disabled={busy}
value={overrideObjectId} checked={all}
onChange={setOverrideObjectId} indeterminate={!all && some}
options={(objects ?? []).map((o) => ({ value: o.id, label: o.name }))} onChange={(e) => {
/> e.stopPropagation()
<Select void setTagsEnabled(ids, e.target.checked)
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 обновлен')
}} }}
> onClick={(e) => e.stopPropagation()}
Сохранить override />
</Button> <Typography.Text strong>{group.tagType}</Typography.Text>
<Button <Typography.Text type="secondary">({ids.length})</Typography.Text>
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> </Space>
),
<Divider style={{ margin: '12px 0' }} /> children: (
<Space direction="vertical" style={{ width: '100%' }}>
<Table {group.rows.map((tag) => (
size="small" <Space key={tag.id}>
rowKey={(row) => `${row.object_id}:${row.check_key}`} <Checkbox
pagination={{ pageSize: 6 }} disabled={busy}
dataSource={checksData?.object_overrides ?? []} checked={isTagEnabled(tag.id)}
scroll={{ x: 900 }} onChange={(e) => void setTagEnabled(tag.id, e.target.checked)}
columns={[ />
{ <Tag color={tag.color}>{tag.name}</Tag>
title: 'Объект', </Space>
key: 'object', ))}
render: (_: unknown, row: DashboardObjectOverride) => </Space>
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>