diff --git a/README.md b/README.md index 24c512e3f13c49400f5d0234feda8461a7092b98..67869e94a4d2e9c4f4641968e13d44649e2f2b20 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Spug是面向中小型企业设计的轻量级无Agent的自动化运维平台 ## 🔥推送助手 -推送助手是一个集成了电话、短信、邮件、飞书、钉钉、微信、企业微信等多通道的消息推送平台,用户只需要调用一个简单的URL,就可以完成多通道的消息推送,点击体验:[https://push.spug.cc](https://push.spug.cc) +推送助手是一个集成了电话、短信、邮件、飞书、钉钉、微信、企业微信等多通道的消息推送平台,可以3分钟实现Zabbix、Prometheus、夜莺等监控系统的电话短信报警,点击体验:[https://push.spug.cc](https://push.spug.cc) ## 特性 diff --git a/spug_api/apps/app/views.py b/spug_api/apps/app/views.py index 67c0336a52f2eed38930c7d206ac135d00b05b32..94b640d7b338c9b837e266837780f97bfac6168f 100644 --- a/spug_api/apps/app/views.py +++ b/spug_api/apps/app/views.py @@ -5,7 +5,7 @@ from django.views.generic import View from django.db.models import F from libs import JsonParser, Argument, json_response, auth from apps.app.models import App, Deploy, DeployExtend1, DeployExtend2 -from apps.config.models import Config, ConfigHistory +from apps.config.models import Config, ConfigHistory, Service from apps.app.utils import fetch_versions, remove_repo from apps.setting.utils import AppSetting import json @@ -14,12 +14,21 @@ import re class AppView(View): def get(self, request): - if request.user.is_supper: - apps = App.objects.all() - else: - ids = request.user.deploy_perms['apps'] - apps = App.objects.filter(id__in=ids) - return json_response(apps) + form, error = JsonParser( + Argument('id', type=int, required=False) + ).parse(request.GET) + if error is None: + if request.user.is_supper: + apps = App.objects.all() + else: + ids = request.user.deploy_perms['apps'] + apps = App.objects.filter(id__in=ids) + + if form.id: + app = apps.filter(pk=form.id).first() + return json_response(app) + return json_response(apps) + return json_response(error=error) @auth('deploy.app.add|deploy.app.edit|config.app.add|config.app.edit') def post(self, request): @@ -30,12 +39,15 @@ class AppView(View): Argument('desc', required=False) ).parse(request.body) if error is None: - if not re.fullmatch(r'[-\w]+', form.key, re.ASCII): - return json_response(error='标识符必须为字母、数字、-和下划线的组合') + if not re.fullmatch(r'\w+', form.key, re.ASCII): + return json_response(error='标识符必须为字母、数字和下划线的组合') app = App.objects.filter(key=form.key).first() if app and app.id != form.id: - return json_response(error=f'唯一标识符 {form.key} 已存在,请更改后重试') + return json_response(error='该识符已存在,请更改后重试') + service = Service.objects.filter(key=form.key).first() + if service: + return json_response(error=f'该标识符已被服务 {service.name} 使用,请更改后重试') if form.id: App.objects.filter(pk=form.id).update(**form) else: diff --git a/spug_api/apps/config/views.py b/spug_api/apps/config/views.py index f2e5a72d8246767f58f6c7eca11a5a10176a3e6d..1b028f7d38b9d59231fb460054e292c84120dbcd 100644 --- a/spug_api/apps/config/views.py +++ b/spug_api/apps/config/views.py @@ -28,8 +28,8 @@ class EnvironmentView(View): Argument('desc', required=False) ).parse(request.body) if error is None: - if not re.fullmatch(r'[-\w]+', form.key, re.ASCII): - return json_response(error='标识符必须为字母、数字、-和下划线的组合') + if not re.fullmatch(r'\w+', form.key, re.ASCII): + return json_response(error='标识符必须为字母、数字和下划线的组合') env = Environment.objects.filter(key=form.key).first() if env and env.id != form.id: @@ -83,8 +83,16 @@ class EnvironmentView(View): class ServiceView(View): @auth('config.src.view') def get(self, request): - services = Service.objects.all() - return json_response(services) + form, error = JsonParser( + Argument('id', type=int, required=False) + ).parse(request.GET) + if error is None: + if form.id: + service = Service.objects.get(pk=form.id) + return json_response(service) + services = Service.objects.all() + return json_response(services) + return json_response(error=error) @auth('config.src.add|config.src.edit') def post(self, request): @@ -95,12 +103,15 @@ class ServiceView(View): Argument('desc', required=False) ).parse(request.body) if error is None: - if not re.fullmatch(r'[-\w]+', form.key, re.ASCII): - return json_response(error='标识符必须为字母、数字、-和下划线的组合') + if not re.fullmatch(r'\w+', form.key, re.ASCII): + return json_response(error='标识符必须为字母、数字和下划线的组合') service = Service.objects.filter(key=form.key).first() if service and service.id != form.id: - return json_response(error=f'唯一标识符 {form.key} 已存在,请更改后重试') + return json_response(error='该标识符已存在,请更改后重试') + app = App.objects.filter(key=form.key).first() + if app: + return json_response(error=f'该标识符已被应用 {app.name} 使用,请更改后重试') if form.id: Service.objects.filter(pk=form.id).update(**form) else: @@ -119,7 +130,8 @@ class ServiceView(View): if form.id in rel_services: rel_apps.append(app.name) if rel_apps: - return json_response(error=f'该服务在配置中心已被 "{", ".join(rel_apps)}" 依赖,请解除依赖关系后再尝试删除。') + return json_response( + error=f'该服务在配置中心已被 "{", ".join(rel_apps)}" 依赖,请解除依赖关系后再尝试删除。') # auto delete configs Config.objects.filter(type='src', o_id=form.id).delete() ConfigHistory.objects.filter(type='src', o_id=form.id).delete() diff --git a/spug_api/apps/host/views.py b/spug_api/apps/host/views.py index c8d9472efdd458dcf8116ae0baa0f41a1381219b..db5036e98b4008f8734bcceddd6724a95f582163 100644 --- a/spug_api/apps/host/views.py +++ b/spug_api/apps/host/views.py @@ -9,6 +9,7 @@ from apps.setting.utils import AppSetting from apps.account.utils import get_host_perms from apps.host.models import Host, Group from apps.host.utils import batch_sync_host, _sync_host_extend +from apps.exec.models import ExecTemplate from apps.app.models import Deploy from apps.schedule.models import Task from apps.monitor.models import Detection @@ -117,6 +118,9 @@ class HostView(View): detection = Detection.objects.filter(type__in=('3', '4'), targets__regex=regex).first() if detection: return json_response(error=f'监控中心的任务【{detection.name}】关联了该主机,请解除关联后再尝试删除该主机') + tpl = ExecTemplate.objects.filter(host_ids__regex=regex).first() + if tpl: + return json_response(error=f'执行模板【{tpl.name}】关联了该主机,请解除关联后再尝试删除该主机') Host.objects.filter(id__in=host_ids).delete() return json_response(error=error) @@ -208,7 +212,7 @@ def _do_host_verify(form): with SSH(form.hostname, form.port, form.username, password=password) as ssh: ssh.add_public_key(public_key) except BadAuthenticationType: - raise Exception('该主机不支持密钥认证,请参考官方文档,错误代码:E01') + raise Exception('该主机不支持密码认证,请参考官方文档,错误代码:E00') except AuthenticationException: raise Exception('密码连接认证失败,请检查密码是否正确') except socket.timeout: diff --git a/spug_api/apps/repository/views.py b/spug_api/apps/repository/views.py index 2b20186352d326cc76b1e07e288a401d9dd88983..c87fb4ab5b3ac5e5b586c519301ca96b537e1285 100644 --- a/spug_api/apps/repository/views.py +++ b/spug_api/apps/repository/views.py @@ -17,8 +17,9 @@ import json class RepositoryView(View): @auth('deploy.repository.view|deploy.request.add|deploy.request.edit') def get(self, request): + apps = request.user.deploy_perms['apps'] deploy_id = request.GET.get('deploy_id') - data = Repository.objects.annotate( + data = Repository.objects.filter(app_id__in=apps).annotate( app_name=F('app__name'), env_name=F('env__name'), created_by_user=F('created_by__nickname')) diff --git a/spug_api/libs/middleware.py b/spug_api/libs/middleware.py index aa412b2b7eae469b60ff3276a519689b2c2e1925..1a3a61482da99910447e04e5c083e54a7c8a083a 100644 --- a/spug_api/libs/middleware.py +++ b/spug_api/libs/middleware.py @@ -37,7 +37,7 @@ class AuthenticationMiddleware(MiddlewareMixin): if user and user.token_expired >= time.time() and user.is_active: if x_real_ip == user.last_ip or AppSetting.get_default('bind_ip') is False: request.user = user - user.token_expired = time.time() + 8 * 60 * 60 + user.token_expired = time.time() + settings.TOKEN_TTL user.save() return None response = json_response(error="验证失败,请重新登录") diff --git a/spug_api/requirements.txt b/spug_api/requirements.txt index 36d178011298718c3bb846c7863b14d24130f15f..ea8a608cb741db4f94ea9d49f2d2ce05bd393a65 100644 --- a/spug_api/requirements.txt +++ b/spug_api/requirements.txt @@ -6,7 +6,7 @@ channels_redis==2.4.1 paramiko==2.11.0 django-redis==4.10.0 requests==2.22.0 -GitPython==3.0.8 +GitPython==3.1.30 python-ldap==3.4.0 openpyxl==3.0.3 user_agents==2.2.0 \ No newline at end of file diff --git a/spug_api/spug/settings.py b/spug_api/spug/settings.py index 89cd2c46c3e64e97980ce5b8307e4d677ffcdecb..225a5da5dceede71e15b1d201ad5014de9889ed0 100644 --- a/spug_api/spug/settings.py +++ b/spug_api/spug/settings.py @@ -133,7 +133,7 @@ AUTHENTICATION_EXCLUDES = ( re.compile('/apis/.*'), ) -SPUG_VERSION = 'v3.2.4' +SPUG_VERSION = 'v3.2.7' # override default config try: diff --git a/spug_web/src/components/AppSelector.js b/spug_web/src/components/AppSelector.js index 6777afa55bbe5b46a45bedac9fabaeaf7d26bb08..add15390391c598234ab120969cce3bf7c62dd5d 100644 --- a/spug_web/src/components/AppSelector.js +++ b/spug_web/src/components/AppSelector.js @@ -56,9 +56,8 @@ export default observer(function AppSelector(props) { mode="inline" selectedKeys={[String(env_id)]} style={{border: 'none'}} - onSelect={({selectedKeys}) => setEnvId(selectedKeys[0])}> - {envStore.records.map(item => {item.name})} - + items={envStore.records.map(x => ({key: x.id, label: x.name, title: x.name}))} + onSelect={({selectedKeys}) => setEnvId(selectedKeys[0])}/> diff --git a/spug_web/src/layout/Header.js b/spug_web/src/layout/Header.js index 628cc6dfd1a3e78155e5454db4821c8e66c61c94..efd3365fd60c57b5a9dde744e0458c70e21852f4 100644 --- a/spug_web/src/layout/Header.js +++ b/spug_web/src/layout/Header.js @@ -6,7 +6,8 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { Layout, Dropdown, Menu, Avatar } from 'antd'; -import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LogoutOutlined } from '@ant-design/icons'; +import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LogoutOutlined, CodeOutlined } from '@ant-design/icons'; +import { AuthDiv } from 'components'; import Notification from './Notification'; import styles from './layout.module.less'; import http from '../libs/http'; @@ -20,6 +21,10 @@ export default function (props) { http.get('/api/account/logout/') } + function openTerminal() { + window.open('/ssh') + } + const UserMenu = ( @@ -42,7 +47,10 @@ export default function (props) { -
+ + + +
diff --git a/spug_web/src/layout/Notification.js b/spug_web/src/layout/Notification.js index 24395be069bac27b1808dc7fa35b52579da5318a..329544c7c8c0a3cc8d26af49ef8642d3786ef40f 100644 --- a/spug_web/src/layout/Notification.js +++ b/spug_web/src/layout/Notification.js @@ -106,7 +106,7 @@ export default function () { const count = notifies.length - reads.length; return ( -
+
@@ -133,11 +133,11 @@ export default function () {
)}> - +
0 ? count : 0}> - +
) diff --git a/spug_web/src/layout/Sider.js b/spug_web/src/layout/Sider.js index 238bbfc1f92114c684f19c759e107269887622e0..3b697e30745327010c1f0f300e24537a5f94aa10 100644 --- a/spug_web/src/layout/Sider.js +++ b/spug_web/src/layout/Sider.js @@ -1,13 +1,13 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Layout, Menu } from 'antd'; import { hasPermission, history } from 'libs'; import styles from './layout.module.less'; -import menus from '../routes'; +import routes from '../routes'; import logo from './logo-spug-white.png'; let selectedKey = window.location.pathname; const OpenKeysMap = {}; -for (let item of menus) { +for (let item of routes) { if (item.child) { for (let sub of item.child) { if (sub.title) OpenKeysMap[sub.path] = item.title @@ -19,28 +19,30 @@ for (let item of menus) { export default function Sider(props) { const [openKeys, setOpenKeys] = useState([]); + const [menus, setMenus] = useState([]); - function makeMenu(menu) { - if (menu.auth && !hasPermission(menu.auth)) return null; - if (!menu.title) return null; - return menu.child ? _makeSubMenu(menu) : _makeItem(menu) - } - - function _makeSubMenu(menu) { - return ( - {menu.icon}{menu.title}
}> - {menu.child.map(menu => makeMenu(menu))} - - ) - } + useEffect(() => { + const tmp = [] + for (let item of routes) { + const menu = handleRoute(item) + tmp.push(menu) + } + setMenus(tmp) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - function _makeItem(menu) { - return ( - - {menu.icon} - {menu.title} - - ) + function handleRoute(item) { + if (item.auth && !hasPermission(item.auth)) return + if (!item.title) return; + const menu = {label: item.title, key: item.path, icon: item.icon} + if (item.child) { + menu.children = [] + for (let sub of item.child) { + const subMenu = handleRoute(sub) + menu.children.push(subMenu) + } + } + return menu } const tmp = window.location.pathname; @@ -60,13 +62,12 @@ export default function Sider(props) { history.push(menu.key)}> - {menus.map(menu => makeMenu(menu))} - + onSelect={menu => history.push(menu.key)}/> ) diff --git a/spug_web/src/layout/avatar.png b/spug_web/src/layout/avatar.png index 64669bd69aba9b58a9f166123ad5ce166ce610bc..d8b1c8a606f7aa340a6634a2d3df680d8051d127 100644 Binary files a/spug_web/src/layout/avatar.png and b/spug_web/src/layout/avatar.png differ diff --git a/spug_web/src/layout/layout.module.less b/spug_web/src/layout/layout.module.less index e3799f49988559264344c64459284a9b3f6b9194..1ee56747257ffdd2c102a21882520aa1419bb854 100644 --- a/spug_web/src/layout/layout.module.less +++ b/spug_web/src/layout/layout.module.less @@ -12,7 +12,20 @@ flex: 1; } - .right { + .terminal { + padding: 0 12px; + cursor: pointer; + line-height: 48px; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background: rgba(0, 0, 0, 0.025); + } + } + + .user { .action { cursor: pointer; padding: 0 12px; @@ -27,12 +40,9 @@ } .trigger { - font-size: 20px; - line-height: 48px; cursor: pointer; transition: all 0.3s, padding 0s; - padding: 0 24px; - float: left; + padding: 0 12px; } .trigger:hover { diff --git a/spug_web/src/libs/index.js b/spug_web/src/libs/index.js index f335b0657cba40d2551fcbd314343598d94f1140..4d520cfcf288295295ba1fac60afe613c213c916 100644 --- a/spug_web/src/libs/index.js +++ b/spug_web/src/libs/index.js @@ -10,4 +10,4 @@ export * from './functools'; export * from './router'; export const http = _http; export const history = _history; -export const VERSION = 'v3.2.4'; +export const VERSION = 'v3.2.7'; diff --git a/spug_web/src/pages/alarm/group/Form.js b/spug_web/src/pages/alarm/group/Form.js index 2d7f7bc0840db65502f0437d4c8dcc8cc01fb917..8604c65111d4c3016b81b56bed547268349dd861 100644 --- a/spug_web/src/pages/alarm/group/Form.js +++ b/spug_web/src/pages/alarm/group/Form.js @@ -40,7 +40,7 @@ export default observer(function () { - + + extra="可以由字母、数字和下划线组成。"> diff --git a/spug_web/src/pages/config/app/Rel.js b/spug_web/src/pages/config/app/Rel.js index 1507efee7b1ec8b6eeef6a7048d4781d77251853..93ce49116da427d006a37c43dc2ea5a914d0e5a7 100644 --- a/spug_web/src/pages/config/app/Rel.js +++ b/spug_web/src/pages/config/app/Rel.js @@ -5,7 +5,7 @@ */ import React from 'react'; import { observer } from 'mobx-react'; -import { Modal, Form, Transfer, message, Tabs, Alert } from 'antd'; +import { Modal, Form, Transfer, message, Tabs } from 'antd'; import { http, hasPermission } from 'libs'; import serviceStore from '../service/store'; import store from './store'; @@ -51,26 +51,16 @@ class Rel extends React.Component { return ( store.relVisible = false} confirmLoading={this.state.loading} footer={hasPermission('config.app.edit_config') ? undefined : null} onOk={this.handleSubmit}> - 设置依赖的应用仅会获取到其公共配置,私有配置并不会被其他应用所获取。

, -

服务不存在公共和私有配置的概念,所以会获取到依赖服务的所有配置信息。

- ]}/> - + - + + extra="可以由字母、数字和下划线组成。"> diff --git a/spug_web/src/pages/config/service/Form.js b/spug_web/src/pages/config/service/Form.js index 383e08fa317d9b3a99545a2f9db3c5c07d102837..d33fcd76863391af9eec4b2df5f5e4aa1fd03fb2 100644 --- a/spug_web/src/pages/config/service/Form.js +++ b/spug_web/src/pages/config/service/Form.js @@ -34,16 +34,12 @@ export default observer(function () { confirmLoading={loading} onOk={handleSubmit}>
- - + + - - + + diff --git a/spug_web/src/pages/config/service/index.js b/spug_web/src/pages/config/service/index.js index 5481e792d956889e3d76d02999f490bb636c7efe..0974fd924bc1ca1df163f3c3f26df686cdbd848c 100644 --- a/spug_web/src/pages/config/service/index.js +++ b/spug_web/src/pages/config/service/index.js @@ -17,7 +17,7 @@ export default observer(function () { 首页 配置中心 - 应用配置 + 服务配置 diff --git a/spug_web/src/pages/config/setting/DiffConfig.js b/spug_web/src/pages/config/setting/DiffConfig.js index ff79f3f8f2fb4317e987c2f7316288b6c3a8e8d2..0001118882eea0e16dbf6385b59dd18dbb07ecd9 100644 --- a/spug_web/src/pages/config/setting/DiffConfig.js +++ b/spug_web/src/pages/config/setting/DiffConfig.js @@ -76,9 +76,8 @@ class Record extends React.Component { onClick={() => this.handleEnvCheck(item)} style={{cursor: 'pointer', borderTop: index ? '1px solid #e8e8e8' : ''}}> x.id).includes(item.id)}/> - {item.key} - {item.name} - {item.desc} + {item.key} + {item.name} ))} diff --git a/spug_web/src/pages/config/setting/Form.js b/spug_web/src/pages/config/setting/Form.js index d262848c7916e1b5ed35e16ac790a318999d1872..2646c7a8651ff40046a155334d6738709dbb0361 100644 --- a/spug_web/src/pages/config/setting/Form.js +++ b/spug_web/src/pages/config/setting/Form.js @@ -77,23 +77,25 @@ export default observer(function () { name="is_public" valuePropName="checked" initialValue={store.record.is_public === undefined || store.record.is_public} - tooltip={什么是公共/私有配置?}> + tooltip={什么是公共/私有配置?}> )} - - {envStore.records.map((item, index) => ( - handleEnvCheck(item.id)} - style={{cursor: 'pointer', borderTop: index ? '1px solid #e8e8e8' : ''}}> - - {item.key} - {item.name} - {item.desc} - - ))} - + {isModify ? null : ( + + {envStore.records.map((item, index) => ( + handleEnvCheck(item.id)} + style={{cursor: 'pointer', borderTop: index ? '1px solid #e8e8e8' : ''}}> + + {item.key} + {item.name} + + ))} + + )}
) diff --git a/spug_web/src/pages/config/setting/index.js b/spug_web/src/pages/config/setting/index.js index 64a3097f5929a6c89e0491832da4eb2dd506c1cf..82b392de8df6d92f8aa82cc4a0a045ee01de087d 100644 --- a/spug_web/src/pages/config/setting/index.js +++ b/spug_web/src/pages/config/setting/index.js @@ -5,7 +5,7 @@ */ import React from 'react'; import { observer } from 'mobx-react'; -import { Menu, Input, Button, PageHeader, Modal, Space, Radio, Form } from 'antd'; +import { Menu, Input, Button, PageHeader, Modal, Space, Radio, Form, Alert } from 'antd'; import { DiffOutlined, HistoryOutlined, @@ -24,8 +24,6 @@ import TextView from './TextView'; import JSONView from './JSONView'; import Record from './Record'; import store from './store'; -import appStore from '../app/store'; -import srcStore from '../service/store'; @observer class Index extends React.Component { @@ -40,22 +38,23 @@ class Index extends React.Component { componentDidMount() { const {type, id} = this.props.match.params; - store.type = type; - store.id = id; - if (envStore.records.length === 0) { - envStore.fetchRecords().then(() => { + store.initial(type, id) + .then(() => { if (envStore.records.length === 0) { - Modal.error({ - title: '无可用环境', - content:
配置依赖应用的运行环境,请在 环境管理 中创建环境。
+ envStore.fetchRecords().then(() => { + if (envStore.records.length === 0) { + Modal.error({ + title: '无可用环境', + content:
配置依赖应用的运行环境,请在 环境管理 中创建环境。
+ }) + } else { + this.updateEnv() + } }) } else { this.updateEnv() } }) - } else { - this.updateEnv() - } } updateEnv = (env) => { @@ -73,13 +72,12 @@ class Index extends React.Component { render() { const {view} = this.state; const isApp = store.type === 'app'; - const record = isApp ? appStore.record : srcStore.record; return ( - + }> 配置中心 history.goBack()}>{isApp ? '应用配置' : '服务配置'} - {record.name} + {store.obj.name}
@@ -108,7 +106,8 @@ class Index extends React.Component { - store.f_name = e.target.value} placeholder="请输入"/> + store.f_name = e.target.value} + placeholder="请输入"/> { + this.type = type + this.id = id + const url = type === 'app' ? '/api/app/' : '/api/config/service/' + this.isFetching = true + return http.get(url, {params: {id}}) + .then(res => this.obj = res) + } + fetchRecords = () => { const params = {type: this.type, id: this.id, env_id: this.env.id}; this.isFetching = true; diff --git a/spug_web/src/pages/deploy/app/Ext1Setup1.js b/spug_web/src/pages/deploy/app/Ext1Setup1.js index a88f9f789a1b3f1e9ecb5507be3197f18b3a3fc9..3f8da294b8e486088ff49065f31406ebdc9c4b02 100644 --- a/spug_web/src/pages/deploy/app/Ext1Setup1.js +++ b/spug_web/src/pages/deploy/app/Ext1Setup1.js @@ -9,7 +9,7 @@ import { Link } from 'react-router-dom'; import { Switch, Form, Input, Select, Button, Radio } from 'antd'; import Repo from './Repo'; import envStore from 'pages/config/environment/store'; -import Selector from 'pages/host/Selector'; +import HostSelector from 'pages/host/Selector'; import store from './store'; export default observer(function Ext1Setup1() { @@ -62,8 +62,7 @@ export default observer(function Ext1Setup1() { - {info.host_ids.length > 0 && 已选择 {info.host_ids.length} 台} - + info.host_ids = ids}/> setVisible(true)}>私有仓库?}> info['git_repo'] = e.target.value} @@ -116,11 +115,6 @@ export default observer(function Ext1Setup1() { disabled={!(info.env_id && info.git_repo && info.host_ids.length)} onClick={() => store.page += 1}>下一步 - store.selectorVisible = false} - onOk={(_, ids) => info.host_ids = ids}/> {visible && info['git_repo'] = v} onCancel={() => setVisible(false)}/>} ) diff --git a/spug_web/src/pages/deploy/app/Ext2Setup1.js b/spug_web/src/pages/deploy/app/Ext2Setup1.js index 90dda9e8db19aa016e96d12071820c6f3944322a..22485b6ae9e0f906f1fafeadfd3ebe3b2ae9d4a0 100644 --- a/spug_web/src/pages/deploy/app/Ext2Setup1.js +++ b/spug_web/src/pages/deploy/app/Ext2Setup1.js @@ -8,12 +8,11 @@ import { observer } from 'mobx-react'; import { Link } from 'react-router-dom'; import { Form, Switch, Select, Button, Input, Radio } from 'antd'; import envStore from 'pages/config/environment/store'; -import Selector from 'pages/host/Selector'; +import HostSelector from 'pages/host/Selector'; import store from './store'; export default observer(function Ext2Setup1() { const [envs, setEnvs] = useState([]); - const [selectorVisible, setSelectorVisible] = useState(false); function updateEnvs() { const ids = store.currentRecord['deploys'].map(x => x.env_id); @@ -61,8 +60,7 @@ export default observer(function Ext2Setup1() { - {info.host_ids.length > 0 && 已选择 {info.host_ids.length} 台} - + info.host_ids = ids}/> store.page += 1}>下一步 - setSelectorVisible(false)} - onOk={(_, ids) => info.host_ids = ids}/> ) }) diff --git a/spug_web/src/pages/deploy/app/Form.js b/spug_web/src/pages/deploy/app/Form.js index 1e9adde552088582531ab0149f149fe1848fb614..b1be2a9170e2c49471457cb12a0c162b46136f7e 100644 --- a/spug_web/src/pages/deploy/app/Form.js +++ b/spug_web/src/pages/deploy/app/Form.js @@ -42,7 +42,7 @@ export default observer(function () { name="key" label="唯一标识符" tooltip="给应用设置的唯一标识符,会用于配置中心的配置生成。" - extra="可以由字母、数字、-和下划线组成。"> + extra="可以由字母、数字和下划线组成。"> diff --git a/spug_web/src/pages/deploy/app/store.js b/spug_web/src/pages/deploy/app/store.js index 80d7d378dc7e12fbbbadc2f38e3041fa40170725..8963f55a5cccddb1b955adc7e603920a84152f94 100644 --- a/spug_web/src/pages/deploy/app/store.js +++ b/spug_web/src/pages/deploy/app/store.js @@ -20,7 +20,6 @@ class Store { @observable ext1Visible = false; @observable ext2Visible = false; @observable autoVisible = false; - @observable selectorVisible = false; @observable f_name; @observable f_desc; diff --git a/spug_web/src/pages/deploy/repository/index.js b/spug_web/src/pages/deploy/repository/index.js index 7a601e05efe34b2465960ac349505eda629f0996..9d2deec803b247b21dae1d213273ef34361a8b4c 100644 --- a/spug_web/src/pages/deploy/repository/index.js +++ b/spug_web/src/pages/deploy/repository/index.js @@ -57,11 +57,14 @@ export default observer(function () { - item.extend === '1'} onCancel={() => store.addVisible = false} onSelect={store.confirmAdd}/> + )} + {store.formVisible && } {store.logVisible && } diff --git a/spug_web/src/pages/exec/task/index.js b/spug_web/src/pages/exec/task/index.js index a91f9ebc8ebb533daf50112f6110f2c7dbc47663..c48798a2cb852f4d104d9406c8e0c64aab5bb403 100644 --- a/spug_web/src/pages/exec/task/index.js +++ b/spug_web/src/pages/exec/task/index.js @@ -6,9 +6,9 @@ import React, { useState, useEffect } from 'react'; import { observer } from 'mobx-react'; import { PlusOutlined, ThunderboltOutlined, BulbOutlined, QuestionCircleOutlined } from '@ant-design/icons'; -import { Form, Button, Alert, Radio, Tooltip } from 'antd'; +import { Form, Button, Radio, Tooltip } from 'antd'; import { ACEditor, AuthDiv, Breadcrumb } from 'components'; -import Selector from 'pages/host/Selector'; +import HostSelector from 'pages/host/Selector'; import TemplateSelector from './TemplateSelector'; import Parameter from './Parameter'; import Output from './Output'; @@ -87,17 +87,7 @@ function TaskIndex() { } - onClick={() => store.showHost = true}/> - ) : ( - - )} + store.host_ids = ids}/> @@ -144,12 +134,6 @@ function TaskIndex() { {store.showTemplate && } {store.showConsole && } {visible && setVisible(false)} onOk={v => handleSubmit(v)}/>} - store.showHost = false} - onOk={(_, ids) => store.host_ids = ids}/> - ) } diff --git a/spug_web/src/pages/exec/task/index.module.less b/spug_web/src/pages/exec/task/index.module.less index e6add70640436a9681e33a8b5d541d6221bba860..2d2c04c0bad2833063b7cb7e3f34ef832773cebe 100644 --- a/spug_web/src/pages/exec/task/index.module.less +++ b/spug_web/src/pages/exec/task/index.module.less @@ -8,12 +8,7 @@ .left { padding: 24px; width: 60%; - - .area { - cursor: pointer; - width: 200px; - height: 32px; - } + border-right: 1px solid #dfdfdf; .tips { position: absolute; diff --git a/spug_web/src/pages/exec/task/store.js b/spug_web/src/pages/exec/task/store.js index 0d0a8b88d430b630b4d6cc7a60af77b5a9df17b6..d8d8025a6406b38b7691b193675393b966a12021 100644 --- a/spug_web/src/pages/exec/task/store.js +++ b/spug_web/src/pages/exec/task/store.js @@ -11,7 +11,6 @@ class Store { @observable tag = ''; @observable host_ids = []; @observable token = null; - @observable showHost = false; @observable showConsole = false; @observable showTemplate = false; @@ -50,10 +49,6 @@ class Store { } } - switchHost = () => { - this.showHost = !this.showHost; - }; - switchTemplate = () => { this.showTemplate = !this.showTemplate }; diff --git a/spug_web/src/pages/exec/template/Form.js b/spug_web/src/pages/exec/template/Form.js index 193fc0e2dcd94616e4a044331247283f5184de09..aa40bfa523c67e340184e5bda8b3f5f776be3c80 100644 --- a/spug_web/src/pages/exec/template/Form.js +++ b/spug_web/src/pages/exec/template/Form.js @@ -8,7 +8,7 @@ import { observer } from 'mobx-react'; import { ExclamationCircleOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; import { Modal, Form, Input, Select, Button, Radio, Table, Tooltip, message } from 'antd'; import { ACEditor } from 'components'; -import Selector from 'pages/host/Selector'; +import HostSelector from 'pages/host/Selector'; import Parameter from './Parameter'; import { http, cleanCommand } from 'libs'; import lds from 'lodash'; @@ -20,7 +20,6 @@ export default observer(function () { const [body, setBody] = useState(S.record.body); const [parameter, setParameter] = useState(); const [parameters, setParameters] = useState([]); - const [visible, setVisible] = useState(false); useEffect(() => { setParameters(S.record.parameters) @@ -136,18 +135,12 @@ export default observer(function () { - {info.host_ids.length > 0 && 已选择 {info.host_ids.length} 台} - + info.host_ids = ids}/> - setVisible(false)} - onOk={(_, ids) => info.host_ids = ids}/> {parameter ? ( { diff --git a/spug_web/src/pages/exec/transfer/index.js b/spug_web/src/pages/exec/transfer/index.js index 853a1670aa5bdcbe5d8089bef80fadc88d353229..66663de32e02a32f5a61f863fd3d9d2aad5ce269 100644 --- a/spug_web/src/pages/exec/transfer/index.js +++ b/spug_web/src/pages/exec/transfer/index.js @@ -6,16 +6,15 @@ import React, { useState, useEffect } from 'react'; import { observer } from 'mobx-react'; import { - PlusOutlined, ThunderboltOutlined, QuestionCircleOutlined, UploadOutlined, CloudServerOutlined, BulbOutlined, } from '@ant-design/icons'; -import { Form, Button, Alert, Tooltip, Space, Card, Table, Input, Upload, message } from 'antd'; +import { Form, Button, Tooltip, Space, Card, Table, Input, Upload, message } from 'antd'; import { AuthDiv, Breadcrumb } from 'components'; -import Selector from 'pages/host/Selector'; +import HostSelector from 'pages/host/Selector'; import Output from './Output'; import { http, uniqueId } from 'libs'; import moment from 'moment'; @@ -27,7 +26,6 @@ function TransferIndex() { const [files, setFiles] = useState([]) const [dir, setDir] = useState('') const [hosts, setHosts] = useState([]) - const [sProps, setSProps] = useState({visible: false}) const [percent, setPercent] = useState() const [token, setToken] = useState() const [histories, setHistories] = useState([]) @@ -80,23 +78,14 @@ function TransferIndex() { }) } - function handleAddHostFile() { - setSProps({ - visible: true, - onlyOne: true, - selectedRowKeys: [], - onCancel: () => setSProps({visible: false}), - onOk: (_, __, row) => setFiles([{id: uniqueId(), type: 'host', name: row.name, path: '', host_id: row.id}]), - }) - } - - function handleAddHost() { - setSProps({ - visible: true, - selectedRowKeys: hosts.map(x => x.id), - onCancel: () => setSProps({visible: false}), - onOk: (_, __, rows) => setHosts(rows), - }) + function makeFile(row) { + setFiles([{ + id: uniqueId(), + type: 'host', + name: row.name, + path: '', + host_id: row.id + }]) } function handleUpload(_, fileList) { @@ -113,6 +102,13 @@ function TransferIndex() { setFiles([...files]) } + function handleCloseOutput() { + setToken() + if (!store.counter['0'] && !store.counter['2']) { + setFiles([]) + } + } + return ( 首页 @@ -121,11 +117,15 @@ function TransferIndex() {
- - {token ? setToken()}/> : null} + {token ? : null}
) } diff --git a/spug_web/src/pages/exec/transfer/index.module.less b/spug_web/src/pages/exec/transfer/index.module.less index 0cec33ce121d26b04c6cd8a285eb1805652eb65e..2d6d744b0022796de7d0c07256aac605d4d47329 100644 --- a/spug_web/src/pages/exec/transfer/index.module.less +++ b/spug_web/src/pages/exec/transfer/index.module.less @@ -8,6 +8,12 @@ .left { padding: 24px; width: 60%; + border-right: 1px solid #dfdfdf; + + .table { + max-height: calc(100vh - 600px); + overflow: auto; + } .area { cursor: pointer; diff --git a/spug_web/src/pages/host/Selector.js b/spug_web/src/pages/host/Selector.js index 4bd76a071ab8fffda67f6e04122de709c03a9682..54b750d54d5df190c59b4984b4bf0f23f46caf23 100644 --- a/spug_web/src/pages/host/Selector.js +++ b/spug_web/src/pages/host/Selector.js @@ -5,15 +5,15 @@ */ import React, { useEffect, useState } from 'react'; import { observer } from 'mobx-react'; -import { Modal, Row, Col, Tree, Table, Button, Space, Input } from 'antd'; -import { FolderOpenOutlined, FolderOutlined } from '@ant-design/icons'; +import { Modal, Row, Col, Tree, Table, Button, Space, Input, Alert } from 'antd'; +import { FolderOpenOutlined, FolderOutlined, PlusOutlined } from '@ant-design/icons'; import IPAddress from './IPAddress'; import hStore from './store'; import store from './store2'; -import styles from './index.module.less'; +import styles from './selector.module.less'; - -export default observer(function (props) { +function HostSelector(props) { + const [visible, setVisible] = useState(false) const [isReady, setIsReady] = useState(false) const [loading, setLoading] = useState(false); const [selectedRowKeys, setSelectedRowKeys] = useState([]); @@ -24,7 +24,7 @@ export default observer(function (props) { hStore.initial().then(() => { store.rawRecords = hStore.rawRecords; store.rawTreeData = hStore.rawTreeData; - store.group = store.treeData[0] + store.group = store.treeData[0] || {} }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -42,8 +42,8 @@ export default observer(function (props) { }, [store.treeData]) useEffect(() => { - setSelectedRowKeys(props.selectedRowKeys || []) - }, [props.selectedRowKeys]) + setSelectedRowKeys([...props.value]) + }, [props.value]) useEffect(() => { if (props.onlySelf) { @@ -62,21 +62,17 @@ export default observer(function (props) { } function handleSubmit() { - if (props.onOk) { - setLoading(true); - let res - const selectedRows = store.rawRecords.filter(x => selectedRowKeys.includes(x.id)) - if (props.onlyOne) { - res = props.onOk(store.group, selectedRowKeys[0], selectedRows[0]) - } else { - res = props.onOk(store.group, selectedRowKeys, selectedRows); - } - if (res && res.then) { - res.then(props.onCancel, () => setLoading(false)) - } else { - props.onCancel(); - setLoading(false) - } + if (props.mode === 'ids') { + props.onChange(props.onlyOne ? selectedRowKeys[0] : selectedRowKeys) + handleClose() + } else if (props.mode === 'rows') { + const value = store.rawRecords.filter(x => selectedRowKeys.includes(x.id)) + props.onChange(props.onlyOne ? value[0] : value) + handleClose() + } else if (props.mode === 'group') { + setLoading(true) + props.onChange(store.group, selectedRowKeys) + .then(handleClose, () => setLoading(false)) } } @@ -109,67 +105,113 @@ export default observer(function (props) { ) } + function handleClose() { + setSelectedRowKeys([]) + setLoading(false) + setVisible(false) + if (props.onCancel) { + props.onCancel() + } + } + return ( - - - -
分组列表
- store.group = node} - /> - - -
- store.f_word = e.target.value}/> - -
- { - return { - onClick: () => handleClickRow(record) - } - }} - rowSelection={{ - selectedRowKeys, - hideSelectAll: props.onlyOne, - onSelect: handleClickRow, - onSelectAll: handleSelectAll - }}> - - ( - - - +
+ {props.mode !== 'group' && ( + props.children ? ( +
setVisible(true)}>{props.children}
+ ) : ( + props.type === 'button' ? ( + props.value.length > 0 ? ( + 已选择 {props.value.length} 台主机
} + onClick={() => setVisible(true)}/> + ) : ( + + )) : ( +
+ {props.value.length > 0 && 已选择 {props.value.length} 台} + +
+ ) + ) + )} + + + +
+
分组列表
+ store.group = node} + /> + + +
+ store.f_word = e.target.value}/> + - )}/> - -
- -
-
+ + { + return { + onClick: () => handleClickRow(record) + } + }} + rowSelection={{ + selectedRowKeys, + hideSelectAll: props.onlyOne, + onSelect: handleClickRow, + onSelectAll: handleSelectAll + }}> + + ( + + + + + )}/> + +
+ + + + ) -}) \ No newline at end of file +} + +HostSelector.defaultProps = { + value: [], + type: 'text', + mode: 'ids', + onlyOne: false, + nullable: false, + onChange: () => null +} + +export default observer(HostSelector) \ No newline at end of file diff --git a/spug_web/src/pages/host/Table.js b/spug_web/src/pages/host/Table.js index 6bbb29f5831d6cba2d9a2537b1e19707a0ec3b38..6c1b64f8ca46b9c440c3d720a32515fdae5937b7 100644 --- a/spug_web/src/pages/host/Table.js +++ b/spug_web/src/pages/host/Table.js @@ -12,6 +12,7 @@ import IPAddress from './IPAddress'; import { http, hasPermission } from 'libs'; import store from './store'; import icons from './icons'; +import moment from 'moment'; function ComTable() { function handleDelete(text) { @@ -38,6 +39,21 @@ function ComTable() { } } + function ExpTime(props) { + if (!props.value) return null + let value = moment(props.value) + const days = value.diff(moment(), 'days') + if (days > 30) { + return 剩余 {days} + } else if (days > 7) { + return 剩余 {days} + } else if (days >= 0) { + return 剩余 {days} + } else { + return 过期 {Math.abs(days)} + } + } + return ( {info.cpu}核 {info.memory}GB )}/> + }/> } {store.selectorVisible && store.selectorVisible = false} - onOk={store.updateGroup} + onChange={store.updateGroup} />} ); diff --git a/spug_web/src/pages/host/index.module.less b/spug_web/src/pages/host/index.module.less index c502150fd4e1917543847ac25b540ec68ca56a1d..230f13a4693bb186db56166ae1f46722ff90482e 100644 --- a/spug_web/src/pages/host/index.module.less +++ b/spug_web/src/pages/host/index.module.less @@ -26,21 +26,6 @@ } } -.selector { - :global(.ant-modal-footer) { - border-top: none - } - - .gTitle { - height: 44px; - line-height: 44px; - padding-left: 12px; - font-weight: bold; - margin-bottom: 12px; - background: #fafafa; - } -} - .formAddress1 { display: inline-block; diff --git a/spug_web/src/pages/host/selector.module.less b/spug_web/src/pages/host/selector.module.less new file mode 100644 index 0000000000000000000000000000000000000000..b469812185e5ed7b29d0e3a4b157c0f3c97c4d9e --- /dev/null +++ b/spug_web/src/pages/host/selector.module.less @@ -0,0 +1,46 @@ +.modal { + :global(.ant-modal-footer) { + border-top: none + } + + .gTitle { + height: 44px; + line-height: 44px; + padding-left: 12px; + font-weight: bold; + margin-bottom: 12px; + background: #fafafa; + } +} + +.area { + cursor: pointer; + width: 200px; + height: 32px; +} + +.treeNode { + display: flex; + flex-direction: row; + align-items: center; + + .title { + margin-left: 8px; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + } + + .number { + width: 30px; + text-align: right; + } +} + + + + diff --git a/spug_web/src/pages/monitor/MonitorCard.js b/spug_web/src/pages/monitor/MonitorCard.js index 8ea07f918af59163dd67930ac6897696a5727f53..79885cd95016d4ee3a730f107e0e6aef800fc686 100644 --- a/spug_web/src/pages/monitor/MonitorCard.js +++ b/spug_web/src/pages/monitor/MonitorCard.js @@ -6,16 +6,16 @@ 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 { FrownOutlined, ReloadOutlined, SyncOutlined } from '@ant-design/icons'; import styles from './index.module.less'; import store from './store'; const StyleMap = { - '0': {background: '#99999933', border: '2px solid #999', color: '#999999'}, - '1': {background: '#16a98733', border: '2px solid #16a987', color: '#16a987'}, - '2': {background: '#ffba0033', border: '2px solid #ffba00', color: '#ffba00'}, - '3': {background: '#f2655d33', border: '2px solid #f2655d', color: '#f2655d'}, - '10': {background: '#99999919', border: '2px dashed #999999', color: '#999999'} + '0': {background: '#99999933', border: '1px solid #999', color: '#999999'}, + '1': {background: '#16a98733', border: '1px solid #16a987', color: '#16a987'}, + '2': {background: '#ffba0033', border: '1px solid #ffba00', color: '#ffba00'}, + '3': {background: '#f2655d33', border: '1px solid #f2655d', color: '#f2655d'}, + '10': {background: '#99999919', border: '1px dashed #999999', color: '#999999'} } const StatusMap = { @@ -27,9 +27,10 @@ const StatusMap = { } function CardItem(props) { - const {status, type, desc, name, target, latest_run_time} = props.data + const {status, type, group, desc, name, target, latest_run_time} = props.data const title = (
+
分组: {group}
类型: {type}
名称: {name}
目标: {target}
@@ -65,7 +66,7 @@ function MonitorCard() { const filteredRecords = store.ovDataSource.filter(x => !status || x.status === status) return ( -
分组:
@@ -94,21 +95,21 @@ function MonitorCard() { {Object.entries(StyleMap).map(([s, style]) => { const count = store.ovDataSource.filter(x => x.status === s).length; return count ? ( -
setStatus(s === status ? '' : s)}> - {store.ovDataSource.filter(x => x.status === s).length} -
+ +
setStatus(s === status ? '' : s)}> + {store.ovDataSource.filter(x => x.status === s).length} +
+
) : null })} -
- {autoReload ? : } -
+ +
+ {autoReload ? : } +
+
{filteredRecords.length > 0 ? ( diff --git a/spug_web/src/pages/monitor/Step1.js b/spug_web/src/pages/monitor/Step1.js index b7ce77757ce2343ed0faa58c8a318faeb7aee127..9c14ac2a0dc9d8556f682e1c3a939ec3d41a090e 100644 --- a/spug_web/src/pages/monitor/Step1.js +++ b/spug_web/src/pages/monitor/Step1.js @@ -8,7 +8,7 @@ import { observer } from 'mobx-react'; import { ExclamationCircleOutlined } from '@ant-design/icons'; import { Modal, Form, Input, Select, Button, message } from 'antd'; import TemplateSelector from '../exec/task/TemplateSelector'; -import Selector from 'pages/host/Selector'; +import HostSelector from 'pages/host/Selector'; import { LinkButton, ACEditor } from 'components'; import { http, cleanCommand } from 'libs'; import store from './store'; @@ -22,7 +22,6 @@ const helpMap = { export default observer(function () { const [loading, setLoading] = useState(false); const [showTmp, setShowTmp] = useState(false); - const [showSelector, setShowSelector] = useState(false); function handleTest() { setLoading(true) @@ -84,7 +83,7 @@ export default observer(function () { } function getStyle(t) { - return t.includes(store.record.type) ? {display: 'flex'} : {display: 'none'} + return t.includes(store.record.type) ? {} : {display: 'none'} } const {name, desc, type, targets, extra, group} = store.record; @@ -118,6 +117,7 @@ export default observer(function () { store.record.targets = v} placeholder="IP或域名,支持多个地址,每输入完成一个后按回车确认" notFoundContent={null}/>
- {store.record.targets?.length > 0 && ( - 已选择 {store.record.targets.length} 台 - )} - + store.record.targets = ids}/> Tips: 仅测试第一个监控地址 {showTmp && store.record.extra = body} onCancel={() => setShowTmp(false)}/>} - setShowSelector(false)} - onOk={(_, ids) => store.record.targets = ids}/> ) }) \ No newline at end of file diff --git a/spug_web/src/pages/monitor/index.module.less b/spug_web/src/pages/monitor/index.module.less index 9258725014a6955b5fcfae8aac510d7b306052a2..81f01d6f321a19a64425c878401416c6aadf737e 100644 --- a/spug_web/src/pages/monitor/index.module.less +++ b/spug_web/src/pages/monitor/index.module.less @@ -7,8 +7,8 @@ display: flex; justify-content: center; align-items: center; - width: 40px; - height: 40px; + width: 16px; + height: 16px; font-size: 12px; color: #fff; border-radius: 2px; @@ -17,6 +17,7 @@ .header { display: flex; justify-content: flex-end; + align-items: center; margin-bottom: 12px; margin-top: -6px; @@ -25,24 +26,19 @@ justify-content: center; align-items: center; min-width: 26px; - height: 26px; - padding: 0 1px; + height: 20px; margin-left: 12px; - border-radius: 2px; + border-radius: 10px; + padding: 0 8px; color: #fff; font-weight: bold; cursor: pointer; } - .authLoad { - display: flex; - justify-content: center; - align-items: center; - width: 26px; - height: 26px; - color: #fff; + .autoLoad { margin-left: 24px; - border-radius: 2px; + font-size: 18px; + color: #999999; } } diff --git a/spug_web/src/pages/schedule/Step2.js b/spug_web/src/pages/schedule/Step2.js index e092573cef033c6aab1946707e7e66d4531b78ec..50c8576a16d63aa05dfe91c92933178608ceb145 100644 --- a/spug_web/src/pages/schedule/Step2.js +++ b/spug_web/src/pages/schedule/Step2.js @@ -3,7 +3,7 @@ * Copyright (c) * Released under the AGPL-3.0 License. */ -import React, { useState } from 'react'; +import React from 'react'; import { observer } from 'mobx-react'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { Form, Select, Button } from 'antd'; @@ -13,11 +13,16 @@ import hostStore from 'pages/host/store'; import styles from './index.module.css'; export default observer(function () { - const [visible, setVisible] = useState(false) + function handleChange(ids) { + if (store.targets.includes('local')) { + ids.unshift('local') + } + store.targets = ids + } return ( -
+ {store.targets.map((id, index) => ( @@ -43,15 +48,10 @@ export default observer(function () { ))} - + x !== 'local')} onChange={handleChange}> + + - setVisible(false)} - onOk={(_, ids) => store.targets = ids}/>