mirror of https://github.com/openspug/spug
A 添加工作台最近30天登录记录
parent
42f3ce1bf2
commit
b283ff4c16
|
@ -0,0 +1,19 @@
|
||||||
|
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
# Copyright: (c) <spug.dev@gmail.com>
|
||||||
|
# Released under the AGPL-3.0 License.
|
||||||
|
from django.views.generic import View
|
||||||
|
from django.db.models import F
|
||||||
|
from libs import json_response
|
||||||
|
from apps.account.models import History
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryView(View):
|
||||||
|
def get(self, request):
|
||||||
|
histories = []
|
||||||
|
for item in History.objects.annotate(nickname=F('user__nickname')):
|
||||||
|
histories.append({
|
||||||
|
'nickname': item.nickname,
|
||||||
|
'ip': item.ip,
|
||||||
|
'created_at': item.created_at.split('-', 1)[1],
|
||||||
|
})
|
||||||
|
return json_response(histories)
|
|
@ -110,3 +110,13 @@ class Role(models.Model, ModelMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'roles'
|
db_table = 'roles'
|
||||||
ordering = ('-id',)
|
ordering = ('-id',)
|
||||||
|
|
||||||
|
|
||||||
|
class History(models.Model, ModelMixin):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
ip = models.CharField(max_length=50)
|
||||||
|
created_at = models.CharField(max_length=20, default=human_datetime)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'login_histories'
|
||||||
|
ordering = ('-id',)
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from apps.account.views import *
|
from apps.account.views import *
|
||||||
|
from apps.account.history import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^login/', login),
|
url(r'^login/$', login),
|
||||||
url(r'^logout/', logout),
|
url(r'^logout/$', logout),
|
||||||
url(r'^user/$', UserView.as_view()),
|
url(r'^user/$', UserView.as_view()),
|
||||||
url(r'^role/$', RoleView.as_view()),
|
url(r'^role/$', RoleView.as_view()),
|
||||||
url(r'^self/$', SelfView.as_view()),
|
url(r'^self/$', SelfView.as_view()),
|
||||||
|
url(r'^login/history/$', HistoryView.as_view())
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||||
|
# Released under the AGPL-3.0 License.
|
||||||
|
from apps.account.models import History
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
def auto_clean_login_history():
|
||||||
|
date = datetime.now() - timedelta(days=30)
|
||||||
|
History.objects.filter(created_at__lt=date.strftime('%Y-%m-%d')).delete()
|
|
@ -6,7 +6,7 @@ from django.views.generic import View
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from libs import JsonParser, Argument, human_datetime, json_response
|
from libs import JsonParser, Argument, human_datetime, json_response
|
||||||
from libs.utils import get_request_real_ip
|
from libs.utils import get_request_real_ip
|
||||||
from apps.account.models import User, Role
|
from apps.account.models import User, Role, History
|
||||||
from apps.setting.models import Setting
|
from apps.setting.models import Setting
|
||||||
from libs.ldap import LDAP
|
from libs.ldap import LDAP
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
@ -198,6 +198,7 @@ def handle_user_info(user, x_real_ip):
|
||||||
user.last_login = human_datetime()
|
user.last_login = human_datetime()
|
||||||
user.last_ip = x_real_ip
|
user.last_ip = x_real_ip
|
||||||
user.save()
|
user.save()
|
||||||
|
History.objects.create(user=user, ip=x_real_ip)
|
||||||
return json_response({
|
return json_response({
|
||||||
'access_token': user.access_token,
|
'access_token': user.access_token,
|
||||||
'nickname': user.nickname,
|
'nickname': user.nickname,
|
||||||
|
|
|
@ -4,6 +4,6 @@ from apps.alarm.models import Alarm
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
def auto_clean_records():
|
def auto_clean_alarm_records():
|
||||||
date = datetime.now() - timedelta(days=30)
|
date = datetime.now() - timedelta(days=30)
|
||||||
Alarm.objects.filter(created_at__lt=date.strftime('%Y-%m-%d')).delete()
|
Alarm.objects.filter(created_at__lt=date.strftime('%Y-%m-%d')).delete()
|
||||||
|
|
|
@ -15,9 +15,16 @@ import json
|
||||||
|
|
||||||
|
|
||||||
def get_statistic(request):
|
def get_statistic(request):
|
||||||
|
if request.user.is_supper:
|
||||||
|
app = App.objects.count()
|
||||||
|
host = Host.objects.filter(deleted_at__isnull=True).count()
|
||||||
|
else:
|
||||||
|
deploy_perms, host_perms = request.user.deploy_perms, request.user.host_perms
|
||||||
|
app = App.objects.filter(id__in=deploy_perms['apps']).count()
|
||||||
|
host = Host.objects.filter(id__in=host_perms, deleted_at__isnull=True).count()
|
||||||
data = {
|
data = {
|
||||||
'app': App.objects.count(),
|
'app': app,
|
||||||
'host': Host.objects.filter(deleted_at__isnull=True).count(),
|
'host': host,
|
||||||
'task': Task.objects.count(),
|
'task': Task.objects.count(),
|
||||||
'detection': Detection.objects.count()
|
'detection': Detection.objects.count()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,8 @@ from apps.schedule.utils import send_fail_notify
|
||||||
from apps.notify.models import Notify
|
from apps.notify.models import Notify
|
||||||
from apps.schedule.executors import dispatch
|
from apps.schedule.executors import dispatch
|
||||||
from apps.schedule.utils import auto_clean_schedule_history
|
from apps.schedule.utils import auto_clean_schedule_history
|
||||||
from apps.alarm.utils import auto_clean_records
|
from apps.alarm.utils import auto_clean_alarm_records
|
||||||
|
from apps.account.utils import auto_clean_login_history
|
||||||
from apps.deploy.utils import auto_update_status
|
from apps.deploy.utils import auto_update_status
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from libs import AttrDict, human_datetime
|
from libs import AttrDict, human_datetime
|
||||||
|
@ -88,8 +89,9 @@ class Scheduler:
|
||||||
send_fail_notify(obj)
|
send_fail_notify(obj)
|
||||||
|
|
||||||
def _init_builtin_jobs(self):
|
def _init_builtin_jobs(self):
|
||||||
self.scheduler.add_job(auto_clean_records, 'cron', hour=0, minute=0)
|
self.scheduler.add_job(auto_clean_alarm_records, 'cron', hour=0, minute=1)
|
||||||
self.scheduler.add_job(auto_clean_schedule_history, 'cron', hour=0, minute=0)
|
self.scheduler.add_job(auto_clean_login_history, 'cron', hour=0, minute=2)
|
||||||
|
self.scheduler.add_job(auto_clean_schedule_history, 'cron', hour=0, minute=3)
|
||||||
self.scheduler.add_job(auto_update_status, 'interval', minutes=5)
|
self.scheduler.add_job(auto_update_status, 'interval', minutes=5)
|
||||||
|
|
||||||
def _init(self):
|
def _init(self):
|
||||||
|
|
|
@ -45,6 +45,7 @@ class ComTable extends React.Component {
|
||||||
ellipsis: true
|
ellipsis: true
|
||||||
}, {
|
}, {
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
width: 260,
|
||||||
className: hasPermission('deploy.app.edit|deploy.app.del') ? null : 'none',
|
className: hasPermission('deploy.app.edit|deploy.app.del') ? null : 'none',
|
||||||
render: info => (
|
render: info => (
|
||||||
<Action>
|
<Action>
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
|
|
||||||
* Copyright (c) <spug.dev@gmail.com>
|
|
||||||
* Released under the AGPL-3.0 License.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import { Card } from 'antd';
|
|
||||||
import { Chart, Geom, Axis, Tooltip, Coord, Guide, Label } from 'bizcharts';
|
|
||||||
import DataSet from "@antv/data-set";
|
|
||||||
import { http } from 'libs';
|
|
||||||
|
|
||||||
export default class AlarmTrend extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
loading: true,
|
|
||||||
host: 0,
|
|
||||||
res: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
http.get('/api/home/deploy/')
|
|
||||||
.then(res => this.setState(res))
|
|
||||||
.finally(() => this.setState({loading: false}))
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {res, host, loading} = this.state;
|
|
||||||
const dv = new DataSet.DataView();
|
|
||||||
dv.source(res).transform({
|
|
||||||
type: "percent",
|
|
||||||
field: "count",
|
|
||||||
dimension: "name",
|
|
||||||
as: "percent"
|
|
||||||
});
|
|
||||||
const cols = {
|
|
||||||
percent: {
|
|
||||||
formatter: val => {
|
|
||||||
val = val * 100 + "%";
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Card loading={loading} title="应用部署">
|
|
||||||
<Chart height={300} data={dv} scale={cols} padding={[-30, 0, -30, 50]} forceFit>
|
|
||||||
<Coord type={"theta"} radius={0.75} innerRadius={0.6}/>
|
|
||||||
<Axis name="percent"/>
|
|
||||||
<Tooltip showTitle={false}/>
|
|
||||||
<Guide>
|
|
||||||
<Guide.Html
|
|
||||||
position={["50%", "50%"]}
|
|
||||||
html={`<div style="color:#8c8c8c;font-size:1.16em;text-align: center;width: 10em;">主机<br><span style="color:#262626;font-size:2.5em">${host}</span>台</div>`}
|
|
||||||
alignX="middle"
|
|
||||||
alignY="middle"
|
|
||||||
/>
|
|
||||||
</Guide>
|
|
||||||
<Geom
|
|
||||||
type="intervalStack"
|
|
||||||
position="percent"
|
|
||||||
color="name"
|
|
||||||
tooltip={[
|
|
||||||
"name*count",
|
|
||||||
(name, count) => {
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
value: count + '台'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
style={{lineWidth: 1, stroke: "#fff"}}>
|
|
||||||
<Label
|
|
||||||
content="percent"
|
|
||||||
formatter={(val, item) => {
|
|
||||||
const percent = (item.point['percent'] * 100).toFixed(2) + '%';
|
|
||||||
return item.point.name + ": " + percent;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Geom>
|
|
||||||
</Chart>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* 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 { Card, List, Tag } from 'antd';
|
||||||
|
import { http } from 'libs';
|
||||||
|
import styles from './index.module.css';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const [name, setName] = useState(null);
|
||||||
|
const [ip, setIp] = useState(null);
|
||||||
|
const [rawData, setRawData] = useState([]);
|
||||||
|
const [dataSource, setDataSource] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
http.get('/api/account/login/history/')
|
||||||
|
.then(res => setRawData(res))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let data = rawData;
|
||||||
|
if (name) data = data.filter(x => x.nickname === name);
|
||||||
|
if (ip) data = data.filter(x => x.ip === ip);
|
||||||
|
setDataSource(data)
|
||||||
|
}, [name, ip, rawData])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card loading={loading} title="最近30天登录" bodyStyle={{paddingTop: 0}} extra={(
|
||||||
|
<div>
|
||||||
|
{name !== null && <Tag closable color="#1890ff" onClose={() => setName(null)}>{name}</Tag>}
|
||||||
|
{ip !== null && <Tag closable color="#1890ff" onClose={() => setIp(null)}>{ip}</Tag>}
|
||||||
|
</div>
|
||||||
|
)}>
|
||||||
|
<List style={{height: 329, overflow: 'scroll'}} dataSource={dataSource} renderItem={item => (
|
||||||
|
<List.Item>
|
||||||
|
<span>{item.created_at}</span>
|
||||||
|
<span className={styles.spanText} onClick={() => setName(item.nickname)}>{item.nickname}</span>
|
||||||
|
<span>通过</span>
|
||||||
|
<span className={styles.spanText} onClick={() => setIp(item.ip)}>{item.ip}</span>
|
||||||
|
<span>登录</span>
|
||||||
|
</List.Item>
|
||||||
|
)}/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import { AuthDiv } from 'components';
|
||||||
import StatisticsCard from './StatisticCard';
|
import StatisticsCard from './StatisticCard';
|
||||||
import AlarmTrend from './AlarmTrend';
|
import AlarmTrend from './AlarmTrend';
|
||||||
import RequestTop from './RequestTop';
|
import RequestTop from './RequestTop';
|
||||||
import DeployPie from './DeployPie';
|
import LoginActive from './LoginActive';
|
||||||
|
|
||||||
class HomeIndex extends React.Component {
|
class HomeIndex extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
|
@ -22,7 +22,7 @@ class HomeIndex extends React.Component {
|
||||||
<RequestTop/>
|
<RequestTop/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={10} offset={1}>
|
<Col span={10} offset={1}>
|
||||||
<DeployPie/>
|
<LoginActive/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</AuthDiv>
|
</AuthDiv>
|
||||||
|
|
|
@ -16,4 +16,10 @@
|
||||||
|
|
||||||
.spanButton:hover {
|
.spanButton:hover {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spanText {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #1890ff;
|
||||||
|
padding: 0 4px;
|
||||||
}
|
}
|
Loading…
Reference in New Issue