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) <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
|
||||
|
|
|
@ -7,5 +7,6 @@ from .views import *
|
|||
|
||||
urlpatterns = [
|
||||
path('', DetectionView.as_view()),
|
||||
path('overview/', get_overview),
|
||||
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.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)
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 { 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>
|
||||
|
|
|
@ -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