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/about-spug/
|
||||||
- 更新日志:https://spug.cc/docs/change-log/
|
- 更新日志:https://spug.cc/docs/change-log/
|
||||||
- 常见问题:https://spug.cc/docs/faq/
|
- 常见问题:https://spug.cc/docs/faq/
|
||||||
|
- 推送助手:https://push.spug.cc
|
||||||
|
|
||||||
## 演示环境
|
## 演示环境
|
||||||
|
|
||||||
演示地址:https://demo.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)
|
[Yearning — MYSQL 开源SQL语句审核平台](https://github.com/cookieY/Yearning)
|
||||||
|
|
|
@ -52,8 +52,7 @@ class Command(BaseCommand):
|
||||||
self.echo_error('缺少参数')
|
self.echo_error('缺少参数')
|
||||||
self.print_help()
|
self.print_help()
|
||||||
user = User.objects.filter(username=options['u'], deleted_by_id__isnull=True).first()
|
user = User.objects.filter(username=options['u'], deleted_by_id__isnull=True).first()
|
||||||
if not user:
|
if user:
|
||||||
return self.echo_error(f'未找到登录名为【{options["u"]}】的账户')
|
|
||||||
user.is_active = True
|
user.is_active = True
|
||||||
user.save()
|
user.save()
|
||||||
cache.delete(user.username)
|
cache.delete(user.username)
|
||||||
|
|
|
@ -41,13 +41,12 @@ class UserView(AdminView):
|
||||||
return json_response(error=f'已存在登录名为【{form.username}】的用户')
|
return json_response(error=f'已存在登录名为【{form.username}】的用户')
|
||||||
|
|
||||||
role_ids, password = form.pop('role_ids'), form.pop('password')
|
role_ids, password = form.pop('role_ids'), form.pop('password')
|
||||||
if not verify_password(password):
|
|
||||||
return json_response(error='请设置至少8位包含数字、小写和大写字母的新密码')
|
|
||||||
|
|
||||||
if form.id:
|
if form.id:
|
||||||
user = User.objects.get(pk=form.id)
|
user = User.objects.get(pk=form.id)
|
||||||
user.update_by_dict(form)
|
user.update_by_dict(form)
|
||||||
else:
|
else:
|
||||||
|
if not verify_password(password):
|
||||||
|
return json_response(error='请设置至少8位包含数字、小写和大写字母的新密码')
|
||||||
user = User.objects.create(
|
user = User.objects.create(
|
||||||
password_hash=User.make_password(password),
|
password_hash=User.make_password(password),
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
|
|
|
@ -6,6 +6,7 @@ from apps.setting.utils import AppSetting
|
||||||
from apps.deploy.models import Deploy, DeployRequest
|
from apps.deploy.models import Deploy, DeployRequest
|
||||||
from apps.repository.models import Repository
|
from apps.repository.models import Repository
|
||||||
from apps.deploy.utils import dispatch as deploy_dispatch
|
from apps.deploy.utils import dispatch as deploy_dispatch
|
||||||
|
from libs.utils import human_datetime
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
@ -119,4 +120,7 @@ def _dispatch(deploy_id, ref, commit_id=None, message=None):
|
||||||
|
|
||||||
req.save()
|
req.save()
|
||||||
if req.status == '2':
|
if req.status == '2':
|
||||||
|
req.do_at = human_datetime()
|
||||||
|
req.do_by = deploy.created_by
|
||||||
|
req.save()
|
||||||
deploy_dispatch(req)
|
deploy_dispatch(req)
|
||||||
|
|
|
@ -130,6 +130,10 @@ class RequestDetailView(View):
|
||||||
outputs[item['key']]['status'] = item['status']
|
outputs[item['key']]['status'] = item['status']
|
||||||
data = rds.lrange(key, counter, counter + 9)
|
data = rds.lrange(key, counter, counter + 9)
|
||||||
response['index'] = counter
|
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 req.is_quick_deploy:
|
||||||
if outputs['local']['data']:
|
if outputs['local']['data']:
|
||||||
outputs['local']['data'] = f'{human_time()} 读取数据... ' + 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):
|
class Group(models.Model, ModelMixin):
|
||||||
name = models.CharField(max_length=20)
|
name = models.CharField(max_length=50)
|
||||||
parent_id = models.IntegerField(default=0)
|
parent_id = models.IntegerField(default=0)
|
||||||
sort_id = models.IntegerField(default=0)
|
sort_id = models.IntegerField(default=0)
|
||||||
hosts = models.ManyToManyField(Host, related_name='groups')
|
hosts = models.ManyToManyField(Host, related_name='groups')
|
||||||
|
|
|
@ -202,6 +202,8 @@ def fetch_host_extend(ssh):
|
||||||
code, out = ssh.exec_command_raw('hostname -I')
|
code, out = ssh.exec_command_raw('hostname -I')
|
||||||
if code == 0:
|
if code == 0:
|
||||||
for ip in out.strip().split():
|
for ip in out.strip().split():
|
||||||
|
if len(ip) > 15: # ignore ipv6
|
||||||
|
continue
|
||||||
if ipaddress.ip_address(ip).is_global:
|
if ipaddress.ip_address(ip).is_global:
|
||||||
if len(public_ip_address) < 10:
|
if len(public_ip_address) < 10:
|
||||||
public_ip_address.add(ip)
|
public_ip_address.add(ip)
|
||||||
|
|
|
@ -114,6 +114,7 @@ def get_overview(request):
|
||||||
for item in Detection.objects.all():
|
for item in Detection.objects.all():
|
||||||
data = {}
|
data = {}
|
||||||
for key in json.loads(item.targets):
|
for key in json.loads(item.targets):
|
||||||
|
key = str(key)
|
||||||
data[key] = {
|
data[key] = {
|
||||||
'id': f'{item.id}_{key}',
|
'id': f'{item.id}_{key}',
|
||||||
'group': item.group,
|
'group': item.group,
|
||||||
|
|
|
@ -6,6 +6,7 @@ from apps.account.models import History
|
||||||
from apps.alarm.models import Alarm
|
from apps.alarm.models import Alarm
|
||||||
from apps.schedule.models import Task, History as TaskHistory
|
from apps.schedule.models import Task, History as TaskHistory
|
||||||
from apps.deploy.models import DeployRequest
|
from apps.deploy.models import DeployRequest
|
||||||
|
from apps.app.models import DeployExtend1
|
||||||
from apps.exec.models import ExecHistory
|
from apps.exec.models import ExecHistory
|
||||||
from apps.notify.models import Notify
|
from apps.notify.models import Notify
|
||||||
from apps.deploy.utils import dispatch
|
from apps.deploy.utils import dispatch
|
||||||
|
@ -21,6 +22,12 @@ def auto_run_by_day():
|
||||||
History.objects.filter(created_at__lt=date_30).delete()
|
History.objects.filter(created_at__lt=date_30).delete()
|
||||||
Notify.objects.filter(created_at__lt=date_7, unread=False).delete()
|
Notify.objects.filter(created_at__lt=date_7, unread=False).delete()
|
||||||
Alarm.objects.filter(created_at__lt=date_30).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:
|
try:
|
||||||
record = ExecHistory.objects.all()[50]
|
record = ExecHistory.objects.all()[50]
|
||||||
ExecHistory.objects.filter(id__lt=record.id).delete()
|
ExecHistory.objects.filter(id__lt=record.id).delete()
|
||||||
|
|
|
@ -131,7 +131,7 @@ AUTHENTICATION_EXCLUDES = (
|
||||||
re.compile('/apis/.*'),
|
re.compile('/apis/.*'),
|
||||||
)
|
)
|
||||||
|
|
||||||
SPUG_VERSION = 'v3.1.5'
|
SPUG_VERSION = 'v3.1.7'
|
||||||
|
|
||||||
# override default config
|
# override default config
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -10,4 +10,4 @@ export * from './functools';
|
||||||
export * from './router';
|
export * from './router';
|
||||||
export const http = _http;
|
export const http = _http;
|
||||||
export const history = _history;
|
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>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={10}>
|
<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="请输入保存的版本数量"/>
|
<Input value={info['versions']} onChange={e => info['versions'] = e.target.value} placeholder="请输入保存的版本数量"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -55,12 +55,12 @@ export default observer(function () {
|
||||||
<Form.Item required name="name" label="申请标题">
|
<Form.Item required name="name" label="申请标题">
|
||||||
<Input placeholder="请输入申请标题"/>
|
<Input placeholder="请输入申请标题"/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item required name="request_id" label="选择版本">
|
<Form.Item required name="request_id" label="选择版本" tooltip="可选择回滚版本与发布配置中的版本数量配置相关。">
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
placeholder="请选择回滚至哪个版本"
|
placeholder="请选择回滚至哪个版本"
|
||||||
filterOption={(input, option) => includes(option.props.children, input)}>
|
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}>
|
<Select.Option key={item.id} value={item.id} record={item} disabled={index === 0}>
|
||||||
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||||
<span>{`${item.name} (${item.version})`}</span>
|
<span>{`${item.name} (${item.version})`}</span>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { http, hasPermission } from 'libs';
|
||||||
import { Action, AuthButton, TableCard } from 'components';
|
import { Action, AuthButton, TableCard } from 'components';
|
||||||
import S from './index.module.less';
|
import S from './index.module.less';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
function DeployConfirm() {
|
function DeployConfirm() {
|
||||||
return (
|
return (
|
||||||
|
@ -72,7 +73,7 @@ function ComTable() {
|
||||||
className: S.min120,
|
className: S.min120,
|
||||||
dataIndex: 'created_at',
|
dataIndex: 'created_at',
|
||||||
sorter: (a, b) => a['created_at'].localeCompare(b['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
|
hide: true
|
||||||
}, {
|
}, {
|
||||||
title: '审核人',
|
title: '审核人',
|
||||||
|
@ -83,7 +84,7 @@ function ComTable() {
|
||||||
title: '审核时间',
|
title: '审核时间',
|
||||||
className: S.min120,
|
className: S.min120,
|
||||||
dataIndex: 'approve_at',
|
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: '发布人',
|
title: '发布人',
|
||||||
className: S.min120,
|
className: S.min120,
|
||||||
|
@ -93,7 +94,7 @@ function ComTable() {
|
||||||
title: '发布时间',
|
title: '发布时间',
|
||||||
className: S.min120,
|
className: S.min120,
|
||||||
dataIndex: 'do_at',
|
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
|
hide: true
|
||||||
}, {
|
}, {
|
||||||
title: '备注',
|
title: '备注',
|
||||||
|
|
|
@ -4,14 +4,15 @@
|
||||||
* Released under the AGPL-3.0 License.
|
* Released under the AGPL-3.0 License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Breadcrumb, Table, Switch, Progress, Modal, message } from 'antd';
|
import { Breadcrumb, Table, Switch, Progress, Modal, Input, message } from 'antd';
|
||||||
import {
|
import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
FileOutlined,
|
FileOutlined,
|
||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
HomeOutlined,
|
HomeOutlined,
|
||||||
UploadOutlined
|
UploadOutlined,
|
||||||
|
EditOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { AuthButton, Action } from 'components';
|
import { AuthButton, Action } from 'components';
|
||||||
import { http, uniqueId, X_TOKEN } from 'libs';
|
import { http, uniqueId, X_TOKEN } from 'libs';
|
||||||
|
@ -23,10 +24,12 @@ class FileManager extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.input = null;
|
this.input = null;
|
||||||
|
this.input2 = null
|
||||||
this.state = {
|
this.state = {
|
||||||
fetching: false,
|
fetching: false,
|
||||||
showDot: false,
|
showDot: false,
|
||||||
uploading: false,
|
uploading: false,
|
||||||
|
inputPath: null,
|
||||||
uploadStatus: 'active',
|
uploadStatus: 'active',
|
||||||
pwd: [],
|
pwd: [],
|
||||||
objects: [],
|
objects: [],
|
||||||
|
@ -97,10 +100,11 @@ class FileManager extends React.Component {
|
||||||
this.setState({fetching: true});
|
this.setState({fetching: true});
|
||||||
pwd = pwd || this.state.pwd;
|
pwd = pwd || this.state.pwd;
|
||||||
const path = '/' + pwd.join('/');
|
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 => {
|
.then(res => {
|
||||||
const objects = lds.orderBy(res, [this._kindSort, 'name'], ['desc', 'asc']);
|
const objects = lds.orderBy(res, [this._kindSort, 'name'], ['desc', 'asc']);
|
||||||
this.setState({objects, pwd})
|
this.setState({objects, pwd})
|
||||||
|
this.state.inputPath !== null && this.setState({inputPath: path})
|
||||||
})
|
})
|
||||||
.finally(() => this.setState({fetching: false}))
|
.finally(() => this.setState({fetching: false}))
|
||||||
};
|
};
|
||||||
|
@ -109,6 +113,7 @@ class FileManager extends React.Component {
|
||||||
let pwd = this.state.pwd.map(x => x);
|
let pwd = this.state.pwd.map(x => x);
|
||||||
if (action === '1') {
|
if (action === '1') {
|
||||||
pwd.push(name)
|
pwd.push(name)
|
||||||
|
this.setState({inputPath: null})
|
||||||
} else if (action === '2') {
|
} else if (action === '2') {
|
||||||
const index = pwd.indexOf(name);
|
const index = pwd.indexOf(name);
|
||||||
pwd = pwd.splice(0, index + 1)
|
pwd = pwd.splice(0, index + 1)
|
||||||
|
@ -118,6 +123,28 @@ class FileManager extends React.Component {
|
||||||
this.fetchFiles(pwd)
|
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 = () => {
|
handleUpload = () => {
|
||||||
this.input.click();
|
this.input.click();
|
||||||
this.input.onchange = e => {
|
this.input.onchange = e => {
|
||||||
|
@ -198,16 +225,27 @@ class FileManager extends React.Component {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<input style={{display: 'none'}} type="file" ref={ref => this.input = ref}/>
|
<input style={{display: 'none'}} type="file" ref={ref => this.input = ref}/>
|
||||||
<div className={styles.drawerHeader}>
|
<div className={styles.drawerHeader}>
|
||||||
<Breadcrumb>
|
{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')}>
|
<Breadcrumb.Item href="#" onClick={() => this.handleChdir('', '0')}>
|
||||||
<HomeOutlined/>
|
<HomeOutlined style={{fontSize: 16}}/>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
{this.state.pwd.map(item => (
|
{this.state.pwd.map(item => (
|
||||||
<Breadcrumb.Item key={item} href="#" onClick={() => this.handleChdir(item, '2')}>
|
<Breadcrumb.Item key={item} href="#" onClick={() => this.handleChdir(item, '2')}>
|
||||||
<span>{item}</span>
|
<span>{item}</span>
|
||||||
</Breadcrumb.Item>
|
</Breadcrumb.Item>
|
||||||
))}
|
))}
|
||||||
|
<Breadcrumb.Item onClick={this.handleInputEnter}>
|
||||||
|
<EditOutlined className={styles.edit}/>
|
||||||
|
</Breadcrumb.Item>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.action}>
|
<div className={styles.action}>
|
||||||
<span>显示隐藏文件:</span>
|
<span>显示隐藏文件:</span>
|
||||||
<Switch
|
<Switch
|
||||||
|
|
|
@ -162,6 +162,24 @@
|
||||||
height: 24px;
|
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 {
|
.progress {
|
||||||
width: 94px;
|
width: 94px;
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
|
@ -174,6 +192,15 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-right: 24px;
|
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 {
|
.drawerBtn {
|
||||||
|
|
Loading…
Reference in New Issue