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

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 { 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 { useEffect, useMemo, useState } from 'react'
import { useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { useObjects } from '../api/objects'
import {
useDashboardChecks,
useDashboardMonitorStatus,
useDashboardPolicy,
useDashboardRunNow,
usePatchDashboardChecks,
usePatchDashboardObjectOverrides,
usePatchDashboardObjectPolicy,
usePatchDashboardTagPolicy,
type DashboardObjectOverride,
} from '../api/dashboard'
import { useTags } from '../api/tags'
const CATEGORY_OPTIONS = [
'main_server',
'vm',
'router',
'embedded',
'camera',
'io_board',
'bank_terminal',
'cash_register',
'other',
]
type OverrideEnabledMode = 'inherit' | 'enabled' | 'disabled'
type OverrideCategoriesMode = 'inherit' | 'custom'
function cityKey(city: string | null): string {
return city && city.trim() ? city : 'Без города'
}
export function HomeSettingsPage() {
const navigate = useNavigate()
@ -38,27 +23,13 @@ export function HomeSettingsPage() {
const { data: objects } = useObjects()
const { data: tags } = useTags()
const { data: policy } = useDashboardPolicy()
const { data: checksData } = useDashboardChecks()
const { data: monitorStatus } = useDashboardMonitorStatus()
const runNow = useDashboardRunNow()
const patchTagPolicy = usePatchDashboardTagPolicy()
const patchObjectPolicy = usePatchDashboardObjectPolicy()
const patchChecks = usePatchDashboardChecks()
const patchOverrides = usePatchDashboardObjectOverrides()
const [overrideObjectId, setOverrideObjectId] = useState<number | undefined>()
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 busy = runNow.isPending || patchTagPolicy.isPending || patchObjectPolicy.isPending
const objectPolicyMap = useMemo(() => {
const map = new Map<number, 'inherit' | 'include' | 'exclude'>()
@ -66,46 +37,88 @@ export function HomeSettingsPage() {
return map
}, [policy])
const overrideMap = useMemo(() => {
const map = new Map<string, DashboardObjectOverride>()
for (const row of checksData?.object_overrides ?? []) {
map.set(`${row.object_id}:${row.check_key}`, row)
}
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
}, [checksData])
}, [policy])
useEffect(() => {
if (!overrideObjectId || !overrideCheckKey) {
setOverrideEnabledMode('inherit')
setOverrideInterval(undefined)
setOverrideCategoriesMode('inherit')
setOverrideCategories([])
return
const objectsByCity = useMemo(() => {
const map = new Map<string, NonNullable<typeof objects>>()
for (const obj of objects ?? []) {
const key = cityKey(obj.city)
if (!map.has(key)) map.set(key, [])
map.get(key)!.push(obj)
}
const current = overrideMap.get(`${overrideObjectId}:${overrideCheckKey}`)
if (!current) {
setOverrideEnabledMode('inherit')
setOverrideInterval(undefined)
setOverrideCategoriesMode('inherit')
setOverrideCategories([])
return
return Array.from(map.entries())
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([city, rows]) => ({
city,
rows: rows.sort((x, y) => x.name.localeCompare(y.name)),
}))
}, [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')
else if (current.enabled_override === false) setOverrideEnabledMode('disabled')
else setOverrideEnabledMode('inherit')
setOverrideInterval(current.interval_sec_override ?? undefined)
if (current.categories_override && current.categories_override.length > 0) {
setOverrideCategoriesMode('custom')
setOverrideCategories(current.categories_override)
} else {
setOverrideCategoriesMode('inherit')
setOverrideCategories([])
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('Не удалось обновить теги')
}
}
}, [overrideObjectId, overrideCheckKey, overrideMap])
const handleRunNow = async () => {
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 (
<div>
<Row justify="space-between" align="middle" gutter={[8, 8]} style={{ marginBottom: 12 }}>
@ -130,9 +147,11 @@ export function HomeSettingsPage() {
</Space>
</Col>
<Col>
<Space wrap>
<Button type="primary" onClick={handleRunNow} loading={runNow.isPending}>
Запустить сейчас
</Button>
</Space>
</Col>
</Row>
@ -147,239 +166,110 @@ export function HomeSettingsPage() {
<Alert type="error" showIcon style={{ marginBottom: 12 }} message={monitorStatus.last_error} />
)}
<Card title="Расписание проверок" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: '100%' }}>
{(checksData?.checks ?? []).map((check) => (
<Row key={check.check_key} gutter={[12, 12]} align="middle">
<Col xs={24} md={6}><Typography.Text strong>{check.check_key}</Typography.Text></Col>
<Col xs={24} md={4}>
<Switch
checked={check.enabled}
onChange={async (enabled) => {
await patchChecks.mutateAsync([{ check_key: check.check_key, enabled }])
}}
/>
</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 }}>
<Space direction="vertical" style={{ width: '100%' }} size={12}>
<Checkbox
disabled={busy || allObjectIds.length === 0}
indeterminate={!allObjectsEnabled && someObjectsEnabled}
checked={allObjectsEnabled}
onChange={(e) => void setObjectsEnabled(allObjectIds, e.target.checked)}
>
Все объекты
</Checkbox>
<Card title="Политика по тегам" style={{ marginBottom: 16 }}>
<Row gutter={[12, 8]}>
{(tags ?? []).map((tag) => (
<Col xs={24} md={12} xl={8} key={tag.id}>
<Collapse
items={objectsByCity.map((group) => {
const ids = group.rows.map((o) => o.id)
const all = ids.length > 0 && ids.every((id) => isObjectEnabled(id))
const some = ids.some((id) => isObjectEnabled(id))
return {
key: group.city,
label: (
<Space>
<Tag color={tag.color}>{tag.name}</Tag>
<Switch
size="small"
checked={tagPolicyMap.get(tag.id) ?? true}
onChange={async (enabled) => {
await patchTagPolicy.mutateAsync([{ tag_id: tag.id, enabled }])
<Checkbox
disabled={busy}
checked={all}
indeterminate={!all && some}
onChange={(e) => {
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>
</Col>
))}
<Col>
<Typography.Text type="secondary">{obj.device_count} устр.</Typography.Text>
</Col>
</Row>
))}
</Space>
),
}
})}
/>
</Space>
</Card>
<Card title="Политика объектов и overrides" style={{ marginBottom: 16 }}>
<Table
size="small"
rowKey="id"
pagination={{ pageSize: 8 }}
dataSource={objects ?? []}
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 }])
}}
/>
),
},
]}
/>
<Card title="Категории (теги)">
<Collapse
items={tagsByType.map((group) => {
const ids = group.rows.map((t) => t.id)
const all = ids.length > 0 && ids.every((id) => isTagEnabled(id))
const some = ids.some((id) => isTagEnabled(id))
<Divider style={{ margin: '12px 0' }} />
<Space wrap>
<Select
placeholder="Объект"
style={{ width: 220 }}
value={overrideObjectId}
onChange={setOverrideObjectId}
options={(objects ?? []).map((o) => ({ value: o.id, label: o.name }))}
/>
<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 обновлен')
return {
key: group.tagType,
label: (
<Space>
<Checkbox
disabled={busy}
checked={all}
indeterminate={!all && some}
onChange={(e) => {
e.stopPropagation()
void setTagsEnabled(ids, e.target.checked)
}}
>
Сохранить 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>
onClick={(e) => e.stopPropagation()}
/>
<Typography.Text strong>{group.tagType}</Typography.Text>
<Typography.Text type="secondary">({ids.length})</Typography.Text>
</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(', '),
},
]}
),
children: (
<Space direction="vertical" style={{ width: '100%' }}>
{group.rows.map((tag) => (
<Space key={tag.id}>
<Checkbox
disabled={busy}
checked={isTagEnabled(tag.id)}
onChange={(e) => void setTagEnabled(tag.id, e.target.checked)}
/>
<Tag color={tag.color}>{tag.name}</Tag>
</Space>
))}
</Space>
),
}
})}
/>
</Card>
</div>