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:
 | 
			
		||||
        db_table = 'roles'
 | 
			
		||||
        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 apps.account.views import *
 | 
			
		||||
from apps.account.history import *
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    url(r'^login/', login),
 | 
			
		||||
    url(r'^logout/', logout),
 | 
			
		||||
    url(r'^login/$', login),
 | 
			
		||||
    url(r'^logout/$', logout),
 | 
			
		||||
    url(r'^user/$', UserView.as_view()),
 | 
			
		||||
    url(r'^role/$', RoleView.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 libs import JsonParser, Argument, human_datetime, json_response
 | 
			
		||||
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 libs.ldap import LDAP
 | 
			
		||||
import ipaddress
 | 
			
		||||
| 
						 | 
				
			
			@ -198,6 +198,7 @@ def handle_user_info(user, x_real_ip):
 | 
			
		|||
    user.last_login = human_datetime()
 | 
			
		||||
    user.last_ip = x_real_ip
 | 
			
		||||
    user.save()
 | 
			
		||||
    History.objects.create(user=user, ip=x_real_ip)
 | 
			
		||||
    return json_response({
 | 
			
		||||
        'access_token': user.access_token,
 | 
			
		||||
        'nickname': user.nickname,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,6 @@ from apps.alarm.models import Alarm
 | 
			
		|||
from datetime import datetime, timedelta
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def auto_clean_records():
 | 
			
		||||
def auto_clean_alarm_records():
 | 
			
		||||
    date = datetime.now() - timedelta(days=30)
 | 
			
		||||
    Alarm.objects.filter(created_at__lt=date.strftime('%Y-%m-%d')).delete()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,9 +15,16 @@ import json
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
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 = {
 | 
			
		||||
        'app': App.objects.count(),
 | 
			
		||||
        'host': Host.objects.filter(deleted_at__isnull=True).count(),
 | 
			
		||||
        'app': app,
 | 
			
		||||
        'host': host,
 | 
			
		||||
        'task': Task.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.schedule.executors import dispatch
 | 
			
		||||
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 django.conf import settings
 | 
			
		||||
from libs import AttrDict, human_datetime
 | 
			
		||||
| 
						 | 
				
			
			@ -88,8 +89,9 @@ class Scheduler:
 | 
			
		|||
                    send_fail_notify(obj)
 | 
			
		||||
 | 
			
		||||
    def _init_builtin_jobs(self):
 | 
			
		||||
        self.scheduler.add_job(auto_clean_records, 'cron', hour=0, minute=0)
 | 
			
		||||
        self.scheduler.add_job(auto_clean_schedule_history, 'cron', hour=0, minute=0)
 | 
			
		||||
        self.scheduler.add_job(auto_clean_alarm_records, 'cron', hour=0, minute=1)
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
    def _init(self):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,6 +45,7 @@ class ComTable extends React.Component {
 | 
			
		|||
    ellipsis: true
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '操作',
 | 
			
		||||
    width: 260,
 | 
			
		||||
    className: hasPermission('deploy.app.edit|deploy.app.del') ? null : 'none',
 | 
			
		||||
    render: info => (
 | 
			
		||||
      <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 AlarmTrend from './AlarmTrend';
 | 
			
		||||
import RequestTop from './RequestTop';
 | 
			
		||||
import DeployPie from './DeployPie';
 | 
			
		||||
import LoginActive from './LoginActive';
 | 
			
		||||
 | 
			
		||||
class HomeIndex extends React.Component {
 | 
			
		||||
  render() {
 | 
			
		||||
| 
						 | 
				
			
			@ -22,7 +22,7 @@ class HomeIndex extends React.Component {
 | 
			
		|||
            <RequestTop/>
 | 
			
		||||
          </Col>
 | 
			
		||||
          <Col span={10} offset={1}>
 | 
			
		||||
            <DeployPie/>
 | 
			
		||||
            <LoginActive/>
 | 
			
		||||
          </Col>
 | 
			
		||||
        </Row>
 | 
			
		||||
      </AuthDiv>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,4 +16,10 @@
 | 
			
		|||
 | 
			
		||||
.spanButton:hover {
 | 
			
		||||
    color: #1890ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spanText {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    color: #1890ff;
 | 
			
		||||
    padding: 0 4px;
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue