mirror of https://github.com/openspug/spug
A 新增监控中心总览查看实时状态
parent
ba4f561aa6
commit
88ba758d49
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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)
|
|
@ -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>
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
.steps {
|
|
||||||
width: 520px;
|
|
||||||
margin: 0 auto 30px;
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue