mirror of https://github.com/openspug/spug
				
				
				
			
						commit
						0061fc3c62
					
				
							
								
								
									
										10
									
								
								README.md
								
								
								
								
							
							
						
						
									
										10
									
								
								README.md
								
								
								
								
							| 
						 | 
				
			
			@ -10,11 +10,16 @@ Spug是面向中小型企业设计的轻量级无Agent的自动化运维平台
 | 
			
		|||
- 使用文档:https://spug.cc/docs/about-spug/
 | 
			
		||||
- 更新日志:https://spug.cc/docs/change-log/
 | 
			
		||||
- 常见问题:https://spug.cc/docs/faq/
 | 
			
		||||
- 推送助手:https://push.spug.cc
 | 
			
		||||
 | 
			
		||||
## 演示环境
 | 
			
		||||
 | 
			
		||||
演示地址:https://demo.spug.cc
 | 
			
		||||
 | 
			
		||||
## 🔥推送助手
 | 
			
		||||
 | 
			
		||||
推送助手是一个集成了电话、短信、邮件、飞书、钉钉、微信、企业微信等多通道的消息推送平台,用户只需要调用一个简单的URL,就可以完成多通道的消息推送,点击体验:[https://push.spug.cc](https://push.spug.cc)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## 特性
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -39,9 +44,10 @@ Spug是面向中小型企业设计的轻量级无Agent的自动化运维平台
 | 
			
		|||
 | 
			
		||||
## 安装
 | 
			
		||||
 | 
			
		||||
[官方文档](https://spug.cc/docs/install/)
 | 
			
		||||
[官方文档](https://spug.cc/docs/install-docker)
 | 
			
		||||
 | 
			
		||||
更多使用帮助请参考: [使用文档](https://spug.cc/docs/host-manage/)
 | 
			
		||||
 | 
			
		||||
更多使用帮助请参考 [使用文档](https://spug.cc/docs/host-manage/)。
 | 
			
		||||
 | 
			
		||||
## 推荐项目
 | 
			
		||||
[Yearning — MYSQL 开源SQL语句审核平台](https://github.com/cookieY/Yearning)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -52,10 +52,9 @@ class Command(BaseCommand):
 | 
			
		|||
                self.echo_error('缺少参数')
 | 
			
		||||
                self.print_help()
 | 
			
		||||
            user = User.objects.filter(username=options['u'], deleted_by_id__isnull=True).first()
 | 
			
		||||
            if not user:
 | 
			
		||||
                return self.echo_error(f'未找到登录名为【{options["u"]}】的账户')
 | 
			
		||||
            user.is_active = True
 | 
			
		||||
            user.save()
 | 
			
		||||
            if user:
 | 
			
		||||
                user.is_active = True
 | 
			
		||||
                user.save()
 | 
			
		||||
            cache.delete(user.username)
 | 
			
		||||
            self.echo_success('账户已启用')
 | 
			
		||||
        elif action == 'reset':
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,13 +41,12 @@ class UserView(AdminView):
 | 
			
		|||
                return json_response(error=f'已存在登录名为【{form.username}】的用户')
 | 
			
		||||
 | 
			
		||||
            role_ids, password = form.pop('role_ids'), form.pop('password')
 | 
			
		||||
            if not verify_password(password):
 | 
			
		||||
                return json_response(error='请设置至少8位包含数字、小写和大写字母的新密码')
 | 
			
		||||
 | 
			
		||||
            if form.id:
 | 
			
		||||
                user = User.objects.get(pk=form.id)
 | 
			
		||||
                user.update_by_dict(form)
 | 
			
		||||
            else:
 | 
			
		||||
                if not verify_password(password):
 | 
			
		||||
                    return json_response(error='请设置至少8位包含数字、小写和大写字母的新密码')
 | 
			
		||||
                user = User.objects.create(
 | 
			
		||||
                    password_hash=User.make_password(password),
 | 
			
		||||
                    created_by=request.user,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ from apps.setting.utils import AppSetting
 | 
			
		|||
from apps.deploy.models import Deploy, DeployRequest
 | 
			
		||||
from apps.repository.models import Repository
 | 
			
		||||
from apps.deploy.utils import dispatch as deploy_dispatch
 | 
			
		||||
from libs.utils import human_datetime
 | 
			
		||||
from threading import Thread
 | 
			
		||||
import hashlib
 | 
			
		||||
import hmac
 | 
			
		||||
| 
						 | 
				
			
			@ -119,4 +120,7 @@ def _dispatch(deploy_id, ref, commit_id=None, message=None):
 | 
			
		|||
 | 
			
		||||
    req.save()
 | 
			
		||||
    if req.status == '2':
 | 
			
		||||
        req.do_at = human_datetime()
 | 
			
		||||
        req.do_by = deploy.created_by
 | 
			
		||||
        req.save()
 | 
			
		||||
        deploy_dispatch(req)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -130,6 +130,10 @@ class RequestDetailView(View):
 | 
			
		|||
                        outputs[item['key']]['status'] = item['status']
 | 
			
		||||
            data = rds.lrange(key, counter, counter + 9)
 | 
			
		||||
        response['index'] = counter
 | 
			
		||||
        if counter == 0:
 | 
			
		||||
            for item in outputs:
 | 
			
		||||
                outputs[item]['data'] += '\r\n\r\n未读取到数据,Spug 仅保存最近2周的日志信息。'
 | 
			
		||||
 | 
			
		||||
        if req.is_quick_deploy:
 | 
			
		||||
            if outputs['local']['data']:
 | 
			
		||||
                outputs['local']['data'] = f'{human_time()} 读取数据...        ' + outputs['local']['data']
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -84,7 +84,7 @@ class HostExtend(models.Model, ModelMixin):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class Group(models.Model, ModelMixin):
 | 
			
		||||
    name = models.CharField(max_length=20)
 | 
			
		||||
    name = models.CharField(max_length=50)
 | 
			
		||||
    parent_id = models.IntegerField(default=0)
 | 
			
		||||
    sort_id = models.IntegerField(default=0)
 | 
			
		||||
    hosts = models.ManyToManyField(Host, related_name='groups')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -202,6 +202,8 @@ def fetch_host_extend(ssh):
 | 
			
		|||
        code, out = ssh.exec_command_raw('hostname -I')
 | 
			
		||||
        if code == 0:
 | 
			
		||||
            for ip in out.strip().split():
 | 
			
		||||
                if len(ip) > 15:   # ignore ipv6
 | 
			
		||||
                    continue
 | 
			
		||||
                if ipaddress.ip_address(ip).is_global:
 | 
			
		||||
                    if len(public_ip_address) < 10:
 | 
			
		||||
                        public_ip_address.add(ip)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -114,6 +114,7 @@ def get_overview(request):
 | 
			
		|||
    for item in Detection.objects.all():
 | 
			
		||||
        data = {}
 | 
			
		||||
        for key in json.loads(item.targets):
 | 
			
		||||
            key = str(key)
 | 
			
		||||
            data[key] = {
 | 
			
		||||
                'id': f'{item.id}_{key}',
 | 
			
		||||
                'group': item.group,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ from apps.account.models import History
 | 
			
		|||
from apps.alarm.models import Alarm
 | 
			
		||||
from apps.schedule.models import Task, History as TaskHistory
 | 
			
		||||
from apps.deploy.models import DeployRequest
 | 
			
		||||
from apps.app.models import DeployExtend1
 | 
			
		||||
from apps.exec.models import ExecHistory
 | 
			
		||||
from apps.notify.models import Notify
 | 
			
		||||
from apps.deploy.utils import dispatch
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +22,12 @@ def auto_run_by_day():
 | 
			
		|||
        History.objects.filter(created_at__lt=date_30).delete()
 | 
			
		||||
        Notify.objects.filter(created_at__lt=date_7, unread=False).delete()
 | 
			
		||||
        Alarm.objects.filter(created_at__lt=date_30).delete()
 | 
			
		||||
        for item in DeployExtend1.objects.all():
 | 
			
		||||
            index = 0
 | 
			
		||||
            for req in DeployRequest.objects.filter(deploy_id=item.deploy_id, repository_id__isnull=False):
 | 
			
		||||
                if index > item.versions and req.repository_id:
 | 
			
		||||
                    req.repository.delete()
 | 
			
		||||
                index += 1
 | 
			
		||||
        try:
 | 
			
		||||
            record = ExecHistory.objects.all()[50]
 | 
			
		||||
            ExecHistory.objects.filter(id__lt=record.id).delete()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -131,7 +131,7 @@ AUTHENTICATION_EXCLUDES = (
 | 
			
		|||
    re.compile('/apis/.*'),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SPUG_VERSION = 'v3.1.5'
 | 
			
		||||
SPUG_VERSION = 'v3.1.7'
 | 
			
		||||
 | 
			
		||||
# override default config
 | 
			
		||||
try:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,4 +10,4 @@ export * from './functools';
 | 
			
		|||
export * from './router';
 | 
			
		||||
export const http = _http;
 | 
			
		||||
export const history = _history;
 | 
			
		||||
export const VERSION = 'v3.1.5';
 | 
			
		||||
export const VERSION = 'v3.1.7';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ export default observer(function () {
 | 
			
		|||
          </Form.Item>
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col span={10}>
 | 
			
		||||
          <Form.Item required label="版本数量" tooltip="早于指定数量的历史版本会被删除,以释放磁盘空间。">
 | 
			
		||||
          <Form.Item required label="版本数量" tooltip="早于指定数量的构建纪录及历史版本会被删除,以释放磁盘空间。">
 | 
			
		||||
            <Input value={info['versions']} onChange={e => info['versions'] = e.target.value} placeholder="请输入保存的版本数量"/>
 | 
			
		||||
          </Form.Item>
 | 
			
		||||
        </Col>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,12 +55,12 @@ export default observer(function () {
 | 
			
		|||
        <Form.Item required name="name" label="申请标题">
 | 
			
		||||
          <Input placeholder="请输入申请标题"/>
 | 
			
		||||
        </Form.Item>
 | 
			
		||||
        <Form.Item required name="request_id" label="选择版本">
 | 
			
		||||
        <Form.Item required name="request_id" label="选择版本" tooltip="可选择回滚版本与发布配置中的版本数量配置相关。">
 | 
			
		||||
          <Select
 | 
			
		||||
            showSearch
 | 
			
		||||
            placeholder="请选择回滚至哪个版本"
 | 
			
		||||
            filterOption={(input, option) => includes(option.props.children, input)}>
 | 
			
		||||
            {store.records.filter(x => x.deploy_id === deploy_id && ['3', '-3'].includes(x.status)).map((item, index) => (
 | 
			
		||||
            {store.records.filter(x => x.repository_id && x.deploy_id === deploy_id && ['3', '-3'].includes(x.status)).map((item, index) => (
 | 
			
		||||
              <Select.Option key={item.id} value={item.id} record={item} disabled={index === 0}>
 | 
			
		||||
                <div style={{display: 'flex', justifyContent: 'space-between'}}>
 | 
			
		||||
                  <span>{`${item.name} (${item.version})`}</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import { http, hasPermission } from 'libs';
 | 
			
		|||
import { Action, AuthButton, TableCard } from 'components';
 | 
			
		||||
import S from './index.module.less';
 | 
			
		||||
import store from './store';
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
 | 
			
		||||
function DeployConfirm() {
 | 
			
		||||
  return (
 | 
			
		||||
| 
						 | 
				
			
			@ -72,7 +73,7 @@ function ComTable() {
 | 
			
		|||
    className: S.min120,
 | 
			
		||||
    dataIndex: 'created_at',
 | 
			
		||||
    sorter: (a, b) => a['created_at'].localeCompare(b['created_at']),
 | 
			
		||||
    render: v => <Tooltip title={v}>{v ? v.substring(0, 10) : null}</Tooltip>,
 | 
			
		||||
    render: v => <Tooltip title={v}>{v ? moment(v).fromNow() : null}</Tooltip>,
 | 
			
		||||
    hide: true
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '审核人',
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +84,7 @@ function ComTable() {
 | 
			
		|||
    title: '审核时间',
 | 
			
		||||
    className: S.min120,
 | 
			
		||||
    dataIndex: 'approve_at',
 | 
			
		||||
    render: v => <Tooltip title={v}>{v ? v.substring(0, 10) : null}</Tooltip>,
 | 
			
		||||
    render: v => <Tooltip title={v}>{v ? moment(v).fromNow() : null}</Tooltip>,
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '发布人',
 | 
			
		||||
    className: S.min120,
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +94,7 @@ function ComTable() {
 | 
			
		|||
    title: '发布时间',
 | 
			
		||||
    className: S.min120,
 | 
			
		||||
    dataIndex: 'do_at',
 | 
			
		||||
    render: v => <Tooltip title={v}>{v ? v.substring(0, 10) : null}</Tooltip>,
 | 
			
		||||
    render: v => <Tooltip title={v}>{v ? moment(v).fromNow() : null}</Tooltip>,
 | 
			
		||||
    hide: true
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '备注',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,14 +4,15 @@
 | 
			
		|||
 * Released under the AGPL-3.0 License.
 | 
			
		||||
 */
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Breadcrumb, Table, Switch, Progress, Modal, message } from 'antd';
 | 
			
		||||
import { Breadcrumb, Table, Switch, Progress, Modal, Input, message } from 'antd';
 | 
			
		||||
import {
 | 
			
		||||
  DeleteOutlined,
 | 
			
		||||
  DownloadOutlined,
 | 
			
		||||
  FileOutlined,
 | 
			
		||||
  FolderOutlined,
 | 
			
		||||
  HomeOutlined,
 | 
			
		||||
  UploadOutlined
 | 
			
		||||
  UploadOutlined,
 | 
			
		||||
  EditOutlined
 | 
			
		||||
} from '@ant-design/icons';
 | 
			
		||||
import { AuthButton, Action } from 'components';
 | 
			
		||||
import { http, uniqueId, X_TOKEN } from 'libs';
 | 
			
		||||
| 
						 | 
				
			
			@ -23,10 +24,12 @@ class FileManager extends React.Component {
 | 
			
		|||
  constructor(props) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.input = null;
 | 
			
		||||
    this.input2 = null
 | 
			
		||||
    this.state = {
 | 
			
		||||
      fetching: false,
 | 
			
		||||
      showDot: false,
 | 
			
		||||
      uploading: false,
 | 
			
		||||
      inputPath: null,
 | 
			
		||||
      uploadStatus: 'active',
 | 
			
		||||
      pwd: [],
 | 
			
		||||
      objects: [],
 | 
			
		||||
| 
						 | 
				
			
			@ -97,10 +100,11 @@ class FileManager extends React.Component {
 | 
			
		|||
    this.setState({fetching: true});
 | 
			
		||||
    pwd = pwd || this.state.pwd;
 | 
			
		||||
    const path = '/' + pwd.join('/');
 | 
			
		||||
    http.get('/api/file/', {params: {id: this.props.id, path}})
 | 
			
		||||
    return http.get('/api/file/', {params: {id: this.props.id, path}})
 | 
			
		||||
      .then(res => {
 | 
			
		||||
        const objects = lds.orderBy(res, [this._kindSort, 'name'], ['desc', 'asc']);
 | 
			
		||||
        this.setState({objects, pwd})
 | 
			
		||||
        this.state.inputPath !== null && this.setState({inputPath: path})
 | 
			
		||||
      })
 | 
			
		||||
      .finally(() => this.setState({fetching: false}))
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +113,7 @@ class FileManager extends React.Component {
 | 
			
		|||
    let pwd = this.state.pwd.map(x => x);
 | 
			
		||||
    if (action === '1') {
 | 
			
		||||
      pwd.push(name)
 | 
			
		||||
      this.setState({inputPath: null})
 | 
			
		||||
    } else if (action === '2') {
 | 
			
		||||
      const index = pwd.indexOf(name);
 | 
			
		||||
      pwd = pwd.splice(0, index + 1)
 | 
			
		||||
| 
						 | 
				
			
			@ -118,6 +123,28 @@ class FileManager extends React.Component {
 | 
			
		|||
    this.fetchFiles(pwd)
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handlePathInput = (e) => {
 | 
			
		||||
    const value = e.target.value;
 | 
			
		||||
    const pwd = value.substring(1).split('/')
 | 
			
		||||
    this.setState({pwd})
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleInputEnter = () => {
 | 
			
		||||
    if (this.state.inputPath === null) {
 | 
			
		||||
      if (this.state.pwd.length > 0) {
 | 
			
		||||
        this.setState({inputPath: `/${this.state.pwd.join('/')}/`})
 | 
			
		||||
      } else {
 | 
			
		||||
        this.setState({inputPath: '/'})
 | 
			
		||||
      }
 | 
			
		||||
      setTimeout(() => this.input2.focus(), 100)
 | 
			
		||||
    } else {
 | 
			
		||||
      let pwdStr = this.state.inputPath.replace(/^\/+/, '')
 | 
			
		||||
      pwdStr = pwdStr.replace(/\/+$/, '')
 | 
			
		||||
      this.fetchFiles(pwdStr.split('/'))
 | 
			
		||||
        .then(() => this.setState({inputPath: null}))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleUpload = () => {
 | 
			
		||||
    this.input.click();
 | 
			
		||||
    this.input.onchange = e => {
 | 
			
		||||
| 
						 | 
				
			
			@ -198,16 +225,27 @@ class FileManager extends React.Component {
 | 
			
		|||
      <React.Fragment>
 | 
			
		||||
        <input style={{display: 'none'}} type="file" ref={ref => this.input = ref}/>
 | 
			
		||||
        <div className={styles.drawerHeader}>
 | 
			
		||||
          <Breadcrumb>
 | 
			
		||||
            <Breadcrumb.Item href="#" onClick={() => this.handleChdir('', '0')}>
 | 
			
		||||
              <HomeOutlined/>
 | 
			
		||||
            </Breadcrumb.Item>
 | 
			
		||||
            {this.state.pwd.map(item => (
 | 
			
		||||
              <Breadcrumb.Item key={item} href="#" onClick={() => this.handleChdir(item, '2')}>
 | 
			
		||||
                <span>{item}</span>
 | 
			
		||||
          {this.state.inputPath !== null ? (
 | 
			
		||||
            <Input ref={ref => this.input2 = ref} size="small" className={styles.input}
 | 
			
		||||
                   suffix={<div style={{color: '#999', fontSize: 12}}>回车确认</div>}
 | 
			
		||||
                   value={this.state.inputPath} onChange={e => this.setState({inputPath: e.target.value})}
 | 
			
		||||
                   onPressEnter={this.handleInputEnter}/>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Breadcrumb className={styles.bread}>
 | 
			
		||||
              <Breadcrumb.Item href="#" onClick={() => this.handleChdir('', '0')}>
 | 
			
		||||
                <HomeOutlined style={{fontSize: 16}}/>
 | 
			
		||||
              </Breadcrumb.Item>
 | 
			
		||||
            ))}
 | 
			
		||||
          </Breadcrumb>
 | 
			
		||||
              {this.state.pwd.map(item => (
 | 
			
		||||
                <Breadcrumb.Item key={item} href="#" onClick={() => this.handleChdir(item, '2')}>
 | 
			
		||||
                  <span>{item}</span>
 | 
			
		||||
                </Breadcrumb.Item>
 | 
			
		||||
              ))}
 | 
			
		||||
              <Breadcrumb.Item onClick={this.handleInputEnter}>
 | 
			
		||||
                <EditOutlined className={styles.edit}/>
 | 
			
		||||
              </Breadcrumb.Item>
 | 
			
		||||
            </Breadcrumb>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <div className={styles.action}>
 | 
			
		||||
            <span>显示隐藏文件:</span>
 | 
			
		||||
            <Switch
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -162,6 +162,24 @@
 | 
			
		|||
    height: 24px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .bread:hover {
 | 
			
		||||
    .edit {
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .input {
 | 
			
		||||
    width: 60%;
 | 
			
		||||
    font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .edit {
 | 
			
		||||
    display: none;
 | 
			
		||||
    color: #2563fcbb;
 | 
			
		||||
    margin-left: 24px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .progress {
 | 
			
		||||
    width: 94px;
 | 
			
		||||
    margin-left: 12px;
 | 
			
		||||
| 
						 | 
				
			
			@ -174,6 +192,15 @@
 | 
			
		|||
    white-space: nowrap;
 | 
			
		||||
    margin-right: 24px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  :global(.ant-breadcrumb-separator) {
 | 
			
		||||
    margin: 0 4px;
 | 
			
		||||
    color: rgba(0, 0, 0, 0.85);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  :global(.ant-breadcrumb-link) {
 | 
			
		||||
    color: rgba(0, 0, 0, 0.85);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.drawerBtn {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue