правка настреок
This commit is contained in:
parent
ecb3f6b524
commit
51ea278339
1 changed files with 188 additions and 298 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue