feat: 添加数据库资产、命令拦截器、授权资产等功能,修复GitHub Actions工作流
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {Button, Card, Input, Modal, Result, Space, Spin} from "antd";
|
||||
import accountApi, {AuthType} from "@/api/account-api";
|
||||
import {startAuthentication} from "@simplewebauthn/browser";
|
||||
@@ -26,7 +26,8 @@ const MultiFactorAuthentication = ({open, handleOk, handleCancel, forceReauth =
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [otpValue, setOtpValue] = useState('');
|
||||
const [showSelector, setShowSelector] = useState(false);
|
||||
const [otpKey, setOtpKey] = useState(0); // 用于强制重新挂载 InputOTP
|
||||
const [otpKey, setOtpKey] = useState(0);
|
||||
const handledRef = useRef(false);
|
||||
|
||||
// 查询支持的认证类型
|
||||
const {data: supportedAuthTypes = []} = useQuery({
|
||||
@@ -58,6 +59,7 @@ const MultiFactorAuthentication = ({open, handleOk, handleCancel, forceReauth =
|
||||
setOtpValue('');
|
||||
setShowSelector(false);
|
||||
setAuthType('');
|
||||
handledRef.current = false;
|
||||
};
|
||||
|
||||
// 清除输入
|
||||
@@ -81,12 +83,10 @@ const MultiFactorAuthentication = ({open, handleOk, handleCancel, forceReauth =
|
||||
const validateSecurityToken = () => {
|
||||
const securityToken = sessionStorage.getItem('securityToken');
|
||||
if (securityToken) {
|
||||
// 检查 Token 是否有效,有效则直接使用
|
||||
accountApi.validateSecurityToken(securityToken).then((ok) => {
|
||||
if (ok) {
|
||||
storeSecurityToken(securityToken);
|
||||
} else {
|
||||
// 无效则清除
|
||||
sessionStorage.removeItem('securityToken');
|
||||
}
|
||||
});
|
||||
@@ -121,7 +121,7 @@ const MultiFactorAuthentication = ({open, handleOk, handleCancel, forceReauth =
|
||||
onSuccess: (token) => token && storeSecurityToken(token),
|
||||
onError: (e: any) => {
|
||||
setOtpValue('');
|
||||
setOtpKey(prev => prev + 1); // 强制重新挂载以重新获得焦点
|
||||
setOtpKey(prev => prev + 1);
|
||||
const code = typeof e?.code === 'number' ? e.code : 1;
|
||||
setError({code, message: e?.message || String(e)});
|
||||
}
|
||||
@@ -141,8 +141,13 @@ const MultiFactorAuthentication = ({open, handleOk, handleCancel, forceReauth =
|
||||
return;
|
||||
}
|
||||
|
||||
if (handledRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 没有可用的认证方式,直接通过
|
||||
if (supportedAuthTypes.length === 0) {
|
||||
handledRef.current = true;
|
||||
handleOk('');
|
||||
return;
|
||||
}
|
||||
@@ -153,7 +158,6 @@ const MultiFactorAuthentication = ({open, handleOk, handleCancel, forceReauth =
|
||||
setAuthType(supportedAuthTypes[0]);
|
||||
setShowSelector(false);
|
||||
} else {
|
||||
// 有可用的认证方式
|
||||
if (!authType || authType === 'none') {
|
||||
setAuthType('');
|
||||
setShowSelector(true);
|
||||
@@ -170,8 +174,7 @@ const MultiFactorAuthentication = ({open, handleOk, handleCancel, forceReauth =
|
||||
}, [authType, showSelector, open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && !forceReauth) {
|
||||
// 检查是否有存储的 securityToken
|
||||
if (open && !forceReauth && !handledRef.current) {
|
||||
validateSecurityToken();
|
||||
}
|
||||
}, [open, forceReauth]);
|
||||
|
||||
@@ -93,24 +93,14 @@ const AuthorisedAssetPost = () => {
|
||||
}}
|
||||
request={async () => {
|
||||
let items = await assetApi.getGroups();
|
||||
// let selected = await authorisedAssetApi.selected('assetId', userId, userGroupId, '');
|
||||
// 递归把 key 字段设置为 value,并且非叶子节点全部 disabled
|
||||
function setKeyAndDisabled(item: any) {
|
||||
item.value = item.key;
|
||||
if (!item.isLeaf) {
|
||||
// 递归处理子节点
|
||||
if (item.children) {
|
||||
item.children.forEach(setKeyAndDisabled);
|
||||
}
|
||||
function processNode(item: any) {
|
||||
item.value = item.id;
|
||||
if (item.children) {
|
||||
item.children.forEach(processNode);
|
||||
}
|
||||
// if (selected.includes(item.key)) {
|
||||
// item.disabled = true;
|
||||
// }
|
||||
}
|
||||
|
||||
// 对获取到的所有节点进行处理
|
||||
items.forEach((item: any) => {
|
||||
setKeyAndDisabled(item);
|
||||
processNode(item);
|
||||
});
|
||||
return items;
|
||||
}}
|
||||
@@ -127,28 +117,19 @@ const AuthorisedAssetPost = () => {
|
||||
}}
|
||||
request={async () => {
|
||||
let items = await assetApi.tree();
|
||||
// let selected = await authorisedAssetApi.selected('assetId', userId, userGroupId, '');
|
||||
|
||||
// 递归把 key 字段设置为 value,并且非叶子节点全部 disabled
|
||||
function setKeyAndDisabled(item: any) {
|
||||
item.value = item.key;
|
||||
function processNode(item: any) {
|
||||
item.value = item.id;
|
||||
if (!item.isLeaf) {
|
||||
item.disabled = true;
|
||||
// 递归处理子节点
|
||||
if (item.children) {
|
||||
item.children.forEach(setKeyAndDisabled);
|
||||
}
|
||||
} else {
|
||||
item.title = item.title + ' (' + item.extra?.network + ')';
|
||||
}
|
||||
// if (selected.includes(item.key)) {
|
||||
// item.disabled = true;
|
||||
// }
|
||||
if (item.children) {
|
||||
item.children.forEach(processNode);
|
||||
}
|
||||
}
|
||||
|
||||
// 对获取到的所有节点进行处理
|
||||
items.forEach((item: any) => {
|
||||
setKeyAndDisabled(item);
|
||||
processNode(item);
|
||||
});
|
||||
return items;
|
||||
}}
|
||||
|
||||
@@ -29,7 +29,18 @@ const GatewayGroupDrawer: React.FC<Props> = ({open, group, onClose}) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (open && group) {
|
||||
form.setFieldsValue(group);
|
||||
let members = group.members;
|
||||
if (typeof members === 'string') {
|
||||
try {
|
||||
members = JSON.parse(members);
|
||||
} catch {
|
||||
members = [];
|
||||
}
|
||||
}
|
||||
form.setFieldsValue({
|
||||
...group,
|
||||
members,
|
||||
});
|
||||
} else if (open) {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
@@ -41,10 +52,14 @@ const GatewayGroupDrawer: React.FC<Props> = ({open, group, onClose}) => {
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
try {
|
||||
const submitData = {
|
||||
...values,
|
||||
members: JSON.stringify(values.members || []),
|
||||
};
|
||||
if (group?.id) {
|
||||
await gatewayGroupApi.updateById(group.id, values);
|
||||
await gatewayGroupApi.updateById(group.id, submitData);
|
||||
} else {
|
||||
await gatewayGroupApi.create(values);
|
||||
await gatewayGroupApi.create(submitData);
|
||||
}
|
||||
message.success(t('general.success'));
|
||||
onClose(true);
|
||||
|
||||
@@ -43,6 +43,16 @@ const GatewayGroupPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseMembers = (members: string | undefined): any[] => {
|
||||
if (!members) return [];
|
||||
if (Array.isArray(members)) return members;
|
||||
try {
|
||||
return JSON.parse(members);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProColumns<GatewayGroup>[] = [
|
||||
{
|
||||
title: t('gateway_group.name'),
|
||||
@@ -69,8 +79,9 @@ const GatewayGroupPage: React.FC = () => {
|
||||
dataIndex: 'members',
|
||||
width: 100,
|
||||
render: (_, record) => {
|
||||
const enabledCount = record.members?.filter(m => m.enabled).length || 0;
|
||||
const totalCount = record.members?.length || 0;
|
||||
const members = parseMembers(record.members as unknown as string);
|
||||
const enabledCount = members.filter(m => m.enabled).length;
|
||||
const totalCount = members.length;
|
||||
return `${enabledCount}/${totalCount}`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -194,7 +194,25 @@ const ScheduledTaskLogPage = ({open, jobId, handleCancel}: Props) => {
|
||||
title: t('sysops.logs.result'),
|
||||
dataIndex: 'result',
|
||||
key: 'result',
|
||||
valueType: 'code',
|
||||
width: 200,
|
||||
render: (text: string) => {
|
||||
if (!text) return <Text type="secondary">-</Text>
|
||||
return (
|
||||
<div style={{
|
||||
maxHeight: 60,
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
}}>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
]
|
||||
case 'renew-certificate':
|
||||
@@ -244,6 +262,7 @@ const ScheduledTaskLogPage = ({open, jobId, handleCancel}: Props) => {
|
||||
options={false}
|
||||
dataSource={record.results}
|
||||
pagination={false}
|
||||
rowKey={(r: any, index: number) => r.name || r.id || index}
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {Col, Modal, Popover, Row} from "antd";
|
||||
import scheduledTaskApi, {ScheduledTask} from "@/api/scheduled-task-api";
|
||||
import assetApi from "@/api/asset-api";
|
||||
import ScheduledTaskRuntime from "@/pages/sysops/ScheduledTaskRuntime";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
|
||||
const ScheduledTaskModal = ({
|
||||
open,
|
||||
@@ -23,13 +24,33 @@ const ScheduledTaskModal = ({
|
||||
}: Props) => {
|
||||
let {t} = useTranslation();
|
||||
const formRef = useRef<ProFormInstance>(null);
|
||||
let [spec, setSpec] = useState('');
|
||||
let [runtimeOpen, setRuntimeOpen] = useState(false);
|
||||
let [specForRuntime, setSpecForRuntime] = useState('');
|
||||
|
||||
const {data: assetTreeData} = useQuery({
|
||||
queryKey: ['asset-tree', 'ssh'],
|
||||
queryFn: async () => {
|
||||
let items = await assetApi.tree('ssh');
|
||||
function processItem(item: any) {
|
||||
item.value = item.value || item.key || item.id;
|
||||
item.title = item.title || item.name || '';
|
||||
if (item.isLeaf && item.extra?.network) {
|
||||
item.title = item.title + ' (' + item.extra.network + ')';
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
item.children.forEach(processItem);
|
||||
}
|
||||
}
|
||||
if (items && items.length > 0) {
|
||||
items.forEach(processItem);
|
||||
}
|
||||
return items || [];
|
||||
},
|
||||
});
|
||||
|
||||
const get = async () => {
|
||||
if (id) {
|
||||
let data = await scheduledTaskApi.getById(id);
|
||||
setSpec(data.spec);
|
||||
return data
|
||||
}
|
||||
return {
|
||||
@@ -38,6 +59,14 @@ const ScheduledTaskModal = ({
|
||||
} as ScheduledTask;
|
||||
}
|
||||
|
||||
const handleRuntimeOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
const currentSpec = formRef.current?.getFieldValue('spec') || '';
|
||||
setSpecForRuntime(currentSpec);
|
||||
}
|
||||
setRuntimeOpen(open);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<Modal
|
||||
@@ -82,20 +111,14 @@ const ScheduledTaskModal = ({
|
||||
rules={[{required: true}]}
|
||||
tooltip={t('sysops.spec_tooltip')}
|
||||
fieldProps={{
|
||||
value: spec,
|
||||
onChange: (e) => {
|
||||
setSpec(e.target.value);
|
||||
},
|
||||
addonAfter: <div className={'cursor-pointer'}>
|
||||
<Popover
|
||||
content={<ScheduledTaskRuntime open={runtimeOpen} spec={spec}/>}
|
||||
content={runtimeOpen ? <ScheduledTaskRuntime open={runtimeOpen} spec={specForRuntime}/> : null}
|
||||
title={t('sysops.spec_run_time')}
|
||||
trigger="click"
|
||||
placement="rightTop"
|
||||
open={runtimeOpen}
|
||||
onOpenChange={(open) => {
|
||||
setRuntimeOpen(open);
|
||||
}}
|
||||
onOpenChange={handleRuntimeOpenChange}
|
||||
>
|
||||
{t('sysops.spec_run')}
|
||||
</Popover>
|
||||
@@ -135,34 +158,12 @@ const ScheduledTaskModal = ({
|
||||
name='assetIdList'
|
||||
rules={[{required: true}]}
|
||||
fieldProps={{
|
||||
treeData: assetTreeData || [],
|
||||
multiple: true,
|
||||
showSearch: true,
|
||||
treeDefaultExpandAll: true,
|
||||
treeNodeFilterProp: "title",
|
||||
}}
|
||||
request={async () => {
|
||||
let items = await assetApi.tree('ssh');
|
||||
|
||||
// 递归把 key 字段设置为 value,并且非叶子节点全部 disabled
|
||||
function setKeyAndDisabled(item: any) {
|
||||
item.value = item.key;
|
||||
if (!item.isLeaf) {
|
||||
// item.disabled = true;
|
||||
// 递归处理子节点
|
||||
if (item.children) {
|
||||
item.children.forEach(setKeyAndDisabled);
|
||||
}
|
||||
} else {
|
||||
item.title = item.title + ' (' + item.extra?.network + ')';
|
||||
}
|
||||
}
|
||||
|
||||
// 对获取到的所有节点进行处理
|
||||
items.forEach((item: any) => {
|
||||
setKeyAndDisabled(item);
|
||||
});
|
||||
return items;
|
||||
}}
|
||||
/>
|
||||
}
|
||||
return <></>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import React from 'react';
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import scheduledTaskApi from '@/api/scheduled-task-api';
|
||||
|
||||
@@ -14,26 +14,20 @@ const ScheduledTaskRuntime = ({open, spec}: Props) => {
|
||||
queryFn: () => {
|
||||
return scheduledTaskApi.getNextTenRuns(spec);
|
||||
},
|
||||
enabled: open,
|
||||
enabled: open && !!spec,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
query.refetch();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className={''}>
|
||||
{query.isError && <div className={'text-red-500'}>Error: {query.error?.message}</div>}
|
||||
<div className={'space-y-1'}>
|
||||
{Array.isArray(query.data) ? query.data.map((item: any) => {
|
||||
return <div key={item.id}>{item}</div>
|
||||
{Array.isArray(query.data) ? query.data.map((item: any, index: number) => {
|
||||
return <div key={index}>{item}</div>
|
||||
}) : []}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScheduledTaskRuntime;
|
||||
export default ScheduledTaskRuntime;
|
||||
|
||||
@@ -23,6 +23,11 @@ const ToolsPing = () => {
|
||||
setLogs((prevLogs) => [...prevLogs, event.data]);
|
||||
};
|
||||
|
||||
eventSource.addEventListener('done', () => {
|
||||
eventSource?.close();
|
||||
setRunning(false);
|
||||
});
|
||||
|
||||
eventSource.onerror = () => {
|
||||
eventSource?.close();
|
||||
setRunning(false);
|
||||
@@ -67,11 +72,11 @@ const ToolsPing = () => {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className='border rounded-lg p-4 flex-grow'>
|
||||
<pre>{logs.join("\n")}</pre>
|
||||
<div className='border rounded-lg p-4 flex-grow overflow-auto'>
|
||||
<pre className='whitespace-pre-wrap break-all'>{logs.join("\n")}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolsPing;
|
||||
export default ToolsPing;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Button, Form, Input, InputNumber, Space} from 'antd';
|
||||
import {Button, Form, Input, InputNumber} from 'antd';
|
||||
import React, {useState} from 'react';
|
||||
import {baseUrl, getToken} from "@/api/core/requests";
|
||||
import {useTranslation} from "react-i18next";
|
||||
@@ -15,7 +15,7 @@ const ToolsTcping = () => {
|
||||
let eventSource: EventSource | null = null;
|
||||
|
||||
const onSearch = (host: string, port: number) => {
|
||||
if (running) return; // 防止重复启动
|
||||
if (running) return;
|
||||
setRunning(true);
|
||||
setLogs([]);
|
||||
|
||||
@@ -25,6 +25,11 @@ const ToolsTcping = () => {
|
||||
setLogs((prevLogs) => [...prevLogs, event.data]);
|
||||
};
|
||||
|
||||
eventSource.addEventListener('done', () => {
|
||||
eventSource?.close();
|
||||
setRunning(false);
|
||||
});
|
||||
|
||||
eventSource.onerror = () => {
|
||||
eventSource?.close();
|
||||
setRunning(false);
|
||||
@@ -79,11 +84,11 @@ const ToolsTcping = () => {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className='border rounded-lg p-4 flex-grow'>
|
||||
<pre>{logs.join("\n")}</pre>
|
||||
<div className='border rounded-lg p-4 flex-grow overflow-auto'>
|
||||
<pre className='whitespace-pre-wrap break-all'>{logs.join("\n")}</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolsTcping;
|
||||
export default ToolsTcping;
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
"upload": "上传",
|
||||
"offline": "离线",
|
||||
"online": "在线",
|
||||
"search_placeholder": "关键词搜索"
|
||||
"search_placeholder": "关键词搜索",
|
||||
"max_length": "最大长度为 {{max}} 个字符"
|
||||
},
|
||||
"actions": {
|
||||
"new": "新建",
|
||||
@@ -1834,5 +1835,101 @@
|
||||
"403": "没有权限执行此操作",
|
||||
"404": "请求的资源不存在",
|
||||
"500": "服务器内部错误"
|
||||
},
|
||||
"permissions": {
|
||||
"dashboard": "数据概览",
|
||||
"asset-access": "访问资产",
|
||||
"asset-add": "新增资产",
|
||||
"asset-edit": "编辑资产",
|
||||
"asset-del": "删除资产",
|
||||
"asset-copy": "复制资产",
|
||||
"asset-conn-test": "连接测试",
|
||||
"asset-import": "导入资产",
|
||||
"asset-change-owner": "修改所有者",
|
||||
"asset-authorised-user-add": "授权用户",
|
||||
"asset-authorised-user-del": "取消授权用户",
|
||||
"asset-authorised-user-group-add": "授权用户组",
|
||||
"asset-authorised-user-group-del": "取消授权用户组",
|
||||
"credential-add": "新增凭证",
|
||||
"credential-del": "删除凭证",
|
||||
"credential-edit": "编辑凭证",
|
||||
"command-add": "新增命令",
|
||||
"command-edit": "编辑命令",
|
||||
"command-del": "删除命令",
|
||||
"command-exec": "执行命令",
|
||||
"command-change-owner": "修改所有者",
|
||||
"access-gateway-add": "新增网关",
|
||||
"access-gateway-del": "删除网关",
|
||||
"access-gateway-edit": "编辑网关",
|
||||
"online-session-disconnect": "断开会话",
|
||||
"online-session-monitor": "监控会话",
|
||||
"offline-session-playback": "回放会话",
|
||||
"offline-session-del": "删除会话",
|
||||
"offline-session-clear": "清空会话",
|
||||
"offline-session-command": "命令记录",
|
||||
"offline-session-reviewed": "标记已阅",
|
||||
"offline-session-unreviewed": "标记未读",
|
||||
"offline-session-reviewed-all": "全部标记已阅",
|
||||
"login-log-del": "删除登录日志",
|
||||
"login-log-clear": "清空登录日志",
|
||||
"storage-log-del": "删除文件日志",
|
||||
"storage-log-clear": "清空文件日志",
|
||||
"session-command": "命令日志",
|
||||
"job-add": "新增任务",
|
||||
"job-del": "删除任务",
|
||||
"job-edit": "编辑任务",
|
||||
"job-run": "执行任务",
|
||||
"job-change-status": "修改状态",
|
||||
"job-log": "任务日志",
|
||||
"job-log-clear": "清空日志",
|
||||
"storage-add": "新增存储",
|
||||
"storage-del": "删除存储",
|
||||
"storage-edit": "编辑存储",
|
||||
"storage-browse-download": "下载文件",
|
||||
"storage-browse-upload": "上传文件",
|
||||
"storage-browse-mkdir": "创建目录",
|
||||
"storage-browse-rm": "删除文件",
|
||||
"storage-browse-rename": "重命名",
|
||||
"storage-browse-edit": "编辑文件",
|
||||
"monitoring": "系统监控",
|
||||
"access-security-add": "新增规则",
|
||||
"access-security-del": "删除规则",
|
||||
"access-security-edit": "编辑规则",
|
||||
"login-policy-add": "新增策略",
|
||||
"login-policy-del": "删除策略",
|
||||
"login-policy-edit": "编辑策略",
|
||||
"login-policy-bind-user": "绑定用户",
|
||||
"user-add": "新增用户",
|
||||
"user-del": "删除用户",
|
||||
"user-edit": "编辑用户",
|
||||
"user-change-password": "修改密码",
|
||||
"user-enable-disable": "启用/禁用",
|
||||
"user-reset-totp": "重置OTP",
|
||||
"user-bind-asset": "绑定资产",
|
||||
"user-unbind-asset": "解绑资产",
|
||||
"login-policy-unbind-user": "解绑用户",
|
||||
"user-unbind-login-policy": "解绑策略",
|
||||
"role-add": "新增角色",
|
||||
"role-del": "删除角色",
|
||||
"role-edit": "编辑角色",
|
||||
"role-detail": "角色详情",
|
||||
"user-group-add": "新增用户组",
|
||||
"user-group-del": "删除用户组",
|
||||
"user-group-edit": "编辑用户组",
|
||||
"user-group-detail": "用户组详情",
|
||||
"user-group-bind-asset": "绑定资产",
|
||||
"user-group-unbind-asset": "解绑资产",
|
||||
"command-filter-add": "新增拦截器",
|
||||
"command-filter-del": "删除拦截器",
|
||||
"command-filter-edit": "编辑拦截器",
|
||||
"command-filter-rule-add": "新增规则",
|
||||
"command-filter-rule-put": "修改规则",
|
||||
"command-filter-rule-del": "删除规则",
|
||||
"strategy-add": "新增策略",
|
||||
"strategy-edit": "编辑策略",
|
||||
"strategy-del": "删除策略",
|
||||
"strategy-detail": "策略详情",
|
||||
"setting": "系统设置",
|
||||
"info": "系统信息"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user