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) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License.
from django.db.models import F
from apps.app.models import App
from apps.host.models import Host
from apps.schedule.models import Task

View File

@ -7,5 +7,6 @@ from .views import *
urlpatterns = [
path('', DetectionView.as_view()),
path('overview/', get_overview),
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.executors import dispatch
from apps.setting.utils import AppSetting
from datetime import datetime
import json
@ -104,3 +105,36 @@ def run_test(request):
is_success, message = dispatch(form.type, form.targets[0], form.extra)
return json_response({'is_success': is_success, 'message': message})
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 Step2 from './Step2';
import store from './store';
import styles from './index.module.css';
import styles from './index.module.less';
import groupStore from '../alarm/group/store';
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 { observer } from 'mobx-react';
import { Input, Select } from 'antd';
import { SearchForm, AuthDiv, Breadcrumb } from 'components';
import { AuthDiv, Breadcrumb } from 'components';
import ComTable from './Table';
import ComForm from './Form';
import MonitorCard from './MonitorCard';
import store from './store';
export default observer(function () {
@ -18,23 +18,7 @@ export default observer(function () {
<Breadcrumb.Item>首页</Breadcrumb.Item>
<Breadcrumb.Item>监控中心</Breadcrumb.Item>
</Breadcrumb>
<SearchForm>
<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>
<MonitorCard/>
<ComTable/>
{store.formVisible && <ComForm/>}
</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;
}
}