A 新增监控中心总览查看实时状态

pull/462/head
vapao 2022-03-11 09:51:14 +08:00
parent ba4f561aa6
commit 88ba758d49
8 changed files with 256 additions and 25 deletions

View File

@ -1,7 +1,6 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com> # Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License. # Released under the AGPL-3.0 License.
from django.db.models import F
from apps.app.models import App from apps.app.models import App
from apps.host.models import Host from apps.host.models import Host
from apps.schedule.models import Task from apps.schedule.models import Task

View File

@ -7,5 +7,6 @@ from .views import *
urlpatterns = [ urlpatterns = [
path('', DetectionView.as_view()), path('', DetectionView.as_view()),
path('overview/', get_overview),
path('test/', run_test), path('test/', run_test),
] ]

View File

@ -8,6 +8,7 @@ from libs import json_response, JsonParser, Argument, human_datetime, auth
from apps.monitor.models import Detection from apps.monitor.models import Detection
from apps.monitor.executors import dispatch from apps.monitor.executors import dispatch
from apps.setting.utils import AppSetting from apps.setting.utils import AppSetting
from datetime import datetime
import json import json
@ -104,3 +105,36 @@ def run_test(request):
is_success, message = dispatch(form.type, form.targets[0], form.extra) is_success, message = dispatch(form.type, form.targets[0], form.extra)
return json_response({'is_success': is_success, 'message': message}) return json_response({'is_success': is_success, 'message': message})
return json_response(error=error) return json_response(error=error)
@auth('monitor.monitor.view')
def get_overview(request):
response = []
rds = get_redis_connection()
for item in Detection.objects.all():
data = {}
for key in json.loads(item.targets):
data[key] = {
'id': f'{item.id}_{key}',
'group': item.group,
'name': item.name,
'type': item.get_type_display(),
'target': key,
'desc': item.desc,
'status': '1' if item.is_active else '0',
'latest_run_time': item.latest_run_time,
}
if item.is_active:
for key, val in rds.hgetall(f'spug:det:{item.id}').items():
prefix, key = key.decode().split('_', 1)
if key in data:
val = int(val)
if prefix == 'c':
if data[key]['status'] == '1':
data[key]['status'] = '2'
data[key]['count'] = val
elif prefix == 't':
date = datetime.fromtimestamp(val).strftime('%Y-%m-%d %H:%M:%S')
data[key].update(status='3', notified_at=date)
response.extend(list(data.values()))
return json_response(response)

View File

@ -9,7 +9,7 @@ import { Modal, Steps } from 'antd';
import Step1 from './Step1'; import Step1 from './Step1';
import Step2 from './Step2'; import Step2 from './Step2';
import store from './store'; import store from './store';
import styles from './index.module.css'; import styles from './index.module.less';
import groupStore from '../alarm/group/store'; import groupStore from '../alarm/group/store';
export default observer(function () { export default observer(function () {

View File

@ -0,0 +1,158 @@
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License.
*/
import React, { useState, useEffect } from 'react';
import { observer } from 'mobx-react';
import { Card, Input, Select, Space, Tooltip, Spin, message } from 'antd';
import { FrownOutlined, RedoOutlined, SyncOutlined } from '@ant-design/icons';
import styles from './index.module.less';
import { http, includes } from 'libs';
import moment from 'moment';
import store from './store';
const ColorMap = {
'0': '#cccccc',
'1': '#009400',
'2': '#ffba00',
'3': '#fa383e',
}
const StatusMap = {
'1': '正常',
'2': '警告',
'3': '紧急',
'0': '禁用',
}
let AutoReload = null
function CardItem(props) {
const {status, type, desc, name, target, latest_run_time} = props.data
const title = (
<div>
<div>类型: {type}</div>
<div>名称: {name}</div>
<div>描述: {desc}</div>
<div>目标: {target}</div>
<div>状态: {StatusMap[status]}</div>
{latest_run_time ? <div>更新: {latest_run_time}</div> : null}
</div>
)
return (
<Tooltip title={title}>
<div className={styles.card} style={{backgroundColor: ColorMap[status]}}>
{moment(latest_run_time).fromNow()}
</div>
</Tooltip>
)
}
function MonitorCard() {
const [fetching, setFetching] = useState(true);
const [autoReload, setAutoReload] = useState(false);
const [status, setStatus] = useState();
const [records, setRecords] = useState([]);
const [dataSource, setDataSource] = useState([]);
useEffect(() => {
fetchRecords()
return () => AutoReload = null
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
function fetchRecords() {
if (AutoReload === false) return
setFetching(true);
return http.get('/api/monitor/overview/')
.then(res => setRecords(res))
.finally(() => {
setFetching(false)
if (AutoReload) setTimeout(fetchRecords, 5000)
})
}
useEffect(() => {
const data = records.filter(x =>
(!store.f_type || x.type === store.f_type) &&
(!store.f_group || x.group === store.f_group) &&
(!store.f_name || includes(x.name, store.f_name))
)
setDataSource(data)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [records, store.f_type, store.f_group, store.f_name])
function handleAutoReload() {
AutoReload = !autoReload
message.info(autoReload ? '关闭自动刷新' : '开启自动刷新')
if (!autoReload) fetchRecords()
setAutoReload(!autoReload)
}
const filteredRecords = dataSource.filter(x => !status || x.status === status)
return (
<Card title="总览" style={{marginBottom: 24}} extra={(
<Space size="middle">
<Space>
<div>分组</div>
<Select allowClear style={{minWidth: 150}} value={store.f_group} onChange={v => store.f_group = v}
placeholder="请选择">
{store.groups.map(item => (
<Select.Option value={item} key={item}>{item}</Select.Option>
))}
</Select>
</Space>
<Space>
<div>类型</div>
<Select allowClear style={{width: 120}} value={store.f_type} onChange={v => store.f_type = v}
placeholder="请选择">
{store.types.map(item => <Select.Option key={item} value={item}>{item}</Select.Option>)}
</Select>
</Space>
<Space>
<div>名称</div>
<Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
</Space>
</Space>
)}>
<Spin spinning={fetching}>
<div className={styles.header}>
{Object.entries(StatusMap).map(([s, desc]) => {
const count = dataSource.filter(x => x.status === s).length;
return count ? (
<div
key={s}
className={styles.item}
style={s === status ? {backgroundColor: ColorMap[s]} : {
border: `1.5px solid ${ColorMap[s]}`,
color: ColorMap[s]
}}
onClick={() => setStatus(s === status ? '' : s)}>
{dataSource.filter(x => x.status === s).length}
</div>
) : null
})}
<div
className={styles.authLoad}
style={autoReload ? {backgroundColor: '#1890ff'} : {color: '#1890ff', border: '1.5px solid #1890ff'}}
onClick={handleAutoReload}>
{autoReload ? <SyncOutlined/> : <RedoOutlined/>}
</div>
</div>
{filteredRecords.length > 0 ? (
<Space wrap size={4}>
{filteredRecords.map(item => (
<CardItem key={item.id} data={item}/>
))}
</Space>
) : (
<div className={styles.notMatch}><FrownOutlined/></div>
)}
</Spin>
</Card>
)
}
export default observer(MonitorCard)

View File

@ -5,10 +5,10 @@
*/ */
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Input, Select } from 'antd'; import { AuthDiv, Breadcrumb } from 'components';
import { SearchForm, AuthDiv, Breadcrumb } from 'components';
import ComTable from './Table'; import ComTable from './Table';
import ComForm from './Form'; import ComForm from './Form';
import MonitorCard from './MonitorCard';
import store from './store'; import store from './store';
export default observer(function () { export default observer(function () {
@ -18,23 +18,7 @@ export default observer(function () {
<Breadcrumb.Item>首页</Breadcrumb.Item> <Breadcrumb.Item>首页</Breadcrumb.Item>
<Breadcrumb.Item>监控中心</Breadcrumb.Item> <Breadcrumb.Item>监控中心</Breadcrumb.Item>
</Breadcrumb> </Breadcrumb>
<SearchForm> <MonitorCard/>
<SearchForm.Item span={7} title="监控分组">
<Select allowClear value={store.f_group} onChange={v => store.f_group = v} placeholder="请选择">
{store.groups.map(item => (
<Select.Option value={item} key={item}>{item}</Select.Option>
))}
</Select>
</SearchForm.Item>
<SearchForm.Item span={7} title="监控名称">
<Input allowClear value={store.f_name} onChange={e => store.f_name = e.target.value} placeholder="请输入"/>
</SearchForm.Item>
<SearchForm.Item span={7} title="检测类型">
<Select allowClear value={store.f_type} onChange={v => store.f_type = v} placeholder="请选择">
{store.types.map(item => <Select.Option key={item} value={item}>{item}</Select.Option>)}
</Select>
</SearchForm.Item>
</SearchForm>
<ComTable/> <ComTable/>
{store.formVisible && <ComForm/>} {store.formVisible && <ComForm/>}
</AuthDiv> </AuthDiv>

View File

@ -1,4 +0,0 @@
.steps {
width: 520px;
margin: 0 auto 30px;
}

View File

@ -0,0 +1,59 @@
.steps {
width: 520px;
margin: 0 auto 30px;
}
.card {
display: flex;
justify-content: center;
align-items: center;
width: 60px;
height: 50px;
font-size: 12px;
color: #fff;
border-radius: 2px;
}
.header {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
margin-top: -6px;
.item {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 26px;
margin-left: 12px;
border-radius: 2px;
color: #fff;
font-weight: bold;
cursor: pointer;
}
.authLoad {
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 26px;
color: #fff;
margin-left: 24px;
border-radius: 2px;
}
}
.notMatch {
display: flex;
justify-content: center;
align-items: center;
color: #999;
:global(.anticon) {
font-size: 18px;
margin-right: 8px;
}
}