Compare commits

...

252 Commits

Author SHA1 Message Date
chenjinsong
284888863e fix: 恢复detection的policy显示 2024-05-20 11:26:59 +08:00
刘洪洪
5c4993cf12 fix: 修复report编辑按钮不会disabled的问题 2024-05-11 18:17:55 +08:00
刘洪洪
b8ee685230 fix: report一系列问题修改 2024-05-11 16:28:27 +08:00
陈劲松
30b1328576 Merge branch 'cherry-pick-3513a9c1' into 'dev-24.01-m22'
fix: 修复entity的搜索测试用例报错的问题

See merge request cyber-narrator/cn-ui!72
2024-05-11 06:49:45 +00:00
刘洪洪
740e7bbae6 fix: 修复entity的搜索测试用例报错的问题
(cherry picked from commit 3513a9c145)
2024-05-11 06:49:38 +00:00
陈劲松
f020fe33b5 Merge branch 'cherry-pick-df183c18' into 'dev-24.01-m22'
fix: 修复entity和detection搜索时随意输入字段,没有错误提示且resource为空的问题

See merge request cyber-narrator/cn-ui!71
2024-05-09 09:08:56 +00:00
刘洪洪
75b47d7ba7 fix: 修复entity和detection搜索时随意输入字段,没有错误提示且resource为空的问题
(cherry picked from commit df183c1836)
2024-05-09 09:08:48 +00:00
chenjinsong
a26b0c848d fix: 修复链路四元组图字段名未升级的问题 2024-04-30 17:06:46 +08:00
陈劲松
d2ad042cf9 Merge branch 'cherry-pick-ca6f6a02' into 'dev-24.01-m22'
CN-1651 fix: 修复链路links下钻报错问题

See merge request cyber-narrator/cn-ui!69
2024-04-28 10:31:54 +00:00
chenjinsong
a844d44d1d CN-1651 fix: 修复链路links下钻报错问题
(cherry picked from commit ca6f6a02e9)
2024-04-28 10:31:45 +00:00
chenj
ad603339de fix: 隐藏detection页的policy入口 2024-04-01 21:06:04 +08:00
陈劲松
08f0cd4522 Merge branch 'cherry-pick-e645eeb1' into 'dev-24.01'
fix: 更改license失效码

See merge request cyber-narrator/cn-ui!68
2024-03-25 07:51:28 +00:00
chenjinsong
0c20757d07 fix: 更改license失效码
(cherry picked from commit e645eeb1a5)
2024-03-25 07:51:22 +00:00
陈劲松
2c7c7293b4 Merge branch 'cherry-pick-5511191c' into 'dev-24.01'
fix: license上传文件后进行页面刷新

See merge request cyber-narrator/cn-ui!67
2024-03-25 07:51:06 +00:00
hanyuxia
d035677c2f fix: license上传文件后进行页面刷新
(cherry picked from commit 5511191c90)
2024-03-25 07:50:57 +00:00
陈劲松
50cdbde8bf Merge branch 'cherry-pick-8a18fb09' into 'dev-24.01'
fix: 许可证下载文件名称从响应头中的Content-Disposition获取

See merge request cyber-narrator/cn-ui!66
2024-03-25 07:49:56 +00:00
hanyuxia
d27738b9ec fix: 许可证下载文件名称从响应头中的Content-Disposition获取
(cherry picked from commit 8a18fb09f4)
2024-03-25 07:49:49 +00:00
chenjinsong
85456c1e47 fix: 更改插件的周期信息 2024-03-20 20:08:33 +08:00
陈劲松
327cbde233 Merge branch 'cherry-pick-bcf57611' into 'dev-24.01'
feat:插件id及逻辑变动(有一些同名,但type(ip、domain)不同的,合并成一条;合并后的记录,type列既有ip标签又有domain标签)

See merge request cyber-narrator/cn-ui!65
2024-03-20 09:56:48 +00:00
hyx
cbf71eb1b9 feat:插件id及逻辑变动(有一些同名,但type(ip、domain)不同的,合并成一条;合并后的记录,type列既有ip标签又有domain标签)
(cherry picked from commit bcf57611ef)
2024-03-20 09:56:40 +00:00
chenjinsong
4e0eb3f143 fix: 修复license未认证的情况下,按回车键可以登录的问题 2024-03-19 16:03:21 +08:00
陈劲松
a17c68bb26 Merge branch 'cherry-pick-da015603' into 'dev-24.01'
fix: 修复下载license时文件名不对的问题

See merge request cyber-narrator/cn-ui!63
2024-03-19 07:46:16 +00:00
chenjinsong
d1ae513124 fix: 修复下载license时文件名不对的问题
(cherry picked from commit da0156031c)
2024-03-19 07:46:09 +00:00
陈劲松
91915996c0 Merge branch 'cherry-pick-d0b29c24' into 'dev-24.01'
feat: CN-1558 开发license页面,支持license拦截

See merge request cyber-narrator/cn-ui!62
2024-03-19 03:04:37 +00:00
hanyuxia
8f17647752 feat: CN-1558 开发license页面,支持license拦截
(cherry picked from commit d0b29c24ba)
2024-03-19 03:04:31 +00:00
陈劲松
29a319609e Merge branch 'cherry-pick-21da4f0d' into 'dev-24.01'
feat: CN-1558 开发license页面,支持license拦截

See merge request cyber-narrator/cn-ui!61
2024-03-19 03:04:10 +00:00
hanyuxia
08ae827e31 feat: CN-1558 开发license页面,支持license拦截
(cherry picked from commit 21da4f0d49)
2024-03-19 03:04:04 +00:00
陈劲松
854a846296 Merge branch 'cherry-pick-06a8bd96' into 'dev-24.01'
feat: CN-1558 开发license页面,支持license拦截

See merge request cyber-narrator/cn-ui!60
2024-03-19 03:02:37 +00:00
hanyuxia
42aa7671a3 feat: CN-1558 开发license页面,支持license拦截
(cherry picked from commit 06a8bd9626)
2024-03-19 03:02:30 +00:00
陈劲松
fb89134f95 Merge branch 'cherry-pick-4429d225' into 'dev-24.01'
feat: CN-1558 开发license页面(未完成:登陆页支持license拦截)

See merge request cyber-narrator/cn-ui!59
2024-03-19 03:01:31 +00:00
hyx
584ad8d538 feat: CN-1558 开发license页面(未完成:登陆页支持license拦截)
(cherry picked from commit 4429d225f1)
2024-03-19 03:01:23 +00:00
chenjinsong
c43a17c984 feat: 新增一个vpn 2024-01-30 10:31:37 +08:00
chenjinsong
df5decc3b4 feat: 新增一个vpn 2024-01-30 10:31:36 +08:00
刘洪洪
cca54359a8 fix: 修复schema字段变动导致模糊搜索不能识别的问题 2024-01-30 10:30:03 +08:00
chenjinsong
9e8b3e9d64 fix: vpn类的tag都标位红色 2024-01-29 14:17:36 +08:00
chenjinsong
8e5f5b49cb fix: plugin增加请求参数pageSize=-1 2024-01-29 11:05:59 +08:00
chenjinsong
4441bdef10 fix: 调整plugin页面样式 2024-01-26 17:35:01 +08:00
hyx
849df8c8d8 feat:增加9个系统插件 2024-01-25 12:29:58 +08:00
hyx
475b112835 fix: 调整npm下钻表格列 loading样式 2024-01-18 19:05:17 +08:00
hyx
b286861137 fix:CN-1546 知识库导入空csv,接口报错界面未提示 2024-01-15 18:41:42 +08:00
chenjinsong
e67ec0905a fix: 修改三个新插件的id 2024-01-15 16:26:02 +08:00
hyx
7f697658f4 feat:CN-1545 npm下钻table异步加载的列增加loading提示 2024-01-15 14:29:00 +08:00
刘洪洪
ff17e52bc2 fix: 修复新增事件策略界面返回时,第三步校验总被触发的问题 2024-01-12 17:22:25 +08:00
hanyuxia
b13e313114 feat: 增加3个插件配置 2024-01-12 15:16:04 +08:00
刘洪洪
dbf5885b72 Merge remote-tracking branch 'origin/dev' into dev 2024-01-12 11:56:33 +08:00
刘洪洪
ab10f7ff0f fix: 优化滚动加载代码,避免占用资源 2024-01-12 11:56:08 +08:00
hanyuxia
6d00137260 fix: 知识库删除操作报错 2024-01-12 11:54:23 +08:00
chenjinsong
ddafbf44f2 fix: 修复不能识别9位和12位时间戳的问题 2024-01-12 11:44:20 +08:00
hanyuxia
096666ce69 fix:实体Subscriber详情页,top应用样式调整(柱状图右侧数据显示不全) 2024-01-12 09:51:05 +08:00
hyx
bf34b4321c fix: 1.Subscribe实体详情页top 应用:数据为0时,显示<1B 2024-01-11 18:09:53 +08:00
刘洪洪
83c8f400e2 fix: 修复搜索组件tag模式下in操作多选后没有回显的问题 2024-01-11 18:06:09 +08:00
chenjinsong
cb93658be4 fix: 更改subscriber实体地图最大缩放级别为13 2024-01-11 17:28:45 +08:00
hyx
e546f39c93 fix: 1.Subscribe实体详情页top 应用样式调整 2024-01-11 17:16:27 +08:00
hyx
a3e770963f fix: 1.Subscribe实体详情页top 应用样式调整;2.插件列表展示内容过滤; 2024-01-11 16:36:07 +08:00
刘洪洪
8e4d5bff88 fix: 去除无效打印 2024-01-11 15:16:16 +08:00
刘洪洪
877c008fe7 fix: 1.修复实体详情安全事件只显示10条的问题;2.实体列表的实体关系radio初始化高亮 2024-01-11 15:12:27 +08:00
刘洪洪
fc85069227 Merge remote-tracking branch 'origin/dev' into dev 2024-01-11 14:56:17 +08:00
刘洪洪
9ac9cd93e1 fix: 更改实体列表关于实体、相关实体的选择方式 2024-01-11 14:56:06 +08:00
chenjinsong
426301fce0 fix: 更改subscriber实体地图最大缩放级别 2024-01-11 14:55:10 +08:00
chenjinsong
b8c90fee71 fix: 修复subscriber实体地图时间参数长度不对的问题 2024-01-11 11:50:10 +08:00
chenjinsong
9ae4453587 fix: 修复subscriber实体app曲线中app数量大于6时显示异常的问题 2024-01-11 11:42:35 +08:00
hyx
734bfb33d3 fix: 实体subscribe详情页的Top 应用部分不显示数据 2024-01-11 10:58:01 +08:00
刘洪洪
ada66ee9d2 Merge remote-tracking branch 'origin/dev' into dev 2024-01-11 10:18:45 +08:00
刘洪洪
facbd0f17c fix: 搜索时向地址栏添加hideRelated参数 2024-01-11 10:18:32 +08:00
chenjinsong
471e5f2342 fix: 修复实体地图请求接口未带时间参数的问题 2024-01-11 10:16:54 +08:00
hyx
ad800637fd fix: DNS下钻表格,当前tab为Qtypes或者Rcodes时,自定义中取消当前tab,重新加载的页面的第一列数据为空 2024-01-11 09:20:00 +08:00
刘洪洪
03400f1fab fix: 修复给搜索条件的=添加空格导致识别失败的问题 2024-01-10 17:22:00 +08:00
hyx
0e51d49e15 fix: 1.调整subscriber实体中tab错误提示信息的位置;2.暂时不请求subscriber实体对应的4个tab的接口,显示未noData; 2024-01-10 17:09:56 +08:00
刘洪洪
a3ab4530b7 fix: 1.新增报告时间范围添加国际化;2.detection跳转实体详情缺少参数 2024-01-10 15:49:41 +08:00
hyx
a8ecdd48bd fix: 删除subscriber模块相关的测试内容 2024-01-10 14:54:14 +08:00
刘洪洪
6bdefa5497 fix: 修复搜索组件高度有滚动条的问题 2024-01-09 17:06:00 +08:00
刘洪洪
e0a8815d4a fix: 修复实体切换时间搜索高亮不生效的问题 2024-01-09 11:19:44 +08:00
hanyuxia
f4bf9a1f31 feat:CN-1522 插件管理界面与接口调试 2024-01-09 11:10:28 +08:00
刘洪洪
2661b5e69d CN-1540 fix: 更新获取tag字段名pluginName为vpnServiceName 2024-01-08 16:42:41 +08:00
刘洪洪
0e13925908 fix: 修复隐藏相关实体按钮错误的问题 2024-01-05 17:17:17 +08:00
刘洪洪
2219bba781 fix: 修复实体详情tab单测实例的tag未经过映射转换导致报错的问题 2024-01-04 16:54:20 +08:00
刘洪洪
80027fbd46 CN-1540 fix: 提取Tag展示逻辑公共部分 2024-01-04 16:39:39 +08:00
刘洪洪
8b81181ae2 CN-1540 fix: 实体智能学习类型的Tag展示逻辑更改 2024-01-03 17:45:52 +08:00
刘洪洪
a9215469fb fix: 调整实体详情信誉等级tag字体居中 2024-01-02 17:52:21 +08:00
hanyuxia
467b2c27b7 feat:CN-1522 插件管理界面与接口调试 2023-12-29 18:04:50 +08:00
hanyuxia
ca3b9d926c fix: 详情实体behavior pattern标签顶部数值计算逻辑修改(去掉key为total的内容) 2023-12-29 17:59:55 +08:00
hanyuxia
160f708596 feat:CN-1522 插件管理页开发 2023-12-29 17:06:14 +08:00
刘洪洪
3d3ad53395 fix: 实体详情tab的安全、性能tab的持续时间字段修改 2023-12-29 15:09:06 +08:00
刘洪洪
c874665397 fix: detection顶部柱状图位置调整 2023-12-29 14:13:54 +08:00
刘洪洪
f40e9ae759 fix: 修复搜索组件的历史搜索中同一条搜索语句点击第二次不能填充搜索框的问题 2023-12-29 11:57:45 +08:00
刘洪洪
1f33834146 fix: 完善npm--location的Throughput曲线图的legend初始化 2023-12-29 11:23:20 +08:00
刘洪洪
cbb27e2538 fix: npm曲线图去除重复功能接口调用 2023-12-29 09:50:50 +08:00
hanyuxia
1d5bd39f54 feat:CN-1522 删除内置知识库列表的“智能情报学习”栏 2023-12-29 09:40:42 +08:00
刘洪洪
2e113df17b fix: 1、修复detection顶部柱状图无数据的地方鼠标移上去会显示上一个或下一个柱子的数据;2、x轴显示的时间并非时间选择器的起始时间。 2023-12-28 17:27:02 +08:00
刘洪洪
ec0b3b28d4 fix: 将实体详情的信誉等级与detection的实体信誉等级进行样式统一 2023-12-28 10:38:21 +08:00
hyx
83971fa08b feat:CN-1522 删除内置知识库列表的“智能情报学习”栏 2023-12-28 09:23:00 +08:00
刘洪洪
fc24f6bbfb fix: 修复实体详情的安全事件tab时间显示异常的问题 2023-12-27 18:10:25 +08:00
刘洪洪
d7bd49a9ac fix: 去除未被使用的文件引用,以及调整部分代码格式 2023-12-27 16:08:43 +08:00
hyx
37238377d2 Merge remote-tracking branch 'origin/dev' into dev 2023-12-27 16:05:50 +08:00
hyx
b42bfded33 feat:CN-1522 插件管理页开发 2023-12-27 16:05:35 +08:00
刘洪洪
50e4112181 fix: 调整搜索组件输入过长会换行的样式,以及error提示的层级 2023-12-27 14:20:06 +08:00
hanyuxia
70ab0a46e6 feat:CN-1522 插件管理页开发(细节需要再调整),及代码整理 2023-12-26 18:24:05 +08:00
chenjinsong
cb6d0e4765 fix: 调整link测试用例 2023-12-26 15:40:37 +08:00
chenjinsong
15b90c11e9 CN-1519 fix: 链路配置更改;链路组件逻辑更改; 2023-12-25 18:35:14 +08:00
hyx
3f97c3e97f Merge remote-tracking branch 'origin/dev' into dev 2023-12-25 16:29:01 +08:00
hyx
e70fa5d569 fix: 下钻表格测试用例报错修改 2023-12-25 16:28:46 +08:00
刘洪洪
5ba4d8a1fe CN-1520 fix: 调整实体列表页接口请求方式 2023-12-25 16:27:45 +08:00
hyx
4ae22a1ea0 fix: 各模块流量曲线图测试用例修改 2023-12-25 14:47:48 +08:00
刘洪洪
e08a7a2434 fix: 修复jest单测获取codemirror依赖失败的问题 2023-12-25 14:32:22 +08:00
刘洪洪
2dbf5ee84e fix: 修复jest单测获取不到codemirror依赖的问题 2023-12-25 14:24:28 +08:00
刘洪洪
b7c5638c04 fix: 修复LinkTrafficLine单测用例报错的问题 2023-12-25 13:59:40 +08:00
刘洪洪
d1e8430e37 fix: 去除无效打印 2023-12-22 18:15:42 +08:00
刘洪洪
2028861b26 CN-1375 fix: 编写高级搜索器自动化测试用例 2023-12-22 17:59:44 +08:00
hanyuxia
21eafd4087 fix:CN-1518 Subscriber流量曲线图的交互逻辑优化(减少请求次数),及其他模块相关代码调整 2023-12-22 17:32:02 +08:00
hanyuxia
899b0e1e9d fix:CN-1518 Link Monitor流量曲线图的交互逻辑优化(减少请求次数) 2023-12-22 11:32:31 +08:00
hyx
eb7a9f875c fix: CN-1518 实体详情流量曲线图的交互逻辑优化(减少请求次数) 2023-12-21 15:59:04 +08:00
hyx
0f2f5ecdde Merge remote-tracking branch 'origin/dev' into dev 2023-12-20 20:40:01 +08:00
hyx
164089f99e fix: CN-1518 Npm流量曲线图的交互逻辑优化(减少请求次数) 2023-12-20 20:39:45 +08:00
刘洪洪
abab03eb12 fix: 1、搜索组件有关枚举的添加国际化;2、修复tag模式下切换key,value还保留上次选择的问题;3、修复切换语言环境,搜索参数包含其他语言导致不能识别转换的问题 2023-12-20 18:38:38 +08:00
hyx
19160c0da1 fix: CN-1518 DNS流量曲线图的交互逻辑优化(减少请求次数) 2023-12-20 10:41:43 +08:00
hanyuxia
56ad79bd0d fix:CN-1518 Network Overview流量曲线图的交互逻辑优化,减少请求次数 2023-12-19 13:44:14 +08:00
刘洪洪
a2a7bdcd14 fix: 调整showHint弹窗样式 2023-12-18 10:12:13 +08:00
刘洪洪
1d67584c9a fix: 调整搜索组件showHint样式和提示文本,隐藏/显示相关实体时联动左侧filter 2023-12-15 18:48:51 +08:00
刘洪洪
8b72a37489 fix: 修复domain in()和其他搜索条件一起时,识别in操作符失败的问题 2023-12-15 15:57:12 +08:00
hyx
e13c9afe78 fix: CN-1514 表结构变更后前端代码中字段更改(下钻表格);表格下钻后,切换顶部的下拉列表,表格报错; 2023-12-15 10:50:52 +08:00
刘洪洪
8001d66ca8 fix: 修复搜索高亮取值有时为空的问题 2023-12-14 16:52:09 +08:00
刘洪洪
d2cb42687e fix: 简化部分代码,添加注释,添加是否需要高亮指令noHighlight 2023-12-14 16:20:33 +08:00
刘洪洪
a3c8baea5c fix: 修复搜索组件tag模式下,in操作赋值错误的问题 2023-12-14 10:28:43 +08:00
刘洪洪
f5f857bcb4 CN-1479 fix: 搜索组件showHint提示文本修改 2023-12-13 17:05:16 +08:00
刘洪洪
3d1fbfa5fd fix: 删除无用文件,搜索组件text修改变量名 2023-12-12 17:29:19 +08:00
刘洪洪
3d5c69d87b CN-1479 fix: 搜索组件补全模糊搜索与tag模式关于枚举in操作的处理 2023-12-12 10:06:21 +08:00
chenjinsong
e1a26b60ae CN-1440 fix: policy的library增加鼠标悬浮提示事件 2023-12-11 17:48:38 +08:00
hyx
a7dfa33da2 暂时隐藏实体详情的性能事件 2023-12-11 14:08:20 +08:00
hyx
67189ade10 fix:1.撤销 subscriber top app 上下行数据等于0时转成<0.01 2023-12-11 11:34:12 +08:00
hyx
6c9a05f98d fix:1.subscriber top app 展示数据等于0时转成<0.01 2023-12-11 11:28:47 +08:00
hyx
720754bee2 fix:1.subscriber kpi及top app 展示数据等于0时转成<0.01 2023-12-09 11:24:21 +08:00
hyx
27c1d28870 CN-1510 feat:更改detection柱状图的tooltip交互方式 2023-12-09 10:16:41 +08:00
chenjinsong
535d2646b2 fix: policy接口参数变更 2023-12-08 16:23:06 +08:00
hyx
51d6c1d4be Merge remote-tracking branch 'origin/dev' into dev 2023-12-08 09:49:43 +08:00
hyx
742cf5d6f2 subscriber 基本信息中实体类型名称国际化 2023-12-08 09:49:21 +08:00
刘洪洪
a1ae084216 CN-1479 fix: 搜索组件text和tag模式交互添加枚举 2023-12-07 18:23:20 +08:00
hyx
bca51683e5 fix:实体详情基本信息部分底部空白过大调整 2023-12-07 18:14:38 +08:00
chenjinsong
da2ac78ea7 fix: 修复有时刷新后地图无法显示的问题 2023-12-07 17:22:50 +08:00
chenjinsong
c55a205003 fix: 修复有时刷新后地图无法显示的问题 2023-12-07 16:34:18 +08:00
hyx
f54ca30a58 1.CN-1506 Subscriber详情页基础信息图表开发;2.时间选择器组件选择具体日期时,显示省略号; 2023-12-07 16:25:14 +08:00
chenjinsong
ab30ddf49b fix: 更正subscriber地图标题 2023-12-07 11:39:48 +08:00
刘洪洪
8cd3f87c3e CN-1479 fix: 搜索组件添加showHint自动完成提示 2023-12-07 10:03:31 +08:00
chenjinsong
5106f23c2b fix: 修改subscriber地图组件url 2023-12-06 18:23:59 +08:00
hanyuxia
27e59586ed 暂时隐藏subscriber基本信息的Analysis 2023-12-06 17:52:36 +08:00
hanyuxia
da9ae6831e fix: 1.错误信息提示框将时间选择器组件遮住;2.subscriber kpi代码调整; 2023-12-06 17:30:45 +08:00
hanyuxia
2f315bf52c CN-1507 时间选择器增加左对齐的风格 2023-12-06 16:46:13 +08:00
刘洪洪
2b3f6e6b31 fix: 修复实体列表、检测界面的时间在刷新界面后显示异常的问题 2023-12-06 16:37:49 +08:00
chenjinsong
74d95f110e CN-1508 feat: subscriber流量曲线图开发 2023-12-06 16:16:12 +08:00
chenjinsong
2c12284415 CN-1484 fix: 调整error提示的位置 2023-12-01 18:08:38 +08:00
chenjinsong
60d06792b4 CN-1484 feat: 地图事件开发 2023-12-01 17:22:32 +08:00
chenjinsong
8bbebc3a8b CN-1484 feat: 静态地图 2023-11-30 17:02:44 +08:00
hyx
d43a9c8bba CN-1488 Subscriber详情页TAB组件开发 2023-11-30 09:16:45 +08:00
chenjinsong
d996769f7e fix: 修复测试用例报错问题 2023-11-27 11:23:31 +08:00
hanyuxia
a9e84fd714 CN-1483 network overview单位统一 2023-11-24 17:11:42 +08:00
hanyuxia
6450e8e050 CN-1481 Subscribe详情页KPI组件开发;CN-1482 Subscribe详情页top app组件开发 2023-11-24 14:39:33 +08:00
chenjinsong
f25805ea0a CN-1456 feat: 一些国际化 2023-11-22 16:48:17 +08:00
刘洪洪
5d42334074 fix: 恢复被误删的代码 2023-11-22 10:47:52 +08:00
刘洪洪
b30a8a8a25 fix: 实体列表恢复事件数量,并完善下拉详情和列表事件数量的一致性 2023-11-22 10:44:17 +08:00
刘洪洪
bfb1d93d95 fix: 修复npm--event的严重程度前色块标识丢失的问题 2023-11-21 18:33:22 +08:00
刘洪洪
0fa7aeebe6 fix: 修复实体详情tab因前面接口报错导致后面tab的数量显示为0的问题 2023-11-21 18:09:39 +08:00
hanyuxia
6e8566db75 fix: npm 下钻表格自定义配置metric时,界面混乱(名称过长导致) 2023-11-21 17:47:38 +08:00
chenjinsong
e26d2b40ab CN-1456 feat: 折线图参考线等国际化 2023-11-21 14:33:16 +08:00
chenjinsong
6c1f1ed71b CN-1475 feat: subscribe配置 2023-11-20 18:48:10 +08:00
chenjinsong
b4b72fc1e1 CN-1468 fix: 补充一些开关的权限控制 2023-11-20 18:14:42 +08:00
hyx
b3d4bbd440 fix:CN-1470 调整新增/修改role页面权限树的逻辑:1.选中节点时,同步选中所有的父辈节点,子节点不进行同步操作;2.取消选中节点时,同步取消选中所有子节点,对父节点不进行同步操作; 2023-11-20 15:27:14 +08:00
刘洪洪
6c90ebe7bc fix: 实体详情tab恢复security事件 2023-11-20 11:52:52 +08:00
chenjinsong
ef3994e14a fix: 修复实体下拉中事件时间问题;behavior pattern的百分比计算逻辑调整 2023-11-20 10:54:03 +08:00
chenjinsong
abc1d07e6c CN-1456 feat: 内置知识库标题和描述增加国际化 2023-11-17 17:10:40 +08:00
hanyuxia
38af43b322 fix: 修改用户时点保存按钮无反应(密码验证代码处的问题) 2023-11-17 16:09:18 +08:00
hanyuxia
90ed543a84 fix:1 检测->事件类型饼图: resize情况下的,legend宽度自适应 2023-11-17 15:21:20 +08:00
chenjinsong
c66a6b7f59 fix: 实体详情security event tab取值问题修复 2023-11-17 14:21:37 +08:00
hanyuxia
a0af36b7c0 fix:1 检测->事件类型饼图legend宽度随分辨率而改变代码补充 2023-11-17 14:13:04 +08:00
hanyuxia
f95dc60cd3 Merge remote-tracking branch 'origin/dev' into dev 2023-11-17 13:54:39 +08:00
hanyuxia
7d64868ce0 fix:1 CN-1470 调整新增/修改role页面权限树的逻辑;2.检查模块下的事件类型饼图legend宽度随分辨率而改变; 2023-11-17 13:54:21 +08:00
chenjinsong
62a7a629e5 fix: 修改一些国际化 2023-11-17 11:41:25 +08:00
chenjinsong
e3c6c21545 fix: 补充一些国际化 2023-11-17 11:20:21 +08:00
chenjinsong
3b2eaf3851 fix: 搜索组件样式调整 2023-11-16 19:45:43 +08:00
chenjinsong
ee05f47f2d fix: 一些国际化和样式调整 2023-11-16 17:29:54 +08:00
hyx
07dd16f2c2 CN-1410 Detections模块中,无法根据面包屑导航进行跳转页面 2023-11-15 22:22:53 +08:00
chenjinsong
9103c454c2 fix: 调整.cn-container样式 2023-11-15 18:11:33 +08:00
chenjinsong
e5a7c2abfa fix: behavior pattern的bar设置最小高度 2023-11-15 17:53:34 +08:00
刘洪洪
e288e20fd7 fix: 实体、检测搜索框高度调整 2023-11-15 16:22:49 +08:00
刘洪洪
ec746f0fc7 CN-1478 fix: detection事件列表搜索框增加历史记录 2023-11-15 15:26:37 +08:00
刘洪洪
4ca33e9ede CN-1407 fix: Detections-Policies-Create Event Policy模块下,添加取消按钮 2023-11-15 14:47:51 +08:00
刘洪洪
b8105a4565 CN-1439 fix: Detections-Security events模块中,事件日志详情中添加与Policy的交互 2023-11-15 14:46:02 +08:00
hyx
29157ae7d3 代码提交错误修正 2023-11-15 10:02:59 +08:00
刘洪洪
9c0ce493ab fix: 实体列表ip实体下拉详情添加ISP信息 2023-11-14 15:49:53 +08:00
刘洪洪
4662061fc5 fix: 实体列表样式调整以及禁止detection右侧活跃攻击方条形图的文字点击 2023-11-14 14:42:42 +08:00
刘洪洪
0eb0346abc fix: 实体历史搜索的时间展示和其他时间统一为YYYY-MM-DD hh:mm:ss 2023-11-14 10:18:37 +08:00
刘洪洪
c8926177f7 fix: 修复单测报错的问题 2023-11-13 18:12:04 +08:00
刘洪洪
5aa5d77511 fix: 1、修复实体列表搜索后city两个值一样时,第一个值颜色变浅的问题;2、当搜索结果为空时,将隐藏相关实体按钮进行隐藏。 2023-11-13 17:52:23 +08:00
hyx
38368a6cb8 CN-1461 提取界面各功能的默认时间条件至config.js 2023-11-13 17:23:32 +08:00
刘洪洪
051aeadbca fix: 调整实体搜索高亮样式 2023-11-13 15:36:52 +08:00
刘洪洪
8c2119e773 CN-1458 fix: 修复detection列表点击echarts图形却没有执行搜索 2023-11-13 11:52:08 +08:00
刘洪洪
cb70fb0236 CN-1439 fix: Detections-Security events模块中,事件日志详情中添加Policy ID 2023-11-13 11:50:15 +08:00
刘洪洪
853fa79d4c CN-1448 fix: 实体搜索界面增加相关实体过滤功能 2023-11-10 18:26:24 +08:00
刘洪洪
2f919d1774 fix: 修复实体搜索切换页码不高亮,以及多个搜索关键字拼接为一个字段时不高亮等问题 2023-11-10 18:15:33 +08:00
刘洪洪
360fffde68 fix: detection列表左侧filter的status和Severity进行排序 2023-11-10 17:00:19 +08:00
刘洪洪
bb2a7676e6 CN-1458 fix: detection事件列表的筛选功能与实体的统一 2023-11-10 14:11:46 +08:00
chenjinsong
de698f0a71 CN-1468 fix: 为一些按钮增加权限控制 2023-11-10 11:43:11 +08:00
刘洪洪
1c1d354a6c CN-1449 fix: 实体下拉展开的相关实体添加搜索高亮 2023-11-09 18:42:41 +08:00
刘洪洪
8126618e54 fix: 修复实体、检测界面不能滚动的问题 2023-11-09 17:44:24 +08:00
chenjinsong
5a8796688a CN-1468 fix: 更改知识库的新路由 2023-11-09 17:35:04 +08:00
chenjinsong
be36b2604b fix: 暂时关闭下钻table的自动化测试用例 2023-11-09 16:58:25 +08:00
chenjinsong
330a4b0d3b CN-1468 fix: 路由动态设置 2023-11-09 16:17:25 +08:00
刘洪洪
1410f890f3 CN-1415 fix: 修复搜索历史排序与时间展示问题 2023-11-08 15:42:23 +08:00
刘洪洪
78105475fb CN-1457 fix: 实体列表检索新增时间筛选框 2023-11-08 14:28:37 +08:00
刘洪洪
02a9f35070 fix: 搜索历史面板添加国际化 2023-11-07 18:42:41 +08:00
刘洪洪
c89397da97 CN-1415 fix: 建议Entity列表页增加Search History用于查看和使用历史搜索条件 2023-11-07 18:36:01 +08:00
chenjinsong
36c3db5dee CN-1414 fix: policy搜索框参数从name改为q 2023-11-07 16:35:28 +08:00
hanyuxia
366bb8f17f CN-1443 Detections-Security events模块中,Event type 下的事件类型名称被遮挡 2023-11-07 10:50:49 +08:00
刘洪洪
1c00f568fa CN-1449 fix: 实体搜索结果对命中字段高亮显示 2023-11-06 19:50:29 +08:00
chenjinsong
93be846064 CN-1461 feat: dashboard增加从config.js取时间范围的逻辑 2023-11-06 17:52:55 +08:00
chenjinsong
ffb822d65d fix: 提取默认默认时间条件为配置 2023-11-06 17:40:24 +08:00
刘洪洪
f8e9d36c8a fix: linkMonitor网格图删除空行空列方法添加空判断 2023-11-06 10:35:15 +08:00
刘洪洪
241665cd80 CN-1416 fix: 增加一些字符的国际化映射 2023-11-06 10:33:47 +08:00
chenjinsong
ee57ca4c6c fix: 修改注释 2023-11-03 15:02:31 +08:00
hyx
2bb6c54cab fix:1.自定义知识库列表,引用列,两个引用之间无空隙 2023-11-02 15:10:09 +08:00
hyx
65827d27e4 fix: 1.CN-1446 学习IP趋势图里未按照选择的时间范围展示对应时间周期;2.初次进入知识库的学习引擎日志tab,底部柱状图未初始化当前tab 2023-11-01 16:05:34 +08:00
刘洪洪
98948ff179 fix: 调整实体搜索框的样式 2023-11-01 15:17:49 +08:00
刘洪洪
74dc057090 CN-1447 fix: 修复在Entity搜索框中添加两个重复的搜索条件和一个不重复条件时,页面报错的问题 2023-11-01 14:50:41 +08:00
hyx
e8959bab27 CN-1424 在Network Overview搜索Providers/Applications后无法滚动查看所有符合搜索条件的数据 2023-11-01 14:37:23 +08:00
刘洪洪
a7ce777e10 fix: 修复detection点击左侧filter后取消不生效的问题 2023-11-01 14:18:59 +08:00
刘洪洪
d7d450222b fix: 修复首次登录时语言没有存入缓存的问题 2023-11-01 12:51:00 +08:00
刘洪洪
3da4b4b20a fix: 调整语言的中英文变量 2023-11-01 12:10:54 +08:00
chenjinsong
a0d2160b43 CN-1406 fix: npm map的client、server增加国际化 2023-11-01 11:44:23 +08:00
chenjinsong
b94dba9ed3 fix: 修复内置知识库更新记录有时候会缺少user列的问题 2023-11-01 10:30:36 +08:00
陈劲松
696b03ad40 Merge branch 'cherry-pick-0152c46d' into 'dev'
fix: 修复知识库更新记录的时间丢失时区的问题

See merge request cyber-narrator/cn-ui!57
2023-11-01 02:02:42 +00:00
chenjinsong
f199442c60 fix: 修复知识库更新记录的时间丢失时区的问题
(cherry picked from commit 0152c46d05)
2023-11-01 02:02:35 +00:00
陈劲松
889903bb46 Merge branch 'cherry-pick-64f376e2' into 'dev'
CN-1445 fix: 修复切换不同知识库的更新页面后,psiphon3的柱状图不能显示的问题

See merge request cyber-narrator/cn-ui!56
2023-11-01 02:02:15 +00:00
chenjinsong
e12d76e2ad CN-1445 fix: 修复切换不同知识库的更新页面后,psiphon3的柱状图不能显示的问题
(cherry picked from commit 64f376e22a)
2023-11-01 02:02:03 +00:00
陈劲松
29df6ec60e Merge branch 'cherry-pick-5b89fca7' into 'dev'
CN-1404 fix: 去掉其他知识库的update按钮

See merge request cyber-narrator/cn-ui!55
2023-11-01 02:01:33 +00:00
chenjinsong
20857e1203 CN-1404 fix: 去掉其他知识库的update按钮
(cherry picked from commit 5b89fca77c)
2023-11-01 02:00:37 +00:00
刘洪洪
8d4924a421 fix: 1、修复实体搜索偶现参数解析错误的问题;2、去除detections--policy无效打印 2023-10-31 18:08:23 +08:00
刘洪洪
41d2f48766 CN-1442 fix: 修复Detections多个搜索条件时,去除其中一个,再点击左侧filter刚去除那项导致删除参数错误的问题 2023-10-31 17:54:51 +08:00
刘洪洪
f151415de6 fix: detection--policy的trigger去除秒时间选项 2023-10-31 17:19:26 +08:00
hanyuxia
73dec68e23 fix:CN-1377 1.日历插件的国际化;2.分页插件的国际化;3.”域名“国际化 2023-10-31 15:47:50 +08:00
刘洪洪
60e821fb16 fix: 修复detection列表刷新界面后页码重置为1的问题 2023-10-31 14:43:29 +08:00
刘洪洪
4cb4aba707 CN-1442 fix: 修复Detections-Security events模块中,去掉搜索框筛选条件,左侧菜单栏仍勾选相应条件的问题 2023-10-31 14:04:51 +08:00
hanyuxia
ba4893dae1 fix:CN-1377 1.知识库自定义列表,reference列更多进行国际化 2023-10-31 11:26:46 +08:00
hanyuxia
19c42021db fix:1.顶部菜单只第四级菜单显示title信息;2.实体界面去掉输入框中的信息(Enter...); 2023-10-31 10:23:38 +08:00
hyx
d6c9bd0ee2 Merge remote-tracking branch 'origin/dev' into dev 2023-10-30 16:41:51 +08:00
hyx
63fd8b268f CN-1389 用户密码修改时不允许包含#号 2023-10-30 16:41:37 +08:00
chenjinsong
bd1f755612 CN-1425 fix: 修复dashboard下钻后切换顶部最后一级面包屑时会回到下钻前页面的问题 2023-10-30 16:28:33 +08:00
刘洪洪
dd4f5e1fba fix: eventType取消国际化转换 2023-10-30 16:25:55 +08:00
刘洪洪
ed1d994d5e fix: eventType取消国际化转换 2023-10-30 16:17:01 +08:00
刘洪洪
815af776aa fix: 修复policy新建时点击save按钮不生效的问题 2023-10-30 16:05:29 +08:00
chenjinsong
a4da1dbfac fix: 去掉部分控制台打印 2023-10-30 15:38:07 +08:00
hyx
9a3bf1a4af Merge remote-tracking branch 'origin/dev' into dev 2023-10-30 15:08:29 +08:00
hyx
6a196ba3f0 CN-1377 国际化内容补充:1.时间选择器国际化(右侧操作板国家化);2.下钻table中,protocol+ports的国际化;3.知识库两个小标题的国际化数据,中文的内容也是英文,需要分别改为:智能情报学习、WebSketch集成; 2023-10-30 15:08:17 +08:00
刘洪洪
7b8ca90436 fix: 修复policy新建时form的时间提示被盖住,以及限制输入框内容长度的问题 2023-10-30 15:05:48 +08:00
刘洪洪
90827fd706 fix: policy新建时添加小时不得超过24等时间限制 2023-10-30 14:37:00 +08:00
206 changed files with 20731 additions and 3099 deletions

View File

@@ -28,7 +28,8 @@ module.exports = {
'node'
],
moduleNameMapper: {
'@/(.*)$': '<rootDir>/src/$1'
'@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': '<rootDir>/test/__mocks__/styleMock.js'
},
resetMocks: true
}

View File

@@ -23,6 +23,7 @@
"element-plus": "~1.0.2-beta.71",
"lib-flexible": "^0.3.2",
"lodash": "^4.17.21",
"maplibre-gl": "3.6.2",
"mockjs": "^1.1.0",
"moment-timezone": "^0.5.33",
"node-sass": "~4.14.0",

View File

@@ -1,5 +1,25 @@
const BASE_CONFIG = {
baseUrl: 'http://192.168.44.54:8090/',
version: '23.10',
version: '23.12',
apiVersion: 'v1'
}
// 默认时间过滤条件,单位分钟. 0表示请求接口时不传时间参数
const DEFAULT_TIME_FILTER_RANGE = {
dashboard: 60, // 所有dashboard
entity: {
list: 60, // 实体列表
trafficLine: 60, // 实体详情--通用--流量曲线
subscriberKpi: 60, // 实体详情--subscriber--kpi
subscriberTopApp: 60, // 实体详情--subscriber--topApp
subscriberMap: 60, // 实体详情--subscriber--地图
informationAggregation: 0, // 实体详情--通用--信息聚合
relatedEntity: 60 * 24 * 7, // 实体详情--通用--相关实体
openPort: 60 * 24 * 7, // 实体详情--通用--开放端口
deviceInformation: 60 * 24 * 7, // 实体详情--subscriber--设备信息
accountInformation: 60 * 24 * 7, // 实体详情--subscriber--账户信息
securityEvent: 60 * 24 * 7, // 实体详情--通用--安全事件
performanceEvent: 60 * 24 * 7, // 实体详情--通用--性能事件
behaviorPattern: 60 * 24 * 7 // 实体详情--IP--行为模式
},
detection: 60 // 事件日志列表
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -22,15 +22,39 @@
></el-input>
</el-form-item>
<el-form-item>
<el-button
<el-button :disabled="licenseStatus !== 0"
v-loading="loading"
type="primary"
class="login--input login--button"
:class="{'login-btn__license-error':licenseStatus !== 0}"
@click="login"
@keyup.enter="login"
style="font-size: 16px;"
>Login</el-button
>
>Login
</el-button>
</el-form-item>
<el-form-item v-if="licenseStatus !== 0">
<div class="license-error-msg">{{licenseStatusErrMsg}}</div>
<div class="license-file">
<button style="position: relative;" class="license__btn margin-r-20" @click.prevent="downloadFile" @keyup.enter="login">
<i class="cn-icon-download1 cn-icon margin-r-6"></i><span>Download c2v file</span>
</button>
<el-upload :action="`${baseUrl}sys/license/upload`"
ref="licenseUpload"
id="licenseUpload"
:multiple="false"
:show-file-list="false"
:accept="fileTypeLimit"
:file-list="fileList"
:auto-upload="false"
:on-change="fileChange"
:on-success="uploadSuccess"
:on-error="uploadError">
<button style="position: relative;" class="license__btn" @click.prevent="">
<i class="cn-icon-upload1 cn-icon margin-r-6"></i><span>Upload license</span>
</button>
</el-upload>
</div>
</el-form-item>
</el-form>
</div>
@@ -46,6 +70,7 @@ import { api } from '@/utils/api'
import dayjs from 'dayjs'
import _ from 'lodash'
import utc from 'dayjs/plugin/utc'
import { ref } from 'vue'
dayjs.extend(utc)
export default {
@@ -54,7 +79,12 @@ export default {
return {
loading: false,
username: '',
pin: ''
pin: '',
language: '',
licenseStatus: 1,
licenseStatusErrMsg: '',
downloadC2vUrl: api.downloadLicenseC2v,
supportID: ''
}
},
methods: {
@@ -68,6 +98,9 @@ export default {
} else {
return
}
if (this.licenseStatus !== 0) {
return
}
this.loading = true
axios.post(api.login, { username: this.username, pin: this.pin }).then(
res => {
@@ -75,6 +108,9 @@ export default {
if (!_.isEmpty(res.data.data.user.lang)) {
localStorage.setItem(storageKey.language, res.data.data.user.lang)
}
if (!localStorage.getItem(storageKey.language)) {
localStorage.setItem(storageKey.language, this.language)
}
if (!_.isEmpty(res.data.data.user.theme)) {
localStorage.setItem(storageKey.theme, res.data.data.user.theme)
}
@@ -99,6 +135,72 @@ export default {
}
})
},
downloadFile () {
axios.get(this.downloadC2vUrl, { responseType: 'blob' }).then(res => {
const disposition = res.headers['content-disposition']
const fileName = decodeURI(disposition.split('filename=')[1])
if (window.navigator.msSaveOrOpenBlob) {
// 兼容ie11
const blobObject = new Blob([res.data])
window.navigator.msSaveOrOpenBlob(blobObject, fileName)
} else {
const url = URL.createObjectURL(new Blob([res.data]))
const a = document.createElement('a')
document.body.appendChild(a) // 此处增加了将创建的添加到body当中
a.href = url
a.download = fileName
a.target = '_blank'
a.click()
a.remove() // 将a标签移除
}
}, error => {
const $self = this
const reader = new FileReader()
reader.onload = function (event) {
const responseText = reader.result
const exception = JSON.parse(responseText)
if (exception.message) {
$self.$message.error(exception.message)
} else {
console.error(error)
}
}
reader.readAsText(error.response.data)
})
},
fileChange (file, fileList) {
if (file.status !== 'ready') return
if (!_.endsWith(file.name, '.xml')) {
this.fileList = []
this.$message.error('Only support '+ this.fileTypeLimit + ' files')
} else {
this.fileList = fileList.slice(-1)
this.$refs.licenseUpload.submit()
}
},
uploadSuccess (response) {
this.$message.success('Success')
this.licenseStatus = 0
},
uploadError (error) {
let errorMsg
if (error.message) {
errorMsg = JSON.parse(error.message).message
} else {
errorMsg = 'error'
}
this.licenseStatus = 1
this.$message.error('Upload failed: ' + errorMsg)
},
checkLicenseStatus () {
axios.get(api.licenseStatus).then(res => {
if (res.status === 200) {
this.licenseStatus = res.data.data.status
}
}).catch(e => {
this.licenseStatusErrMsg = this.errorMsgHandler(e)
})
},
queryAppearance () {
axios.get(api.appearance).then(res => {
if (res.status === 200) {
@@ -107,6 +209,7 @@ export default {
})
},
appearanceOut (data) {
this.language = data.lang || defaultLang
if (_.isEmpty(localStorage.getItem(storageKey.language))) {
localStorage.setItem(storageKey.language, data.lang || defaultLang)
}
@@ -123,13 +226,17 @@ export default {
localStorage.setItem(storageKey.sysLogo, data.system_logo)
}
},
mounted () {
async mounted () {
this.queryAppearance()
this.checkLicenseStatus()
},
setup (props) {
const { currentRoute } = useRouter()
return {
loginSuccessPath: currentRoute.value.query.redirect
loginSuccessPath: currentRoute.value.query.redirect,
baseUrl: BASE_CONFIG.baseUrl,
fileTypeLimit: '.xml',
fileList: ref([])
}
}
}
@@ -176,7 +283,7 @@ export default {
}
.inside {
width: 414px;
height: 524px;
height: fit-content;/*524*/
background: #0B325C;
border: 1px solid rgba(103,179,245,0.65);
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.38);
@@ -193,6 +300,7 @@ export default {
.title {
margin-top: 65px;
margin-bottom: 44px;
text-align: center;
}
.title > img {
@@ -203,6 +311,39 @@ export default {
justify-content: center;
flex-direction: column;
align-items: center;
.is-disabled {
background: #21B4ED;
color: #FFFFFF;
opacity: 0.6;
}
.license-error-msg {
color:#c73249;
display: flex;
align-items: flex-start;
justify-content: center;
height:40px;
}
.license-file {
display: flex;
flex-direction: row;
height: 100%;
align-items: center;
justify-content: space-between;
.license__btn {
height: 40px;
padding-left:10px;
padding-right:10px;
min-width: 74px;
color: white;
background-color: #21B4ED;
border: none;
border-radius: 4px;
outline: none;
font-size: 14px;
cursor: pointer;
transition: background-color linear .2s, color linear .1s;
}
}
}
:deep .el-form-item {
width: 334px;
@@ -215,9 +356,8 @@ export default {
width: 17px;
font-size: 17px;
}
.login__box .el-form-item:nth-of-type(3) {
margin-top: 25px;
margin-bottom: 65px;
.login__box .el-form-item:nth-child(3){
margin-bottom: 0px;
}
.login--button {
background: #21B4ED;
@@ -229,5 +369,12 @@ export default {
line-height: 22px;
width: 334px;
height: 52px;
margin-top: 25px;
margin-bottom:65px;
}
.login-btn__license-error {
margin-top: 25px;
margin-bottom: 10px;
height:40px;
}
</style>

View File

@@ -3,84 +3,92 @@
height: 100%;
.search__suffixes {
height: 38px;
&.entity-explorer-home {
margin-right: 1px;
color: #3976CB;
&.search__suffixes--text-mode, &.search__suffixes--tag-mode {
.search__suffix:last-of-type {
width: unset;
height: unset;
margin-right: 12px;
background-color: transparent;
.el-icon-search {
color: #3976CB;
}
}
}
}
&.search__suffixes--text-mode, &.search__suffixes--tag-mode {
position: absolute;
display: flex;
top: 10px;
right: 10px;
align-items: center;
top: 1px;
right: 0;
.search__suffix {
// margin-left: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
border-radius: 0 2px 2px 0;
.cn-icon-search-advance, .cn-icon-search-normal, .cn-icon-filter {
color: #A6AAAE;
font-size: 18px;
}
.el-icon-search {
color: #3976CB;
font-size: 22px;
}
&:hover {
cursor: pointer;
}
&:last-of-type {
margin-right: 0;
width: 41px;
height: 38px;
line-height: 39px;
background: #38ACD2;
.el-icon-search {
font-size: 22px;
color: #fff;
}
}
}
.search__suffix-close {
height: 40px;
line-height: 40px;
margin-top: -9px;
.el-icon-error {
font-size: 17px;
color: #C4C4C4;
margin-right: 12px;
cursor: pointer;
}
}
.entity-explorer-search {
color: #3976CB;
margin-top: -2px;
}
.margin-r-12 {
margin-right: 12px;
}
.new-search__suffix {
width: 41px;
height: 41px;
line-height: 41px;
background: #38ACD2;
text-align: center;
margin-top: -10px;
margin-right: -10px;
.el-icon-search {
color: #fff !important;
margin-top: 9px !important;
}
}
.my-popper-class .el-popper__arrow {
display: none;
}
}
&.search__suffixes--tag-mode__block {
background: #fff;
}
}
/*.search-tip--error {
font-size: 14px;
color: #F56C6C;
}*/
}
.advanced-search--show-list .CodeMirror, .advanced-search--show-list .tag-search {
border: none;
.detections {
.tag-search, .CodeMirror {
border: 1px solid #E2E5EC;
}
}
.tag-search {
display: flex;
align-items: center;
height: 40px;
overflow: auto hidden;
border: 1px solid #CECECE;
border-radius: 2px;
padding-left: 10px;
padding-right: 80px;
background-color: white;
border: 1px solid #DEDEDE;
&::-webkit-scrollbar {
width: 8px;
@@ -104,6 +112,7 @@
border-radius: 1px;
cursor: pointer;
transition: all linear .1s;
margin-right: 30px;
&:hover {
background-color: white;
@@ -167,6 +176,9 @@
}
}
}
.entity__search .tag-search {
padding-left: 65px;
}
.el-popover.my-popper-class {
width: auto !important;

View File

@@ -13,11 +13,16 @@
color: #ccc;
}
}
.entity__search {
.CodeMirror {
padding-left: 60px;
}
}
/* PADDING */
.CodeMirror-lines {
padding: 11px 5px; /* Vertical padding around content */
padding: 9px 5px; /* Vertical padding around content */
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
@@ -170,14 +175,18 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
overflow: hidden;
margin-bottom: -50px; margin-right: -50px;
padding-bottom: 50px;
height: 100%;
height: 50px !important;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
z-index: 0;
}
.CodeMirror-scroll::-webkit-scrollbar {
width: 0;
}
.CodeMirror-sizer {
position: relative;
border-right: 50px solid transparent;
width: 3000px; // 避免搜索过长导致换行
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
@@ -351,3 +360,132 @@ div.CodeMirror-dragcursors {
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }
.CodeMirror-hint-active {
background-color: #eaf1f5;
color: #5a90b0;
}
.el-dropdown-menu__item.CodeMirror-hint {
line-height: 20px;
/*font-family: NotoSansSC-Regular;*/
}
.hint-clear {
color: #31739C !important;
}
.hint-title {
margin: 7px 0 3px 0 !important;
/* 禁止选中 样式 */
background: #fff !important;
font-family: NotoSansHans-Medium;
font-size: 12px;
color: #333333 !important;
letter-spacing: 0;
font-weight: 600 !important;
line-height: 12px;
}
.cm-s-eclipse span.cm-string-2 {
//color: #D85512;
}
.CodeMirror-lines {
/*height: 20px;*/
//padding: 3px 0;
}
.in-coder-panel {
margin-left: 2px;
}
.in-coder-panel pre {
height: auto !important;
}
.in-coder-panel .CodeMirror {
/*height: inherit;*/
}
.in-coder-panel input {
font-size: 12px !important;
display: block;
border-color: transparent !important;
padding: 0 0 0 8px !important;
}
.CodeMirror-hints {
font-family: NotoSansSC-Regular !important;
max-width: 800px;
}
.CodeMirror-line {
//font-family: Arial !important;
}
.CodeMirror-hint {
margin: 0;
padding: 0 !important;
line-height: 20px !important;
border-radius: 2px;
white-space: pre;
color: black;
cursor: pointer;
}
.cm-variable-2{
color: #164 !important;
}
.default-tips-header,.default-tips-title {
height: 24px;
line-height: 24px;
font-weight: 700;
color: #333333;
font-size: 14px;
margin: 6px 0;
font-family: NotoSansSC-Bold;
letter-spacing: 0;
}
.default-tips-header {
font-size: 15px;
}
.show-hint-tips__p {
word-break: keep-all;
margin: 0;
line-height: 24px;
color: #575757;
font-weight: 400;
}
.Hint {
padding: 0;
z-index: 2;
.hint__block {
display: flex;
flex-direction: row;
width: calc(100% - 41px);
min-height: 298px;
margin-top: 6px;
box-shadow: 0 2px 8px 0 rgba(0,0,0,.3);
z-index: 2;
.hint__block-filter {
width: 319px;
background: #fff;
border-right: 1px solid #DEDEDE;
padding: 12px;
z-index: 2;
}
.hint__block-helper {
width: calc(100% - 319px);
background: #fff;
z-index: 2;
}
}
}

View File

@@ -206,9 +206,20 @@
}
.my-date-picker {
z-index: 100004 !important;
.el-popper__arrow {
position: absolute;
top: 20px !important;
left: 642px !important; // element上样式设定是left添加right不生效
}
}
.my-date-picker__left {
.el-popper__arrow {
position: absolute;
top: 20px !important;
left: -6px !important;
}
.el-popper__arrow::before {
border: 1px solid #E7EAED !important;
}
}

View File

@@ -1,9 +1,8 @@
.error-component {
position: absolute;
//width: 100%;
//height: 100%;
left: 0;
top: 0;
z-index: 1000;
}
.error-block {
display: inline-block;

View File

@@ -5,6 +5,7 @@
&>div {
height: 100%;
}
overflow-y: auto;
}
.cn-header {

View File

@@ -80,9 +80,17 @@
@import 'views/administration/AdministrationTabs';
@import 'views/administration/Appearance.scss';
@import 'views/administration/License.scss';
@import 'views/system/Plugin';
@import 'views/setting/knowledgeBase';
@import 'views/charts2/entityDetailLine';
@import 'views/charts2/EntityDetailSubscriberKpi.scss';
@import 'views/charts2/EntityDetailSubscriberTopApp.scss';
@import 'views/charts2/entityDetailSubscriberMap.scss';
@import 'views/charts2/entityDetailSubscriberLine.scss';
@import 'views/charts2/entityDetailTabs';
@import 'views/charts2/digitalCertificate';
@import 'views/charts2/entityDetailBasicInfo';

View File

@@ -0,0 +1,61 @@
.license{
height: 100%;
.license-form {
padding-top:40px;
padding-left:100px;
background-color: white;
position: relative;
display: flex;
flex-direction: column;
height: 100%;
.license-file {
display: flex;
flex-direction: row;
height: 100%;
margin-top: 3px;
.license__btn {
height: 30px;
min-width: 74px;
color: white;
background-color: #38ACD2;
border: none;
border-radius: 4px;
outline: none;
font-size: 14px;
cursor: pointer;
transition: background-color linear .2s, color linear .1s;
}
.license__btn:hover:not(.footer__btn--disabled) {
background-color: lighten(#38ACD2, 10%);
}
.license__btn--light {
background-color: #F5F6F7;
border: 1px solid $--border-color-primary;
color: #333;
}
.license__btn.license__btn--light:hover:not(.license__btn--disabled) {
background-color: white;
border-color: lighten(#38ACD2, 40%);
color: #38ACD2;
}
.license__btn--disabled {
opacity: .6;
cursor: default;
}
}
.el-form .el-form-item {
margin-bottom: 0;
padding:4px 0;
font-size: 14px!important;
.el-form-item__label {
color: #262626 !important;
font-weight: bold;
}
.el-form-item__content {
color: #262626 !important;
font-weight: 400;
}
}
}
}

View File

@@ -0,0 +1,94 @@
.subscriber-kpi {
height: 100%;
.subscriber-kpi-header {
height:34px;
padding-bottom:10px;
font-family: NotoSansHans-Medium;
font-size: 14px;
color: #353636;
font-weight: 500;
display:flex;
align-items: center;
justify-content: space-between;
.subscriber-kpi-title {
height:24px;
overflow: hidden;
}
}
.subscriber-kpi-body {
border: 1px solid #E2E5EC;
border-radius: 4px;
height:calc(100% - 34px);
.subscriber-kpi-content {
height: calc(100% - 36px);
padding: 20px 0 20px 20px;
display: flex;
flex-direction: column;
.panel-chart__no-data {
height: calc(100% - 46px);
}
.kpi-type {
display: flex;
flex-direction: column;
justify-content:space-between;
height: calc(100% - 65px);
.kpi-type-value {
display: flex;
flex-direction: column;
padding-bottom:20px;
.kpi-type-value-name {
line-height: 12px;
margin-bottom: 10px;
font-size: 14px;
color: #575757;
font-weight: 400;
}
.kpi-type-data {
display:flex;
flex-direction: row;
.kpi-type-value-number {
white-space: nowrap;
font-family: Helvetica-Bold;
font-size: 20px;
color: #353636;
font-weight: 700;
}
.data-trend {
display: flex;
width: 50%;
.data-total-trend {
display: flex;
align-items: center;
margin-left: 6px;
justify-content: center;
margin-top: 2px;
border-radius: 10px;
font-weight: 500;
font-size: 12px;
height: 20px;
padding: 0 5px;
}
.data-total-trend-black {
background-color: rgba(113,113,113,0.12);
color: #717171;
width: 36px;
}
.data-total-trend-green {
background-color: rgba(126,159,84,0.12);
color: #7E9F54;
}
.data-total-trend-red {
background-color: rgba(226,97,84,0.12);
color: #E26154;
.cn-icon-rise1{
color: #E44D3E;
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,113 @@
.subscriber-top-app {
height: 100%;
.subscriber-top-app-header {
height:34px;
padding-bottom:10px;
font-family: NotoSansHans-Medium;
font-size: 14px;
color: #353636;
font-weight: 500;
display:flex;
align-items: center;
justify-content: space-between;
.subscriber-top-app-title {
height:24px;
overflow: hidden;
}
}
.subscriber-top-app-body {
border: 1px solid #E2E5EC;
border-radius: 4px;
height:calc(100% - 34px);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 20px 20px 20px;
.panel-chart__no-data {
height: calc(100% - 46px);
}
.top-app-left {
height:100%;
display: flex;
flex-direction: column;
margin-right:15px;
.app-data {
display: flex;
flex-direction: row;
align-items: center;
font-size: 14px;
color: #353636;
font-weight: 400;
height:calc(100%/10);
.app-index {
text-align: right;
width:20px;
margin-right:15px;
}
.app-name {
width:80px;
margin-right:10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.top-app-divider {
height:10px;
background: #717171;
margin-left:10px;
margin-right:8px;
}
.app-trend-right {
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
width:50px;
i {
margin-right:3px;
font-size:12px;
color: #717171;
}
}
.app-trend-left {
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
width:60px;
i {
margin-right:3px;
font-size:12px;
color: #717171;
}
}
.app-up {
font-size: 12px;
color: #717171;
letter-spacing: -0.2px;
text-align: center;
font-weight: 400;
}
.app-down {
font-size: 12px;
color: #717171;
letter-spacing: -0.2px;
text-align: center;
font-weight: 400;
}
}
}
.top-app-right {
height: 100%;
width:calc(100% - 248px);
position: relative;
.chart-content {
height: 100%;
width: 100%;
}
}
}
}

View File

@@ -0,0 +1,51 @@
.entity-detail-line--subscriber {
$blue: #046ECA;
$grey: #353636;
height:100%;
.el-tabs__content {
overflow: visible;
}
.cn-chart__tabs {
height:100%;
.tab-pane {
height:100%;
}
.el-tabs__header {
margin-bottom: 10px;
width: calc(100% - 272px);
}
.el-tabs__nav-wrap::after {
height: 1px;
background-color: transparent ;
}
.el-tabs__nav.is-top {
height: 33px;
.el-tabs__active-bar {
background-color: $blue;
}
.el-tabs__item {
padding: 0 10px;
height: 33px;
color: $grey;
font-size: 14px;
&.el-tabs__item.is-top.is-active {
color:$blue;
}
&:nth-child(2) {
padding-left: 0;
}
}
}
.el-tabs__content {
height: calc(100% - 40px);
border:none;
.el-table__body-wrapper {
height: calc(100% - 45px) !important;
}
}
}
}

View File

@@ -0,0 +1,70 @@
.subscriber-map {
height: 100%;
.subscriber-map-header {
height: 34px;
padding-bottom: 10px;
font-size: 14px;
color: #353636;
display: flex;
align-items: center;
justify-content: space-between;
.subscriber-map-title {
height: 24px;
overflow: hidden;
}
}
.subscriber-map-body {
position: relative;
border: 1px solid #E2E5EC;
border-radius: 4px;
height: calc(100% - 34px);
.subscriber-map {
height: 100%;
width: 100%;
.maplibregl-canvas:focus-visible {
outline: none;
}
}
.panel-chart__no-data {
height: calc(100% - 46px);
}
}
.subscriber-map-point-tooltip {
position: fixed;
background-color: white;
width: 200px;
border: 1px solid #C5C5C5;
box-shadow: -1px 1px 10px -1px rgba(205,205,205,0.85);
border-radius: 2px;
.subscriber-map-point-tooltip__time {
padding: 10px;
font-size: 12px;
color: #353636;
font-weight: bold;
}
.subscriber-map-point-tooltip__coordinates {
padding: 0 10px 10px;
.subscriber-map-point-tooltip__coordinate {
display: flex;
.coordinate__label {
width: 115px;
font-size: 12px;
color: #575757;
}
.coordinate__value {
font-size: 12px;
color: #353636;
font-weight: bold;
}
}
}
}
}

View File

@@ -155,6 +155,14 @@
margin-left: 8px;
}
.entity-subscriber-detail-error {
margin-top: 0px;
margin-left: 0px;
.error-block {
margin:0px;
}
}
.entity-detail-performance {
height: 46px;
border-radius: $tab-border-radius;

View File

@@ -88,7 +88,8 @@ $blue: #046ECA;
.item-popover-header {
display: flex;
align-items: center;
line-height: 32px;
padding: 10px 0;
line-height: 14px;
font-size: 14px;
font-weight: 600;

View File

@@ -52,11 +52,30 @@
display:flex;
flex-direction:column;
align-items: flex-start;
color: #778391;
font-weight: 400;
.data-column__span {
font-weight: bold;
color:#353636;
}
.unit__span {
color: #778391;
font-weight: 400;
}
}
.column-loading {
display: flex;
flex-direction: row;
position:relative;
.loading-icon {
position:absolute;
left: -20px;
transform: translateZ(0) scale(0.5);
width:20px;
height:20px;
margin-right:3px;
}
}
.tab-table {
border: 1px solid #E2E5EC;
border-radius: 4px 4px 0 0;

View File

@@ -15,8 +15,7 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 0 20px 20px;
padding: 20px;
.panel__title {
font-size: 24px;

View File

@@ -194,12 +194,13 @@
}
}
.form-setting__btn, .form-setting__btn1 {
.form-setting__btn, .form-setting__btn1, .policy-form__footer__btn {
width: 100%;
display: flex;
justify-content: flex-end;
.el-button {
width: 80px !important;
height: 30px !important;
min-height: 30px !important;
line-height: 30px !important;
@@ -222,8 +223,15 @@
}
}
}
.policy-form__footer__btn {
justify-content: center;
margin-top: 8px;
.form-setting__btn1 {
.btn1 {
margin-right: 16px;
}
}
.form-setting__btn1, .policy-form__footer__btn {
.el-button {
padding: 0 11px !important;
}

View File

@@ -10,9 +10,9 @@
}
.detection-form-content {
height: 100%;
height: calc(100% - 92px);
overflow: scroll;
padding-bottom: 40px;
padding-bottom: 20px;
.detection-form-collapse {
margin-top: 20px;
@@ -101,6 +101,12 @@
justify-content: flex-start;
height: 24px;
line-height: 24px;
.policy-form-item {
.el-form-item__error {
width: 260px;
}
}
}
.el-input--mini .el-input__inner {
@@ -124,6 +130,12 @@
}
}
}
.policy-form-trigger {
.el-collapse-item__content {
padding-bottom: 0 !important;
}
}
}
.el-input--mini, .el-input--mini .el-input__inner {
@@ -137,4 +149,13 @@
padding-bottom: 20px;
}
.policy-form__footer {
width: calc(100% + 40px);
height: 60px;
margin-left: -20px;
box-shadow: 0 -1px 4px 0 rgba(0,0,0,0.10);
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@@ -34,6 +34,15 @@
.drawer-basic-id {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
i {
font-size: 14px;
font-weight: 400;
cursor: pointer;
}
}
}
}

View File

@@ -2,98 +2,124 @@
display: flex;
flex-direction: column;
width: 280px;
padding: 10px;
margin-right: 10px;
background-color: white;
margin-right: 12px;
overflow: auto;
z-index: 1;
border: 1px solid rgba(226, 229, 236, 1) !important;
border-radius: 4px !important;
.detection-filter {
display: flex;
flex-direction: column;
margin-bottom: 10px;
.filter__header {
display: flex;
flex: 0 0 32px;
align-items: center;
padding-left: 10px;
color: #666;
//background-color: #F3F7FA;
cursor: pointer;
span {
font-size: 14px;
padding-left: 6px;
}
i {
font-size: 12px;
transition: all linear .1s;
transform: rotate(0) translate(0, 2px);
}
i.arrow-rotate {
transform: rotate(90deg) translate(2px, 3px);
}
.new-detection-filter-header-title {
font-size: 14px;
color: #353636;
font-weight: 600;
}
.new-detection-filter-icon {
margin-left: 8px;
margin-bottom: 2px;
font-weight: bold !important;
}
}
.filter__body {
padding: 5px 0 0 15px;
.el-checkbox-group {
display: flex;
flex-direction: column;
.el-checkbox {
display: flex;
align-items: center;
padding: 5px 0;
margin-right: 5px;
.el-checkbox__label {
width: 100%;
}
.filter__checkbox-label {
display: flex;
justify-content: space-between;
align-items: center;
.severity-color-block {
width: 4px;
height: 15px;
border-radius: 2px;
}
}
&:last-of-type {
padding-bottom: 0;
}
}
}
}
.filter-case__header {
padding-left: 8px;
height: 36px;
line-height: 36px;
color: #666;
font-size: 14px;
background: #F7F7F7;
box-shadow: 0 1px 0 0 rgba(226,229,236,1);
border-radius: 4px 4px 0 0;
}
.new-detection-filter-title {
display: flex;
flex: 0 0 32px;
align-items: center;
padding-left: 27px;
background-color: #EFF2F5;
cursor: pointer;
.filter__header {
height: 46px;
line-height: 46px;
margin: 0 20px;
font-size: 14px;
color: #353636;
font-weight: 600;
margin: -10px;
margin-bottom: 10px;
font-weight: 500;
}
.filter__body {
width: calc(100% - 30px);
margin: 0 10px 0 20px;
max-height: 265px;
overflow-y: scroll;
overflow-x: hidden;
.filter__body-item {
height: 26px;
line-height: 26px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.filter__body-item-left {
display: flex;
align-items: center;
font-size: 14px;
color: #353636;
font-weight: 400;
.filter__body-item-left-index {
width: 16px;
height: 16px;
text-align: center;
background: #EFF2F5;
border-radius: 2px;
margin-right: 6px;
font-family: NotoSansHans-Black;
font-size: 9px;
color: #96A2B0;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
}
.filter__body-item-left-label {
max-width: 180px;
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #353636;
font-weight: 400;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.filter__body-item-right {
flex-shrink: 0;
font-family: NotoSansSChineseRegular;
font-size: 12px;
color: #717171;
font-weight: 400;
margin-right: 10px;
}
}
}
}
.filter-country-flag {
width: 18px;
height: 12px;
margin-right: 6px;
border: 1px solid #E8E8E8;
}
.filter-show-more, .filter-no-show-more {
cursor: pointer;
height: 26px;
line-height: 26px;
margin-left: 20px;
color: #046ECA;
user-select: none; // 禁止文本选中
font-size: 12px;
}
.filter-no-show-more {
cursor: not-allowed;
color: rgba(16, 16, 16, 0.3);
}
.filter-hr {
width: calc(100% - 40px);
margin-left: 20px;
margin-top: 6px;
height: 1px;
background: #EFF2F5;
//background: #000;
}
.new-detection-filter-title {
height: 32px;
line-height: 32px;

View File

@@ -149,7 +149,7 @@
flex-direction: row;
flex-wrap: wrap;
.basic-info__item {
.basic-info__item, .basic-info__item1 {
padding-right: 30px;
display: flex;
align-items: center;
@@ -172,6 +172,11 @@
color: #666;
}
}
.basic-info__item1 {
span: {
color: #666;
}
}
}
.show-detail {

View File

@@ -37,7 +37,7 @@
flex-direction: row;
align-items: flex-start;
flex-wrap: nowrap;
padding: 2px 10px 2px 0;
padding: 6px 10px 6px 0;
font-size: 14px;
word-break: break-all;
@@ -45,6 +45,8 @@
padding-right: 20px;
min-width: 100px;
color: #6B717B;
line-height: 14px;
word-break: normal;
}
.row__charts {
@@ -68,6 +70,7 @@
font-size: 14px;
align-items: center;
flex-wrap: wrap;
line-height: 14px;
&.row__content--link {
font-style: italic;
@@ -203,6 +206,7 @@
color: #046ECA;
margin-bottom: 10px;
font-weight: 500;
height: 36px;
}
.timeline__start-time {
font-size: 12px;

View File

@@ -1,4 +1,3 @@
.detection-table {
.el-table th > .cell, .el-table .cell {
padding-left: 0 !important;
@@ -32,7 +31,37 @@
height: 32px !important;
}
}
.policy-library-tip {
max-width: 180px;
padding: 4px;
.tip__header {
color: #353636;
font-weight: bold;
font-size: 14px;
}
.tip__tags {
display: flex;
margin-top: 8px;
.tip__tag {
margin-right: 10px;
padding: 2px 10px;
background-color: #EBF7FA;
color: #046ECA;
box-shadow: 0 2px 4px 0 rgba(51,51,51,0.02);
border-radius: 12px;
}
}
.tip__description {
margin-top: 14px;
color: #666;
&.tip__description--non {
color: #999;
}
}
}
.detection-tag-blue, .detection-tag-red, .detection-tag-gray, .detection-tag-status0, .detection-tag-status1 {
display: inline-block;
border-radius: 10px;

View File

@@ -26,7 +26,7 @@
justify-content: flex-start;
}
.explorer-top-tools, .explorer-detection-top-tools {
.explorer-top-tools, .explorer-detection-top-tools, .explorer-entity-top-tools {
display: flex;
justify-content: flex-end;
align-items: center;
@@ -46,7 +46,10 @@
}
}
}
.explorer-detection-top-tools {
.explorer-entity-top-tools {
width: 100%;
}
.explorer-detection-top-tools, .explorer-entity-top-tools {
display: flex;
justify-content: space-between;
}
@@ -87,6 +90,13 @@
font-size: 14px;
color: #353636;
font-weight: 400;
.entity-hide-entity {
margin-left: 20px;
.el-checkbox__label {
padding-left: 6px;
}
}
}
.explorer-container, .explorer-container-new {
display: flex;

View File

@@ -2,7 +2,7 @@
display: flex;
flex-direction: column;
width: 320px;
margin-right: 20px;
margin-right: 12px;
overflow: auto;
z-index: 1;
border: 1px solid rgba(226, 229, 236, 1) !important;

View File

@@ -351,6 +351,7 @@
}
.score-dot {
display: inline-block;
margin-bottom: 2px;
width: 6px;
height: 6px;
border-radius: 50%;

View File

@@ -43,7 +43,7 @@
align-content: center;
padding: 16px 0;
margin-bottom: 1px;
background-color: white;
//background-color: white;
border-radius: 0 4px 4px 0;
.cn-entity__icon {
@@ -79,11 +79,22 @@
.cn-entity__header-title {
margin-right: 10px;
}
.cn-entity__header-tag {
.entity-related-entity {
font-size: 12px;
color: #717171;
cursor: pointer;
margin-right: 6px;
}
}
.entity-row-tag {
display: flex;
margin-left: 6px;
margin-top: 1px;
flex-wrap: wrap;
margin-bottom: -10px;
}
.cn-entity__body {
display: flex;
flex-direction: column;
@@ -98,7 +109,7 @@
flex-direction: row;
flex-wrap: wrap;
.basic-info__item {
.basic-info__item, .basic-info__item1 {
padding-right: 30px;
.item__box {
@@ -161,6 +172,17 @@
color: #666;
}
}
.basic-info__item1 {
span: {
color: #666;
}
span:first-of-type {
color: #666;
}
.row-item-label {
color: #999 !important;
}
}
.row-item-label {
font-family: NotoSansSChineseRegular;
@@ -177,6 +199,7 @@
.score-dot {
display: inline-block;
margin-bottom: 2px;
width: 6px;
height: 6px;
border-radius: 50%;

View File

@@ -26,7 +26,8 @@
padding: 0 20px;
&.explorer-search__input-case--question-mark-in-line {
flex-direction: row;
//flex-direction: row;
flex-direction: column;
padding: 0;
.explorer-search__input {
@@ -34,9 +35,8 @@
max-width: unset;
}
.explorer-search__input__border {
border: 1px #DEDEDE solid;
height: 43px;
.explorer-search__input--border .CodeMirror {
border: 1px solid #DEDEDE;
}
}
.search-symbol-inline {
@@ -53,14 +53,14 @@
max-width: 1000px;
height: 40px;
}
.explorer-search__foot {
.explorer-search__foot,.explorer-search__foot-list {
display: flex;
padding-top: 18px;
padding-top: 9px;
width: 100%;
max-width: 1000px;
position: relative;
justify-content: space-between;
font-weight: bold;
justify-content: flex-start;
//font-weight: bold;
.foot__item {
display: flex;
@@ -87,14 +87,15 @@
.search__history {
position: absolute;
top: 6px;
display: flex;
padding: 10px 0 0 0;
flex-direction: column;
width: 100%;
max-width: 1000px;
z-index: 2;
top: 47px;
border: 1px solid rgba(206,206,206,0.20);
//top: 47px;
border: 1px solid rgba(226,229,236,1);
border-radius: 2px;
background-color: white;
@@ -137,8 +138,32 @@
color: #66b1ff;
}
}
.history__items-new {
max-height: 300px;
overflow: auto;
.el-table th,.el-table td {
padding: 6px 0;
border: none !important;
}
.el-table {
font-family: NotoSansSChineseRegular;
font-size: 14px;
color: #575757;
font-weight: 400;
thead {
font-family: NotoSansHans-Medium;
font-size: 14px;
color: #353636;
font-weight: 500;
}
.cell {
padding-left: 18px;
}
}
}
.clear-all {
padding-left: 30px;
padding-left: 18px;
font-weight: normal;
font-size: 14px;
height: 35px;
@@ -152,6 +177,71 @@
}
}
}
.explorer-search__foot-list {
max-width: 756px;
position: absolute;
left: 196px;
top: 44px;
.search__history {
top: 6px;
max-width: 756px;
margin-left: -196px;
.history__items-new {
max-height: 300px;
overflow: auto;
.el-table th,.el-table td {
padding: 4px 0;
border: none !important;
}
.el-table {
font-size: 12px;
thead {
font-size: 12px;
}
}
.el-table--scrollable-x .el-table__body-wrapper {
overflow-x: hidden;
}
}
}
}
}
}
.highlight__text {
background: #FEECC2;
padding: 0 3px;
}
.highlight__block {
background: #FEECC2;
}
.explorer-search__foot-list .explorer-search__block {
margin-left: -196px;
top: -44px;
}
.explorer-search__foot .explorer-search__block {
margin-left: 0;
top: -40px;
}
.explorer-search__block {
position: absolute;
padding-left: 15px;
height: 39px;
display: flex;
align-items: center;
cursor: pointer;
z-index: 1;
i {
font-size: 20px;
color: #999;
}
.search-dividing-line {
width: 1px;
height: 26px;
background: #DEDEDE;
margin-left: 15px;
margin-top: 2px;
}
}

View File

@@ -1798,6 +1798,12 @@
margin-right:5px;
}
}
.top-tool-btn--update:disabled {
cursor: not-allowed;
opacity: 0.66;
i {
}
}
.top-tool-btn--update:hover {
background-color: #57B8D9 !important;
border-color: #2E88A6 !important;
@@ -1846,6 +1852,12 @@
margin-right:5px;
}
}
.top-tool-btn--update:disabled {
cursor: not-allowed;
opacity: 0.66;
i {
}
}
.top-tool-btn--update:hover {
background-color: #57B8D9 !important;
border-color: #2E88A6 !important;
@@ -1905,6 +1917,9 @@
&.update-dialog__table--psiphon3 {
height: calc(90vh - 190px - 200px - 50px - 10px);
}
&.update-dialog__table--system-user {
height: calc(100% - 139px);
}
}
.update-knowledge-form {
.el-upload {

View File

@@ -0,0 +1,61 @@
.plugin-management .list-page {
.cn-table {
height: 100%;
.el-table {
height: 100%;
}
.el-table--group::after,.el-table--border::after, .el-table::before {
height: 0;
}
}
.cn-pagination {
display: none;
}
}
.plugin {
font-size: 12px;
color: #353636;
line-height: 14px;
font-weight: 400;
.type-tag {
display: inline-block;
padding: 0 10px;
background-color: #EBF7FA;
color: #046ECA;
box-shadow: 0 2px 4px 0 rgba(51,51,51,0.02);
border-radius: 12px;
margin-right:10px;
}
.plugin-name {
display: flex;
flex-direction: row;
justify-content: left;
align-items: center;
.icon-background {
display:flex;
justify-content: center;
align-items: center;
width:32px;
height:32px;
background: #ECECEC;
border-radius: 4px;
margin-right:6px;
.plugin-name-icon {
width:25px;
height:25px;
color:red;
display:flex;
justify-content: center;
align-items: center;
}
}
}
.two-line {
overflow: hidden; //超出的文本隐藏
text-overflow: ellipsis; //溢出用省略号显示
display: -webkit-box;
line-clamp:2 ;
-webkit-line-clamp: 2; // 超出多少行
-webkit-box-orient: vertical;
}
}

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "cn-icon"; /* Project id 2614877 */
src: url('iconfont.woff2?t=1698229141457') format('woff2'),
url('iconfont.woff?t=1698229141457') format('woff'),
url('iconfont.ttf?t=1698229141457') format('truetype');
src: url('iconfont.woff2?t=1706606024800') format('woff2'),
url('iconfont.woff?t=1706606024800') format('woff'),
url('iconfont.ttf?t=1706606024800') format('truetype');
}
.cn-icon {
@@ -13,6 +13,66 @@
-moz-osx-font-smoothing: grayscale;
}
.cn-icon-license:before {
content: "\e666";
}
.cn-icon-base-station:before {
content: "\e6cf";
}
.cn-icon-home:before {
content: "\e6d0";
}
.cn-icon-company:before {
content: "\e6d1";
}
.cn-icon-pedestrian:before {
content: "\e6d2";
}
.cn-icon-system:before {
content: "\e6cc";
}
.cn-icon-plugin:before {
content: "\e6cd";
}
.cn-icon-IMSI:before {
content: "\e812";
}
.cn-icon-APN:before {
content: "\e813";
}
.cn-icon-shoujihaoma:before {
content: "\e814";
}
.cn-icon-IMEI:before {
content: "\e811";
}
.cn-icon-trace-point:before {
content: "\e810";
}
.cn-icon-account-info:before {
content: "\e80e";
}
.cn-icon-device-info:before {
content: "\e80f";
}
.cn-icon-related:before {
content: "\e640";
}
.cn-icon-indicator-match:before {
content: "\e80c";
}
@@ -333,7 +393,7 @@
content: "\e7b4";
}
.cn-icon-a-GeneralSettings:before {
.cn-icon-general-setting:before {
content: "\e7b5";
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,16 +5,20 @@
@mouseleave="showCloseIcon = false"
>
<text-mode
test-id="text-mode"
v-if="searchMode === 'text'"
ref="textMode"
:column-list="columnList"
:str="str"
:show-list="showList"
:is-show-hint="showHint"
:unit-test-str="unitTestStr"
@changeMode="changeMode"
@search="search"
:show-close-icon="showCloseIcon"
></text-mode>
<tag-mode
test-id="tag-mode"
v-if="searchMode === 'tag'"
ref="tagMode"
:column-list="columnList"
@@ -47,6 +51,11 @@ export default {
showCloseIcon: false
}
},
provide () {
return {
myHighLight: !this.noHighlight
}
},
props: {
// 默认模式tag | text
defaultMode: String,
@@ -64,11 +73,22 @@ export default {
required: true
},
// 连接符列表
connectionList: Array
connectionList: Array,
showHint: {
type: Boolean,
default: false
},
noHighlight: {
type: Boolean,
default: false
}
},
emits: ['search'],
methods: {
search (parseData) {
if (this.isUnitTesting) {
this.unitTestParam = parseData
}
this.$emit('search', parseData)
},
changeMode (mode, { str, metaList }) {

View File

@@ -17,7 +17,7 @@
size="mini"
v-model="meta.column.label"
ref="columnSelect"
:placeholder="meta.column.label || ''"
:placeholder="meta.column.label || ' '"
@blur="columnBlur(meta, index)"
@change="(value) => selectColumn(value, meta)"
>
@@ -40,7 +40,58 @@
<div v-if="meta.operator.value === 'has'" class="condition__operator" style="color: #000C18">({{meta.column.label}},</div>
<!-- -->
<div class="condition__value">
<div v-if="meta.value.isEditing">
<!--点击=操作符时单个选择枚举值-->
<div v-if="meta.value.isEditing && meta.doc && meta.column.type!==columnType.array">
<el-select
allow-create
filterable
size="mini"
v-model="meta.value.value"
ref="columnValue"
:placeholder="meta.value.value || ' '"
@blur="valueBlur1(meta, index)"
@change="(value) => selectValue(value, meta)"
>
<el-option
v-for="(code, i) in meta.doc.data"
:key="i"
:label="code.code"
:value="code.code"
></el-option>
</el-select>
</div>
<!--点击 in 操作符时多个选择枚举值-->
<div v-if="meta.value.isEditing && meta.doc && meta.column.type===columnType.array">
<el-select
v-model="myCheckboxList"
ref="valuesSelect"
multiple
size="mini"
collapse-tags
collapse-tags-tooltip
:placeholder="meta.value.value || ' '"
@blur="valuesBlur(meta, index)"
@focus="valuesFocus(meta)"
@change="(value) => selectValues(value, meta)"
@visible-change="(value) => selectVisibleValues(value, meta)"
popper-class="my-select-class"
>
<template #label>
{{ meta.value.value }}
</template>
<template #default>
<el-option
v-for="item in meta.doc.data"
:key="item.code"
:label="item.code"
:value="item.code"
/>
</template>
</el-select>
</div>
<div v-if="meta.value.isEditing && !meta.doc">
<!--避免blur事件和keyup.enter重复执行-->
<el-input
ref="valueInput"
@@ -90,25 +141,25 @@
</template>
</div>
<div class="tag-search__add" @click="addCondition">{{$t('entities.advancedSearch.add')}}</div>
<div class="search__suffixes search__suffixes--tag-mode">
<div class="search__suffix" style="margin-right: 12px">
<div class="search__suffixes search__suffixes--tag-mode search__suffixes--tag-mode__block" :class="showList ? '' : 'entity-explorer-home'">
<span class="search__suffix">
<el-popover
popper-class="my-popper-class"
placement="top"
trigger="hover"
:content="$t('entity.switchToBasicSearch')"
:content="$t('overall.switchToText')"
>
<template #reference>
<i class="cn-icon cn-icon-search-normal" @click="changeMode"></i>
</template>
</el-popover>
</div>
<div v-show="metaList.length>0" class="search__suffix-close" @click="cleanMetaList">
</span>
<span v-show="metaList.length>0" class="search__suffix search__suffix-close" @click="cleanMetaList">
<i class="el-icon-error"></i>
</div>
<div class="search__suffix" :class="showList ? 'new-search__suffix' : 'entity-explorer-search'" @click="search">
</span>
<span test-id="tag-search" class="search__suffix" @click="search">
<i class="el-icon-search"></i>
</div>
</span>
</div>
</div>
</template>
@@ -119,23 +170,28 @@ import _ from 'lodash'
import { handleErrorTip } from '@/components/advancedSearch/meta/error'
import Parser, { stringInQuot } from '@/components/advancedSearch/meta/parser'
import { overwriteUrl, urlParamsHandler } from '@/utils/tools'
import { enumerateData } from '@/utils/static-data'
export default {
name: 'TagMode',
props: {
columnList: Array,
connectionList: Array,
convertMetaList: Array,
showList: Boolean
showList: Boolean,
unitTestStr: String
},
data () {
return {
condition,
connection,
metaList: [],
columnType,
myCheckboxList: [],
operatorList: [] // 操作符列表根据所选columnList的label来确定一般为=,INtags操作符为has
}
},
emits: ['changeMode', 'search'],
inject: ['myHighLight'],
methods: {
// 新增条件
addCondition (meta) {
@@ -202,6 +258,7 @@ export default {
if (this.isCustomized(value)) {
meta.column.type = columnType.fullText
meta.column.label = value
meta.column.isFullText = true
meta.resetOperator()
meta.resetValue()
} else {
@@ -217,19 +274,61 @@ export default {
if (!meta.operator.value) {
meta.operator.isEditing = true
meta.operator.show = true
} else {
// 切换column清除上次column选择的value包含枚举的则删除in操作符的让类型回归array
meta.value.value = ''
meta.value.label = ''
meta.value.isEditing = true
meta.value.show = true
const obj = enumerateData.find(d => d.name === meta.column.label)
if (obj) {
meta.doc = obj
if (this.$refs.valuesSelect) {
// 触发focus后select弹窗并没有生效
this.$refs.valuesSelect[0].focus(meta)
}
} else {
delete meta.doc
}
if (meta.operator.value.toLowerCase() === 'in') {
meta.column.type = columnType.array
}
}
}, 200)
}
},
selectValue (value, meta) {
const isWrapped = this.isSingleQuoteWrapping(value)
meta.value.value = isWrapped && meta.column.type === columnType.string ? value : `'${value}'`
meta.value.label = isWrapped && meta.column.type === columnType.string ? value : `'${value}'`
setTimeout(() => {
meta.column.isEditing = false
meta.value.isEditing = false
}, 100)
},
selectVisibleValues (value, meta) {
if (!value) {
meta.value.isEditing = false
this.myCheckboxList = []
}
},
selectValues (value, meta) {
if (value.length > 0) {
let str = ''
value.forEach(item => {
str += `'${item}',`
})
str = str.substring(0, str.length - 1)
str = `(${str})`
meta.value.value = value
meta.value.label = str
} else {
meta.value.value = ''
}
},
selectConnection (value, meta) {
meta.isEditing = false
},
columnClick (meta) {
meta.column.isEditing = true
this.$nextTick(() => {
this.$refs.columnSelect[this.$refs.columnSelect.length - 1].focus()
})
},
columnBlur (meta, index) {
setTimeout(() => {
const parser = new Parser(this.columnList)
@@ -247,26 +346,64 @@ export default {
this.operatorList = obj ? obj.doc.constraints.operator_functions.split(',') : ['=', 'IN']
if (meta.column && meta.column.type === 'fullText') {
meta.operator.value = '='
meta.column.show = false
meta.operator.show = false
meta.column.show = true
meta.column.isFullText = true
meta.operator.show = true
const label = JSON.parse(JSON.stringify(meta.column.label))
meta.column.label = parser.getEntityTypeByValue(meta.column.label)
meta.value.value = label
meta.value.label = label
// if (meta.column.label === 'domain') {
// meta.operator.value = 'like'
// meta.value.value = `%${this.delSingleQuote(label)}`
// meta.value.label = `${this.delSingleQuote(label)}`
// } else if (meta.column.label === 'app') {
// meta.operator.value = 'like'
// meta.value.value = `%${this.delSingleQuote(label)}%`
// meta.value.label = `${this.delSingleQuote(label)}`
// }
if (meta.column.label === 'domain') {
meta.operator.value = 'like'
meta.value.value = `%${this.delSingleQuote(label)}`
meta.value.label = `%${this.delSingleQuote(label)}`
} else if (meta.column.label === 'app') {
meta.operator.value = 'like'
meta.value.value = `%${this.delSingleQuote(label)}%`
meta.value.label = `%${this.delSingleQuote(label)}%`
}
meta.column.type = 'string'
}
}, 200)
},
valueBlur1 (meta) {
setTimeout(() => {
meta.value.isEditing = false
}, 200)
},
valuesBlur (meta) {
this.$nextTick(() => {
})
},
valuesFocus (meta) {
this.$nextTick(() => {
meta.value.isEditing = true
setTimeout(() => {
if (meta.value.value && this.myCheckboxList.length === 0) {
let valueArr = []
if (!_.isArray(meta.value.value)) {
let value = meta.value.value
if (value.indexOf('(') === 0 && value.indexOf(')') === value.length - 1) {
value = value.substring(1, value.length)
value = value.substring(0, value.length - 1)
}
valueArr = value.split(',')
valueArr.forEach((item, index) => {
if (item[0] === "'" && item[item.length - 1] === "'") {
item = item.substring(1, item.length - 1)
this.myCheckboxList.push(item)
}
})
} else if (_.isArray(meta.value.value)) {
meta.value.value.forEach(item => {
this.myCheckboxList.push(item)
})
}
}
}, 100)
})
},
connectionClick (meta) {
meta.isEditing = true
},
@@ -277,10 +414,13 @@ export default {
// 处理搜索值
meta.value.isEditing = true
meta.value.show = true
const obj = enumerateData.find(d => d.name === meta.column.label)
if (obj) {
meta.doc = obj
}
// 若是in或not incolumn的type要改成array否则是string
if (operator.toLowerCase().indexOf('in') > -1) {
meta.column.type = columnType.array
meta.value.value = []
} else if (['>', '<', '>=', '<='].indexOf(operator) > -1) {
meta.column.type = columnType.number
} else {
@@ -290,9 +430,16 @@ export default {
}
}
this.$nextTick(() => {
const selectList = this.$refs.valueInput
if (selectList && selectList.length > 0) {
this.$refs.valueInput[selectList.length - 1].focus() // 在for循环里生成的dom所以是数组
if (meta.doc) {
const selectList = this.$refs.columnValue
if (selectList && selectList.length > 0) {
this.$refs.columnValue[selectList.length - 1].focus() // 在for循环里生成的dom所以是数组
}
} else {
const selectList = this.$refs.valueInput
if (selectList && selectList.length > 0) {
this.$refs.valueInput[selectList.length - 1].focus() // 在for循环里生成的dom所以是数组
}
}
})
},
@@ -439,10 +586,26 @@ export default {
}
meta.value.isEditing = !meta.isCompleteCondition()
},
columnClick (meta) {
meta.column.isEditing = true
this.$nextTick(() => {
this.$refs.columnSelect[this.$refs.columnSelect.length - 1].focus()
})
},
valueClick (meta) {
meta.value.isEditing = true
const obj = enumerateData.find(d => d.name === meta.column.label)
if (obj) {
meta.doc = obj
}
this.$nextTick(() => {
this.$refs.valueInput[0].focus()
if (this.$refs.valueInput) {
this.$refs.valueInput[0].focus()
}
if (this.$refs.valuesSelect.length > 0) {
// 触发focus后select弹窗并没有生效
this.$refs.valuesSelect[0].focus(meta)
}
})
},
// 判断是否是用户自己添加的内容,用于判断是否是全局搜索
@@ -460,14 +623,20 @@ export default {
search () {
if (this.metaList.length > 0) {
const parser = new Parser(this.columnList)
const errorList = parser.validateMeta(this.metaList)
let errorList = parser.validateMeta(this.metaList)
// 测试的metaList并不是由new Meta()生成所以instanceof时meta并不在Meta原型链上导致报错故直接略过
if (this.isUnitTesting) {
errorList = []
}
const keywordList = this.myHighLight ? parser.getKeywordList(this.metaList) : [] // 搜索高亮的关键字
if (_.isEmpty(errorList)) {
const strObj = parser.handleMetaListToStr(this.metaList)
const str = strObj.str ? strObj.str : strObj
const str2 = strObj.str2 ? strObj.str2 : strObj
// str为将metaList转成字符串的值str2为地址栏展示的值
const key = parser.handleEntityTypeByStr(str)
this.$emit('search', { ...parser.parseStr(key), str: str2 })
let key = parser.handleEntityTypeByStr(str)
key = parser.conversionEnum(key)
this.$emit('search', { ...parser.parseStr(key), str: str2, keywordList: keywordList })
} else {
this.$message.error(handleErrorTip(errorList[0]))
}
@@ -555,14 +724,17 @@ export default {
const column = this.columnList.find(c => {
return c.label === param.column
})
const metaIndex = this.metaList.findIndex(m => m.column && m.column.label === param.column && m.operator.value === param.operator && m.value.value === this.handleValue(param.value, column, param.operator))
// 不是在首位则删除时顺带删除前一个indexand或or否则顺带删除后一个index
if (metaIndex > 0) {
this.metaList.splice(metaIndex - 1, 2)
} else if (this.metaList.length === 1) {
this.metaList.splice(metaIndex, 1)
} else {
this.metaList.splice(metaIndex, 2)
const obj = this.metaList.find(d => d.column && d.column.label === param.column && d.value && (d.value.value === `'${param.value[0]}'` || d.value.value === param.value[0]))
if (obj) {
const metaIndex = this.metaList.findIndex(m => m.column && m.column.label === param.column && m.operator.value === param.operator && m.value.value === this.handleValue(param.value, column, param.operator))
// 不是在首位则删除时顺带删除前一个indexand或or否则顺带删除后一个index
if (metaIndex > 0) {
this.metaList.splice(metaIndex - 1, 2)
} else if (this.metaList.length === 1) {
this.metaList.splice(metaIndex, 1)
} else {
this.metaList.splice(metaIndex, 2)
}
}
})
},
@@ -622,20 +794,25 @@ export default {
let { q } = this.$route.query
if (q && !this.convertMetaList) {
const parser = new Parser(this.columnList)
if (q.indexOf('+') > -1) {
q = q.replace('+', '')
}
if (q.indexOf('%') === 0 || q.indexOf('%20') > -1 || q.indexOf('%25') > -1) {
q = decodeURI(q)
} else {
const str1 = q.substring(q.indexOf('%'), q.indexOf('%') + 3)
if (q.indexOf('%') > 0 && (str1 !== '%20' || str1 === '%25')) {
if (q.indexOf('%') > 0 && (str1 === '%20' || str1 === '%25')) {
q = decodeURI(q)
}
}
this.metaList = parser.parseStr(q).metaList
}
this.emitter.on('advanced-search', function () {
vm.search()
})
if (!this.isUnitTesting) {
this.emitter.on('advanced-search', function () {
vm.search()
})
}
},
watch: {
convertMetaList: {
@@ -650,6 +827,7 @@ export default {
if (item.column && item.column.type === 'fullText') {
item.operator.value = '='
item.column.show = false
item.column.isFullText = true
item.operator.show = false
const label = JSON.parse(JSON.stringify(item.column.label))
item.column.label = parser.getEntityTypeByValue(item.column.label)

View File

@@ -1,35 +1,56 @@
<template>
<textarea
ref="textSearch"
></textarea>
<div class="search__suffixes search__suffixes--text-mode">
<div class="search__suffix">
<el-popover
popper-class="my-popper-class"
placement="top"
trigger="hover"
:content="$t('entity.switchToAdvancedSearch')"
>
<template #reference>
<i class="cn-icon cn-icon-filter margin-r-12" @click="changeMode"></i>
</template>
</el-popover>
</div>
<div v-show="isCloseIcon" class="search__suffix-close" @click="cleanParams">
<i class="el-icon-error"></i>
</div>
<div class="search__suffix" :class="showList ? 'new-search__suffix' : 'entity-explorer-search'" @click="search">
<i class="el-icon-search"></i>
<div @click="handleClick" v-ele-click-outside="handleBlur">
<textarea
style="text-indent: 65px;"
cols="40"
ref="textSearch"
></textarea>
<div class="search__suffixes search__suffixes--text-mode" :class="showList ? '' : 'entity-explorer-home'" style="padding-left: 1px;background: #fff;">
<!--切换texttag模式图标-->
<span class="search__suffix">
<el-popover
popper-class="my-popper-class"
placement="top"
trigger="hover"
:content="$t('overall.switchToTag')"
>
<template #reference>
<i test-id="text-change-mode" class="cn-icon cn-icon-filter" @click="changeMode"></i>
</template>
</el-popover>
</span>
<!--删除图标-->
<span v-show="isCloseIcon" class="search__suffix search__suffix-close" @click="cleanParams">
<i class="el-icon-error"></i>
</span>
<!--搜索图标-->
<span class="search__suffix" test-id="text-search" @click.stop="search">
<i class="el-icon-search"></i>
</span>
</div>
<!--showHint弹窗部分-->
<el-popover
placement="bottom"
width="100%"
ref="popoverRef"
:visible="hintVisible"
popper-class="search-show-hint-popover"
trigger="click">
<template #reference>
<div>
<hint v-if="hintVisible" :hintList="hintList"
@load="handleHintLoad"
@select="handleSelect"
:hintParams="hintParams"
:hintSearch="searchStr"></hint>
</div>
</template>
</el-popover>
</div>
</template>
<script>
import 'codemirror/theme/ambiance.css'
import 'codemirror/addon/hint/show-hint'
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/display/placeholder'
import 'codemirror/mode/sql/sql'
import Parser, { stringInQuot, handleOperatorSpace } from '@/components/advancedSearch/meta/parser'
import CodeMirror from 'codemirror'
import { toRaw } from 'vue'
@@ -37,23 +58,65 @@ import _ from 'lodash'
import { columnType } from '@/components/advancedSearch/meta/meta'
import { handleErrorTip } from '@/components/advancedSearch/meta/error'
import { overwriteUrl, urlParamsHandler } from '@/utils/tools'
import Hint from '@/components/advancedSearch/showhint/Hint/Hint'
import { getDataset } from '@/components/advancedSearch/showhint/packages/getDataset'
import codeMirrorMixins from '@/components/advancedSearch/showhint/myCodeMirror.js'
export default {
name: 'TextMode',
mixins: [codeMirrorMixins],
props: {
columnList: Array,
str: String,
showList: Boolean,
showCloseIcon: Boolean
showCloseIcon: Boolean,
isShowHint: {
type: Boolean,
default: false
},
unitTestStr: String
},
data () {
return {
codeMirror: null,
isCloseIcon: this.showCloseIcon,
isEdit: false
isEdit: false,
hintVisible: false,
dataset: null,
CodeMirror,
myUnitTestStr: this.unitTestStr
}
},
emits: ['changeMode', 'search'],
inject: ['myHighLight'],
created () {
if (this.isShowHint && !this.isUnitTesting) {
this._initComponent()
}
},
provide () {
return {
getDataset: () => {
// provide() 写成方法之后,保证this的指向
return this.dataset || null
}
}
},
components: {
Hint
},
computed: {
searchStr () {
const { wholeTokenStr } = this.getWholeToken() || ''
if (['not in', 'not like', 'order by', 'group by'].includes(wholeTokenStr?.toLowerCase())) {
return wholeTokenStr
}
if (['operator', 'keyword', 'builtin'].includes(this.hintParams?.token?.type)) {
return this.hintParams?.token?.string
}
return this.hintParams.leftpart || this.hintSearch || ''
}
},
methods: {
cleanParams () {
toRaw(this.codeMirror).setValue('')
@@ -64,43 +127,74 @@ export default {
this.reloadUrl(routeQuery, 'cleanOldParams')
},
initCodeMirror () {
this.codeMirror = CodeMirror.fromTextArea(this.$refs.textSearch, {
mode: {
name: 'sql'
},
placeholder: 'Enter...',
let option = {
mode: 'sql',
placeholder: '',
lineNumbers: false
})
this.codeMirror.setOption('extraKeys', {
Enter: (cm) => {}
})
this.codeMirror.on('focus', () => {
if (this.codeMirror.getValue().trim() !== '') {
this.isEdit = true
this.isCloseIcon = true
}
if (this.isShowHint) {
option = {
keyMap: 'sublime',
tabSize: 2, // 缩进格式
// theme: 'eclipse', // 主题,对应主题库 JS 需要提前引入
line: true,
lineNumbers: false, // 显示行数
indentUnit: 4, // 缩进单位为4
styleActiveLine: true, // 当前行背景高亮
// mode: 'text/x-filter', // HMTL混合模式
mode: 'sql', // HMTL混合模式
foldGutter: true,
lint: true,
auto: 'auto', // 自动换行
autoCloseBrackets: true, // 自动闭合符号
matchBrackets: true, // 是否添加匹配括号高亮
spellcheck: true, // 启用拼写检查
autocorrect: true, // 启用自动更正
lineWrapping: true, // 滚动或换行以显示长行
// 提示配置
hintOptions: {
completeSingle: false, // 自动匹配唯一值
// 匹配 t_test_login.col_a 用. 来连接的
tables: {
filter_table: ['recv_time']
},
alignWithWord: false
}
}
})
this.codeMirror.on('blur', () => {
const timer = setTimeout(() => {
this.isEdit = false
this.isCloseIcon = false
clearTimeout(timer)
}, 200)
})
this.codeMirror.on('update', () => {
this.isEdit = true
this.isCloseIcon = true
})
}
this.codeMirror = CodeMirror.fromTextArea(this.$refs.textSearch, option)
if (this.codeMirror) {
this.codeMirror.setOption('extraKeys', {
Enter: (cm) => {}
})
this.setCodemirrorValue()
this.initEvent()
this.initHint()
}
},
search () {
const str = this.codeMirror.getValue().trim()
this.handleBlur()
let str
if (!this.isUnitTesting) {
str = this.codeMirror.getValue().trim()
} else {
str = this.myUnitTestStr
}
if (str) {
const parser = new Parser(this.columnList)
const keyInfo = parser.comparedEntityKey(parser.handleEntityTypeByStr(str))
const keyInfo = parser.comparedEntityKey(parser.handleEntityTypeByStr(str)) // 校验输入str字段是schema内的字段并将语句进行规范
const metaList = parser.parseStr(_.cloneDeep(str)).metaList
const keywordList = this.myHighLight ? parser.getKeywordList(metaList) : [] // 搜索高亮所需的关键字
if (keyInfo.isKey) {
const errorList = parser.validateStr(keyInfo.key)
const enumKey = parser.conversionEnum(keyInfo.key) // 检查是否包含枚举字段,包含的话进行替换
const errorList = parser.validateStr(enumKey) // 检查语句是否有错误
if (_.isEmpty(errorList)) {
this.$emit('search', { ...parser.parseStr(keyInfo.key), str: str })
// 补全模糊搜索
if (!this.isUnitTesting) {
toRaw(this.codeMirror).setValue(parser.handleEntityTypeByStr(str))
}
// 注参数str1.是用户搜索框的内容在补全模糊搜索后的内容2.部分参数是用户主观可见但格式不符合接口原则的如status='Active'接口需要status=0
this.$emit('search', { ...parser.parseStr(enumKey), str: parser.handleEntityTypeByStr(str), keywordList: keywordList })
} else {
this.$message.error(handleErrorTip(errorList[0]))
}
@@ -111,8 +205,9 @@ export default {
this.$emit('search', { q: '', str: '', metaList: [] })
}
},
focus () {
this.codeMirror.focus()
focus (e) {
toRaw(this.codeMirror).setValue(e.str)
// this.codeMirror.focus()
},
changeMode () {
const str = this.codeMirror.getValue().trim()
@@ -147,7 +242,12 @@ export default {
}
},
addParams (params) {
let current = this.codeMirror.getValue()
let current = ''
if (!this.isUnitTesting) {
current = this.codeMirror.getValue()
} else {
current = this.myUnitTestStr
}
params.forEach(param => {
const column = this.columnList.find(c => c.label === param.column)
if (param.operator === 'has') {
@@ -156,7 +256,11 @@ export default {
current = `${current ? current + ' AND ' : ''}${param.column}${handleOperatorSpace(param.operator)}${this.handleValue(param.value, column, param.operator)}`
}
})
toRaw(this.codeMirror).setValue(current.trim())
if (!this.isUnitTesting) {
toRaw(this.codeMirror).setValue(current.trim())
} else {
this.myUnitTestStr = current
}
},
removeParams (params) {
let current = this.codeMirror.getValue()
@@ -193,6 +297,174 @@ export default {
newUrl = urlParamsHandler(window.location.href, query, newParam, clean)
}
overwriteUrl(newUrl)
},
initEvent () {
this.codeMirror.on('focus', (coder) => {
if (this.codeMirror.getValue().trim() !== '') {
this.isEdit = true
this.isCloseIcon = true
}
if (this.isShowHint && this.$emit) {
this.$emit('focus', coder.getValue())
}
})
this.codeMirror.on('blur', (coder) => {
const timer = setTimeout(() => {
this.isEdit = false
this.isCloseIcon = false
if (this.isShowHint && this.$emit) {
this.$emit('blur', coder.getValue())
}
clearTimeout(timer)
}, 200)
})
this.codeMirror.on('update', () => {
this.isEdit = true
this.isCloseIcon = true
})
if (this.isShowHint) {
// 支持双向绑定
this.codeMirror.on('change', (coder) => {
if (this.$emit) {
this.$emit('input', coder.getValue())
}
})
this.codeMirror.on('startCompletion', () => {
// 展开自动提示的 事件回调
this.hintVisible = true
this.hintVm?.hintDeactive()
})
this.codeMirror.on('endCompletion', () => {
// 自动提示关闭
this.hintVisible = false
this.hintParams = {}
this.hintList = []
})
this.$emit('load', this.codeMirror)
}
},
_initComponent () {
getDataset(this, this.queryParams || {}, this.columnList).then((dataset, dataDisposeFun) => {
this.dataset = Object.freeze(dataset)
}).catch(err => {
console.error(err)
})
},
initHint () {
this.codeMirror.on('inputRead', () => {
setTimeout(() => {
this.codeMirror.showHint()
})
})
},
handleBlur () {
if (this.isShowHint) {
this.hintVisible = false
this.hintParams = {}
this.hintList = []
}
},
handleClick () {
if (this.isShowHint) {
this.hintVisible = true
this.codeMirror.showHint()
}
},
getWholeToken () {
// 获取 前一个token
const editor = this.hintParams.editor
const Pos = this.CodeMirror.Pos
const cur = this.hintParams.cur
const token = this.hintParams.token
if (!editor) {
return
}
const spaceToken = editor.getTokenAt(Pos(cur.line, token.start))
let preToken = ''
if (spaceToken && spaceToken?.string === ' ') {
preToken = editor.getTokenAt(Pos(cur.line, spaceToken.start))
}
const searchKey = `${preToken?.string} ${token?.string}`
return {
wholeTokenStr: searchKey,
spaceToken,
preToken,
token
}
},
handleHintLoad ({ vm }) {
this.hintVm = vm
},
handleSelect (item, index, hintList) {
if (index === 0) {
// 不可能选中0 第0项是标题, 选中0 说明没选中
this.hintParams?.editor?.closeHint()
this.$emit('query', CodeMirror)
return
}
const data = {
from: this.hintParams.from,
to: this.hintParams.to,
list: hintList
}
const { wholeTokenStr, preToken, token } = this.getWholeToken() || ''
let cur = null
cur = this.hintParams?.cur
// 上一个字段 是存在空格的关键字,整体删除上一个关键字
if (['not in', 'not like', 'order by', 'group by'].includes(wholeTokenStr?.toLowerCase())) {
this.hintParams?.editor?.replaceRange('', { line: cur.line, ch: preToken.start }, {
line: cur.line,
ch: token.end
})
}
this.completion && this.completion.pick(data, index)
},
setCodemirrorValue () {
// 如果地址栏包含参数q则将参数q回显到搜索栏内
let { q } = this.$route.query
if (this.str) {
toRaw(this.codeMirror).setValue(this.str)
}
if (q) {
if (q.indexOf('+') > -1) {
q = q.replace('+', ' ')
}
if (q.indexOf('%') === 0 || q.indexOf('%20') > -1 || q.indexOf('%25') > -1) {
q = decodeURI(q)
} else {
const str1 = q.substring(q.indexOf('%'), q.indexOf('%') + 3)
if (q.indexOf('%') > 0 && (str1 === '%20' || str1 === '%25')) {
q = decodeURI(q)
}
}
// 为避免地址栏任意输入导致全查询的q带QUERY解析时不识别导致的语法错误
// 如地址栏输入116.178.222.171此时的q很长刷新界面时需要把q里的116.178.222.171拿出来进行搜索
if (q.indexOf('QUERY') > -1) {
const strList = q.split(' ')
if (strList.length > 0) {
// 此时strList[1]为ip_addr:116.178.222.171获取116.178.222.171
q = strList[1].slice(8)
}
}
if (this.codeMirror) {
toRaw(this.codeMirror).setValue(q)
}
} else {
this.isCloseIcon = false
}
const vm = this
this.emitter.on('advanced-search', function () {
vm.search()
})
}
},
watch: {
@@ -216,39 +488,23 @@ export default {
}
},
mounted () {
// 如果地址栏包含参数q则将参数q回显到搜索栏内
let { q } = this.$route.query
this.initCodeMirror()
if (this.str) {
toRaw(this.codeMirror).setValue(this.str)
}
if (q) {
if (q.indexOf('%') === 0 || q.indexOf('%20') > -1 || q.indexOf('%25') > -1) {
q = decodeURI(q)
} else {
const str1 = q.substring(q.indexOf('%'), q.indexOf('%') + 3)
if (q.indexOf('%') > 0 && (str1 !== '%20' || str1 === '%25')) {
q = decodeURI(q)
if (this.isShowHint) {
this.$nextTick(() => {
// dataset是避免数据未初始化完成注册失败ref是因为组件加载2次避免第二次时dom丢失导致数据挂载失败
if (this.dataset && this.$refs.textSearch) {
this.initShowHint()
this.initCodeMirror()
}
}
// 为避免地址栏任意输入导致全查询的q带QUERY解析时不识别导致的语法错误
// 如地址栏输入116.178.222.171此时的q很长刷新界面时需要把q里的116.178.222.171拿出来进行搜索
if (q.indexOf('QUERY') > -1) {
const strList = q.split(' ')
if (strList.length > 0) {
// 此时strList[1]为ip_addr:116.178.222.171获取116.178.222.171
q = strList[1].slice(8)
}
}
toRaw(this.codeMirror).setValue(q)
} else {
this.isCloseIcon = false
})
} else if (this.$refs.textSearch) {
this.initCodeMirror()
}
const vm = this
this.emitter.on('advanced-search', function () {
vm.search()
})
}
}
</script>
<style>
.el-popper.search-show-hint-popover {
visibility: hidden !important;
}
</style>

View File

@@ -4,6 +4,7 @@ import ParserError, { errorDesc, errorTypes } from '@/components/advancedSearch/
import _ from 'lodash'
import { ElMessage } from 'element-plus'
import i18n from '@/i18n'
import store from '@/store'
const strReg = {
// 需要不限制语言,正则过滤中英日俄语出错实现语言都通过。留个记录观察,后续校验
@@ -12,6 +13,14 @@ const strReg = {
value: /^[\da-zA-Z\u4E00-\u9FA5\u3040-\u309F\u0800-\u4e00\u0400-\u04FF\u2000-\u206F\s.'-_%]$/
}
const operatorList = ['=', ' in ', ' IN ', ' like ', ' LIKE ', 'HAS(', 'has(']
const enumList = ['status', 'eventType', 'severity']
// ipv4校验
const regexIPv4 = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
// ipv6校验
const regexIPv6 = /^(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){1,6})|:(?:(?::[0-9A-Fa-f]{1,4}){1,7}|:)|fe80:(?::[0-9A-Fa-f]{0,4}){0,4}%\w+|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])\.(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])\.(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])\.(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])|(?:[0-9A-Fa-f]{1,4}:){1,4}:192\.88\.99\.(\d{1,3})|(?:[0-9A-Fa-f]{1,4}:){1,4}:192\.0\.2\.(\d{1,3})|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:[0-9A-Fa-f]{1,4}:){0,1}192\.0\.0\.(\d{1,3})|ff00:(?::[0-9A-Fa-f]{0,4}){0,4}|(?:[0-9A-Fa-f]{1,4}:){1,4}:255\.255\.255\.255)$/
// domain校验
const regexDomain = /^(?=^.{3,255}$)(http(s)?:\/\/)?(www\.)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:\d+)*(\/\w+\.\w+)*$/
export default class Parser {
constructor (columnList) {
@@ -860,7 +869,7 @@ export default class Parser {
item.value.label = isWrapped ? `'${this.delSingleQuote(label)}'` : `${this.delSingleQuote(label)}`
item.value.label1 = isWrapped ? `'%${this.delSingleQuote(label)}%'` : `%${this.delSingleQuote(label)}%`
}
item.column.type = 'string'
item.column.type = columnType.string
}
})
// 长度为1时即模糊搜索例如搜索框值为1.1.1.1则直接返回1.1.1.1
@@ -1111,7 +1120,7 @@ export default class Parser {
if (key === 'has') {
returnObj.key += 'has(' + obj.label + item.substring(item.indexOf(','), item.length) + ' AND '
} else {
returnObj.key += obj.label + ' ' + item.substring(item.toLowerCase().indexOf(key.toLowerCase()), item.length) + ' AND '
returnObj.key += obj.label + ' ' + item.substring(obj.label.length, item.length) + ' AND '
}
} else if (returnObj.isKey) {
returnObj.key = '[' + key + ']'
@@ -1126,12 +1135,12 @@ export default class Parser {
}
return returnObj
} else if (q.indexOf(' LIKE ') > -1 || q.indexOf(' like ') > -1) {
} else if (q.toLowerCase().indexOf(' like ') > -1) {
return {
key: q,
isKey: true
}
} else if (q.indexOf(' IN ') > -1 || q.indexOf(' in ') > -1) {
} else if (q.toLowerCase().indexOf(' in ') > -1) {
return {
key: q,
isKey: true
@@ -1145,15 +1154,79 @@ export default class Parser {
return { key: '[' + key + ']', isKey: false }
}
}
} else if (q && (q.indexOf(' IN ') > -1 || q.indexOf(' in ') > -1 || q.indexOf(' LIKE ') > -1 || q.indexOf(' like ') > -1)) {
return {
key: q,
isKey: true
}
} else if (q && (q.indexOf('has(') > -1)) {
return {
key: q,
isKey: true
} else if (q && (q.toLowerCase().indexOf(' in ') > -1 || q.toLowerCase().indexOf(' like ') > -1 || q.toLowerCase().indexOf('has(') > -1)) {
const lowerQ = q.toLowerCase()
if (lowerQ.indexOf(' and ') > -1) {
if (this.checkStrIncludeAnd(q)) {
q = q.replace(/ and /g, ' AND ')
}
const arr = q.split(' AND ')
for (let i = 0; i < arr.length; i++) {
const item = arr[i].toLowerCase()
if (item.indexOf(' like ') > -1) {
const key = item.substring(0, item.indexOf(' like '))
const obj = this.columnList.find(t => t.label.toLowerCase() === key.toLowerCase())
if (!obj) {
return { key: '[' + key + ']', isKey: false }
}
} else if (item.indexOf(' in ') > -1) {
const key = q.substring(0, q.indexOf(' in '))
const obj = this.columnList.find(t => t.label.toLowerCase() === key.toLowerCase())
if (!obj) {
return { key: '[' + key + ']', isKey: false }
}
} else if (item.indexOf('has(') > -1) {
const key = item.substring(0, 4)
if (key === 'has(') {
const label = item.substring(4, item.indexOf(','))
if (label) {
const obj = this.columnList.find(t => t.label.toLowerCase() === label.toLowerCase())
if (!obj) {
return { key: '[' + key + ']', isKey: false }
}
} else {
return { key: 'in index ' + q.indexOf('has('), isKey: false }
}
} else {
return { key: 'in index ' + q.indexOf('has('), isKey: false }
}
}
}
return { key: q, isKey: true }
} else if (lowerQ.indexOf(' like ') > -1) {
const key = q.substring(0, q.indexOf(' like '))
const obj = this.columnList.find(t => t.label.toLowerCase() === key.toLowerCase())
if (obj) {
return { key: obj.label + q.substring(lowerQ.indexOf(' like '), q.length), isKey: true }
} else {
return { key: '[' + key + ']', isKey: false }
}
} else if (lowerQ.indexOf(' in ') > -1) {
const key = lowerQ.substring(0, lowerQ.indexOf(' in '))
const obj = this.columnList.find(t => t.label.toLowerCase() === key.toLowerCase())
if (obj) {
return { key: obj.label + q.substring(lowerQ.indexOf(' in '), q.length), isKey: true }
} else {
return { key: '[' + key + ']', isKey: false }
}
} else if (lowerQ.indexOf('has(') > -1) {
const key = lowerQ.substring(0, 4)
if (key === 'has(') {
const label = lowerQ.substring(4, lowerQ.indexOf(','))
if (label) {
const obj = this.columnList.find(t => t.label.toLowerCase() === label.toLowerCase())
if (obj) {
return { key: 'has(' + obj.label + q.substring(lowerQ.indexOf(','), lowerQ.length), isKey: true }
} else {
return { key: '[' + key + ']', isKey: false }
}
} else {
return { key: 'in index 5', isKey: false }
}
} else {
return { key: 'in index 5', isKey: false }
}
}
} else {
return {
@@ -1216,8 +1289,9 @@ export default class Parser {
if (item.indexOf('it is test keyword') > -1) {
const regex = /\d+/g
const result1 = item.match(regex)
noAndList[index] = noAndList[index].replace(result1[0], '')
noAndList[index] = noAndList[index].replace('it is test keyword', tempList[result1[0]])
result1.forEach((r, i) => {
noAndList[index] = noAndList[index].replace('it is test keyword' + r, tempList[result1[i]])
})
}
})
@@ -1247,6 +1321,10 @@ export default class Parser {
})
return str
} else if (!result) {
// 此处为不能识别的字段不能当成app处理
if (str.indexOf('=') > -1 || str.toLowerCase().indexOf(' in ') > -1 || str.toLowerCase().indexOf(' like ') > -1 || str.toLowerCase().indexOf('has(') > -1) {
return str
}
const regex = /^["']|["']$/
// 去除两侧引号,如'1.1.1.1',避免校验时被当作app
if (regex.test(str)) {
@@ -1273,15 +1351,9 @@ export default class Parser {
if (str[0] === '%' && str[str.length - 1] !== '%') {
str = str.substring(1, str.length)
}
// ipv4校验
const regexIPv4 = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
// ipv6校验
const regexIPv6 = /^(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){1,6})|:(?:(?::[0-9A-Fa-f]{1,4}){1,7}|:)|fe80:(?::[0-9A-Fa-f]{0,4}){0,4}%\w+|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])\.(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])\.(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])\.(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])|(?:[0-9A-Fa-f]{1,4}:){1,4}:192\.88\.99\.(\d{1,3})|(?:[0-9A-Fa-f]{1,4}:){1,4}:192\.0\.2\.(\d{1,3})|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:[0-9A-Fa-f]{1,4}:){0,1}192\.0\.0\.(\d{1,3})|ff00:(?::[0-9A-Fa-f]{0,4}){0,4}|(?:[0-9A-Fa-f]{1,4}:){1,4}:255\.255\.255\.255)$/
// domain校验
const reg = /^(?=^.{3,255}$)(http(s)?:\/\/)?(www\.)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:\d+)*(\/\w+\.\w+)*$/
if (regexIPv4.test(str) || regexIPv6.test(str)) {
const obj = this.columnList.find(t => t.label.toLowerCase() === 'ip')
const obj = this.columnList.find(t => t.label.toLowerCase() === 'ip' || t.label.toLowerCase() === 'ip.addr')
if (obj) {
return `${obj.label}='${str}'`
} else {
@@ -1290,9 +1362,9 @@ export default class Parser {
}
return str
}
} else if (reg.test(str)) {
} else if (regexDomain.test(str)) {
// 只写作domain即可schema字段更改几次避免后续再更改直接拿this.columnList的label进行替换
const obj = this.columnList.find(t => t.label.toLowerCase() === 'domain')
const obj = this.columnList.find(t => t.label.toLowerCase() === 'domain' || t.label.toLowerCase() === 'domain.name')
if (obj) {
return `${obj.label} LIKE '%${str}'`
} else {
@@ -1302,7 +1374,7 @@ export default class Parser {
return str
}
} else {
const obj = this.columnList.find(t => t.label.toLowerCase() === 'app')
const obj = this.columnList.find(t => t.label.toLowerCase() === 'app' || t.label.toLowerCase() === 'app.name')
if (obj) {
return `${obj.label} LIKE '%${str}%'`
} else {
@@ -1314,18 +1386,14 @@ export default class Parser {
}
}
/**
* 判断传过来值的实体类型仅限于ip、domain、app
*/
getEntityTypeByValue (str) {
if (str[0] === "'" && str[str.length - 1] === "'") {
str = str.substring(1, str.length)
str = str.substring(0, str.length - 1)
}
// ipv4校验
const regexIPv4 = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
// ipv6校验
const regexIPv6 = /^(?:(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,7}:|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}:(?:(?::[0-9A-Fa-f]{1,4}){1,6})|:(?:(?::[0-9A-Fa-f]{1,4}){1,7}|:)|fe80:(?::[0-9A-Fa-f]{0,4}){0,4}%\w+|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])\.(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])\.(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])\.(?:(?:2[0-4]|1\d|[1-9])?\d|25[0-5])|(?:[0-9A-Fa-f]{1,4}:){1,4}:192\.88\.99\.(\d{1,3})|(?:[0-9A-Fa-f]{1,4}:){1,4}:192\.0\.2\.(\d{1,3})|(?:[0-9A-Fa-f]{1,4}:){1,4}:(?:[0-9A-Fa-f]{1,4}:){0,1}192\.0\.0\.(\d{1,3})|ff00:(?::[0-9A-Fa-f]{0,4}){0,4}|(?:[0-9A-Fa-f]{1,4}:){1,4}:255\.255\.255\.255)$/
// domain校验
const reg = /^(?=^.{3,255}$)(http(s)?:\/\/)?(www\.)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:\d+)*(\/\w+\.\w+)*$/
if (regexIPv4.test(str) || regexIPv6.test(str)) {
const obj = this.columnList.find(t => t.label.toLowerCase() === 'ip')
if (obj) {
@@ -1333,7 +1401,7 @@ export default class Parser {
} else {
return str
}
} else if (reg.test(str)) {
} else if (regexDomain.test(str)) {
const obj = this.columnList.find(t => t.label.toLowerCase() === 'domain')
if (obj) {
return obj.label
@@ -1415,7 +1483,7 @@ export default class Parser {
lastObj[i] = `has(${i},${commonObj[i]})`
} else {
// 单独存在的,直接保留
lastObj[i] = `${i} = '${commonObj[i]}'`
lastObj[i] = `${i}=${commonObj[i]}`
}
}
@@ -1476,6 +1544,78 @@ export default class Parser {
return this.columnList.find(t => t.label.toLowerCase() === label.toLowerCase())
}
/**
* 检测str是否包含枚举字段包含的话进行替换
* @param str
* @returns {string|*}
*/
conversionEnum (str) {
if (str) {
let enumFlag = false // 判断字符串是否包含枚举类型的key不包含则直接返回
enumList.forEach(item => {
if (str.toLocaleLowerCase().indexOf(item.toLocaleLowerCase()) > -1) {
enumFlag = true
}
})
if (enumFlag) {
let key = _.cloneDeep(str)
let searchList = [] // 将字符串按AND分割成单独的搜索条件
if (key.indexOf(' AND ') > -1) {
searchList = key.split(' AND ')
} else {
searchList = [key]
}
searchList.forEach((item, index) => {
const obj = this.columnList.find(d => item.indexOf(d.label) > -1)
if (obj && obj.doc.data) {
for (let i = 0; i < obj.doc.data.length; i++) {
const item1 = obj.doc.data[i]
if (item.indexOf(item1.code) > -1) {
searchList[index] = searchList[index].replace(new RegExp(item1.code, 'g'), item1.value)
// 匹配到code终止匹配
break
} else {
// 该操作是避免中文参数切换到英文环境时code经i18n转为英文匹配不到中文参数的情况
Object.keys(store.state.i18nObj).forEach(lang => {
const i18nCode = store.state.i18nObj[lang][item1.code1]
if (item.indexOf(i18nCode) > -1) {
searchList[index] = searchList[index].replace(new RegExp(i18nCode, 'g'), item1.value)
}
})
}
}
}
})
key = searchList.join(' AND ')
return key
} else {
return str
}
} else {
return str
}
}
/**
* 获取关键字列表,即高亮字段
* @param metaList
* @returns {*[]}
*/
getKeywordList (metaList) {
const keywordList = []
if (metaList && metaList.length > 0) {
metaList.forEach(item => {
if (item.column && item.column.isFullText) {
keywordList.push({ type: 'fullText', value: item.value.value })
} else if (item.column && !item.column.isFullText) {
keywordList.push({ type: item.column.type, value: item.value.value || item.column.label })
}
})
}
return keywordList
}
}
// 使用单引号包裹

View File

@@ -0,0 +1,171 @@
<template>
<div class="HelperInfo">
<div class="tips-container">
<Renderer ref="renderVm" :renderProps="renderProps" :renderFun="helperDataFun"></Renderer>
</div>
</div>
</template>
<script>
import operatorTips from '@/components/advancedSearch/showhint/const/operatorTips'
import defaultTips from '@/components/advancedSearch/showhint/const/defaultTips.js'
import sqlTips from '@/components/advancedSearch/showhint/const/sqlTips.js'
import functionTips from '@/components/advancedSearch/showhint/const/functionTips.js'
import filterTips from '@/components/advancedSearch/showhint/const/filterTips.js'
import varTips from '@/components/advancedSearch/showhint/const/varTips.js'
import { fieldRender } from '@/components/advancedSearch/showhint/const/fieldTips.js'
import { EN, storageKey, ZH } from '@/utils/constants'
export default {
name: 'HelperInfo',
inject: ['getDataset'],
props: {
hintSearch: {},
hintParams: {}
},
data () {
return {
operatorTips,
sqlTips,
functionTips,
renderProps: {},
isShow: true
}
},
methods: {
refreshRender () {
this.$refs?.renderVm.$forceUpdate()
},
matchFields (searchKey) {
const dataset = this.getDataset()
const fieldInfo = dataset.getFieldInfo(searchKey)
if (!fieldInfo) {
return false
}
let operates = dataset.getOperates(fieldInfo.type, fieldInfo._matchItem)
operates = operates.map(item => item.text.toUpperCase())
let funs = dataset.getFunctions(fieldInfo.type, fieldInfo._matchItem)
funs = funs.map(item => item.text.toUpperCase())
let operatorReference = dataset.sourceData.operatorReference
let funcReference = dataset.sourceData.funcReference
operatorReference = operatorReference.filter(item => {
return operates.includes(item.label)
})
funcReference = funcReference.filter(item => {
return funs.includes(item.name)
})
this.renderProps = {
fieldInfo,
funcReference,
operatorReference,
funs,
operates
}
return fieldRender
}
},
computed: {
helperDataFun () {
let hintSearch = ''
if (this.hintSearch) {
hintSearch = JSON.parse(JSON.stringify(this.hintSearch))
const fields = this.getDataset().sourceData.fields
const obj = fields.find(d => d.label === hintSearch)
if (obj) {
hintSearch = obj.label
}
}
let searchKey = hintSearch.toUpperCase() || ''
searchKey = searchKey.trim()
// if (functionTips[searchKey]) {
// return functionTips[searchKey].description
// }
if (operatorTips[searchKey]) {
return operatorTips[searchKey].description
}
// if (sqlTips[searchKey]) {
// return sqlTips[searchKey].description
// }
if (filterTips[searchKey]) {
return filterTips[searchKey].description
}
// if (varTips[searchKey]) {
// return varTips[searchKey].description
// }
// 完整的匹配关键字
if (this.getDataset()) {
const fieldRender = this.matchFields(searchKey)
if (fieldRender) {
return fieldRender
}
}
const language = localStorage.getItem(storageKey.language) || EN
if (language === ZH) {
return defaultTips.zhDefault.description
}
return defaultTips.enDefault.description
}
}
}
</script>
<style scoped>
.HelperInfo {
min-width: 450px;
background-color: #fff;
height: 294px;
box-sizing: border-box;
overflow: auto;
z-index: 99;
}
.tips-container {
padding: 12px;
}
/deep/ ul li {
list-style: inside;
line-height: 20px;
}
/deep/ h3 {
margin: 8px 0;
line-height: 22px;
}
/deep/ p {
line-height: 22px;
word-break: break-all;
}
/deep/ code,
.code {
background: initial;
border: 1px solid #DEDEDE;
height: 24px;
line-height: 24px;
padding: 0 12px;
margin: 6px 0;
display: block;
}
/deep/ .sub-url {
padding-left: 18px;
}
/deep/ .sub-url li {
list-style: inside circle;
}
/deep/ i.ref-txt {
line-height: 20px;
color: #aaa;
}
</style>

View File

@@ -0,0 +1,35 @@
<!-- 提示信息 -->
<template>
<div class="Hint" @click.stop>
<div class="hint__block">
<div class="hint__block-filter">
<hint-info v-on="$listeners" :hintList="hintList" @select="onSelect"></hint-info>
</div>
<div class="hint__block-helper">
<helper-info :hintSearch="hintSearch"></helper-info>
</div>
</div>
</div>
</template>
<script>
import HelperInfo from './HelperInfo.vue'
import HintInfo from './HintInfo.vue'
export default {
name: 'Hint',
props: {
hintList: [],
hintSearch: {}
},
components: {
HintInfo,
HelperInfo
},
methods: {
onSelect (item, index, hintList) {
this.$emit('select', item, index, hintList)
}
}
}
</script>

View File

@@ -0,0 +1,127 @@
<template>
<div class="HintInfo">
<ul style="padding-left: 0;margin: -10px 0 0 0;min-width: calc(100% - 12px)">
<template v-for="(item,index) in hintList" :key="index">
<li :ref="'hint_'+index" class="relative-item CodeMirror-hint"
style="margin-bottom: 2px"
@click="handleSelect(item,index,hintList)"
:class="{'CodeMirror-hint-active':index === activeIndex,[item.className]:true}"
>{{ item.displayText }}</li>
</template>
</ul>
</div>
</template>
<script>
export default {
name: 'HintInfo',
props: {
hintList: {
type: Array,
default () {
return []
}
}
},
data () {
return {
active: true,
activeIndex: null
}
},
mounted () {
// this.handleFocus()
this.$emit('load', {
name: this.name,
label: this.name,
vm: this
})
},
methods: {
scrollToView (index = 0) {
// 移动到可视区域
const li = this.$refs['hint_' + index][0]
li && li.scrollIntoView(false)
},
handleDown () {
if (!this.active) {
this.hintActive()
return
}
let nextIndex = this.activeIndex + 1
let nextItem = this.hintList[nextIndex]
if (nextItem?.type === 'abstract') {
nextIndex++
nextItem = this.hintList[nextIndex]
}
if (nextItem?.type === 'abstract') {
nextIndex++
}
nextIndex >= this.hintList.length ? this.activeIndex = 1 : this.activeIndex = nextIndex
this.scrollToView(this.activeIndex)
},
handleUp () {
if (!this.active) {
this.hintActive()
return
}
let preIndex = this.activeIndex - 1
let preItem = this.hintList[preIndex]
if (preItem?.type === 'abstract') {
preIndex--
preItem = this.hintList[preIndex]
}
if (preItem?.type === 'abstract') {
preIndex--
}
preIndex > 0 ? this.activeIndex = preIndex : this.activeIndex = this.hintList.length - 1
this.scrollToView(this.activeIndex)
},
hintActive () {
this.active = true
this.activeIndex = 1
this.scrollToView(this.activeIndex)
},
hintDeactive () {
this.active = false
this.activeIndex = null
},
handleSelect (item, index) {
this.$emit('select', item, index, this.hintList)
},
triggerSelect () {
const index = this.activeIndex || 0
const item = this.hintList[index]
this.$emit('select', item, index, this.hintList)
}
}
}
</script>
<style scoped>
.HintInfo {
height: 280px;
overflow: auto;
background: #fff;
}
ul {
min-width: 300px;
height:auto;
width: fit-content;
box-sizing: border-box;
padding-bottom: 10px;
}
.el-dropdown-menu__item{
text-indent: 1em;
font-size: 12px !important;
font-family: NotoSansSChineseRegular;
color: #575757;
font-weight: 400;
}
.hint-clear{
text-indent: 1em;
}
</style>

View File

@@ -0,0 +1,16 @@
<script>
export default {
name: 'Renderer',
// abstract: true,
props: {
renderFun: {
type: Function,
require: true
},
renderProps: {}
},
render (h) {
return this.renderFun(h, this.renderProps)
}
}
</script>

View File

@@ -0,0 +1,22 @@
import sqlHint from './sql-hint.js'
import showHint from './show-hint.js'
import manualShowHint from './manual-show-hint'
import 'codemirror/addon/hint/show-hint.css'
export default function createHint (hitType = 'default', CodeMirror, {
dataset,
hinthook,
keywordshook,
callback,
keyboardUp,
keyboardDown,
keyboardEnter
}) {
hitType === 'default' ? showHint(CodeMirror) : manualShowHint(CodeMirror, {
cb: callback,
keyboardUp,
keyboardDown,
keyboardEnter
})
sqlHint(CodeMirror, { dataset, hinthook, keywordshook })
}

View File

@@ -0,0 +1,596 @@
export default function (CodeMirror,{
cb,
keyboardUp,
keyboardDown,
keyboardEnter
}) {
var HINT_ELEMENT_CLASS = "CodeMirror-hint";
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
// This is the old interface, kept around for now to stay
// backwards-compatible.
CodeMirror.showHint = function (cm, getHints, options) {
if (!getHints) return cm.showHint(options);
if (options && options.async) getHints.async = true;
var newOpts = {hint: getHints};
if (options) for (var prop in options) newOpts[prop] = options[prop];
return cm.showHint(newOpts);
};
CodeMirror.defineExtension("showHint", function (options) {
options = parseOptions(this, this.getCursor("start"), options);
var selections = this.listSelections()
if (selections.length > 1) return;
// By default, don't allow completion when something is selected.
// A hint function can have a `supportsSelection` property to
// indicate that it can handle selections.
if (this.somethingSelected()) {
if (!options.hint.supportsSelection) return;
// Don't try with cross-line selections
for (var i = 0; i < selections.length; i++)
if (selections[i].head.line != selections[i].anchor.line) return;
}
if (this.state.completionActive) this.state.completionActive.close();
var completion = this.state.completionActive = new Completion(this, options);
if (!completion.options.hint) return;
CodeMirror.signal(this, "startCompletion", this);
completion.update(true);
cb && cb({completion})
});
CodeMirror.defineExtension("closeHint", function () {
if (this.state.completionActive) this.state.completionActive.close()
})
function Completion(cm, options) {
this.cm = cm;
this.options = options;
this.widget = null;
this.debounce = 0;
this.tick = 0;
this.startPos = this.cm.getCursor("start");
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
if (this.options.updateOnCursorActivity) {
var self = this;
cm.on("cursorActivity", this.activityFunc = function () {
self.cursorActivity();
});
}
}
var requestAnimationFrame = window.requestAnimationFrame || function (fn) {
return setTimeout(fn, 1000 / 60);
};
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
Completion.prototype = {
close: function () {
if (!this.active()) return;
this.cm.state.completionActive = null;
this.tick = null;
if (this.options.updateOnCursorActivity) {
this.cm.off("cursorActivity", this.activityFunc);
}
if (this.widget && this.data) CodeMirror.signal(this.data, "close");
if (this.widget) this.widget.close();
CodeMirror.signal(this.cm, "endCompletion", this.cm);
},
active: function () {
return this.cm.state.completionActive == this;
},
pick: function (data, i) {
var completion = data.list[i], self = this;
this.cm.operation(function () {
if (completion.hint)
completion.hint(self.cm, data, completion);
else
self.cm.replaceRange(getText(completion), completion.from || data.from,
completion.to || data.to, "complete");
CodeMirror.signal(data, "pick", completion);
self.cm.scrollIntoView();
});
if (this.options.closeOnPick) {
this.close();
}
},
cursorActivity: function () {
if (this.debounce) {
cancelAnimationFrame(this.debounce);
this.debounce = 0;
}
var identStart = this.startPos;
if (this.data) {
identStart = this.data.from;
}
var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
pos.ch < identStart.ch || this.cm.somethingSelected() ||
(!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
this.close();
} else {
var self = this;
this.debounce = requestAnimationFrame(function () {
self.update();
});
if (this.widget) this.widget.disable();
}
},
update: function (first) {
if (this.tick == null) return
var self = this, myTick = ++this.tick
fetchHints(this.options.hint, this.cm, this.options, function (data) {
if (self.tick == myTick) self.finishUpdate(data, first)
})
},
finishUpdate: function (data, first) {
if (this.data) CodeMirror.signal(this.data, "update");
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
if (this.widget) this.widget.close();
this.data = data;
if (data && data.list.length) {
if (picked && data.list.length == 1) {
this.pick(data, 0);
} else {
this.widget = new Widget(this, data);
CodeMirror.signal(data, "shown");
}
}
}
};
function parseOptions(cm, pos, options) {
var editor = cm.options.hintOptions;
var out = {};
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
if (editor) for (var prop in editor)
if (editor[prop] !== undefined) out[prop] = editor[prop];
if (options) for (var prop in options)
if (options[prop] !== undefined) out[prop] = options[prop];
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
return out;
}
function getText(completion) {
if (typeof completion == "string") return completion;
else return completion.text;
}
function buildKeyMap(completion, handle) {
var baseMap = {
Up: function () {
handle.moveFocus(-1);
},
Down: function () {
handle.moveFocus(1);
},
PageUp: function () {
handle.moveFocus(-handle.menuSize() + 1, true);
},
PageDown: function () {
handle.moveFocus(handle.menuSize() - 1, true);
},
Home: function () {
handle.setFocus(0);
},
End: function () {
handle.setFocus(handle.length - 1);
},
// Enter: handle.pick,
// Tab: handle.pick,
Tab: keyboardEnter,
Enter: keyboardEnter,
Esc: handle.close
};
var mac = /Mac/.test(navigator.platform);
if (mac) {
baseMap["Ctrl-P"] = function () {
handle.moveFocus(-1);
};
baseMap["Ctrl-N"] = function () {
handle.moveFocus(1);
};
}
var custom = completion.options.customKeys;
var ourMap = custom ? {} : baseMap;
function addBinding(key, val) {
var bound;
if (typeof val != "string")
bound = function (cm) {
return val(cm, handle);
};
// This mechanism is deprecated
else if (baseMap.hasOwnProperty(val))
bound = baseMap[val];
else
bound = val;
ourMap[key] = bound;
}
if (custom)
for (var key in custom) if (custom.hasOwnProperty(key))
addBinding(key, custom[key]);
var extra = completion.options.extraKeys;
if (extra)
for (var key in extra) if (extra.hasOwnProperty(key))
addBinding(key, extra[key]);
return ourMap;
}
function getHintElement(hintsElement, el) {
while (el && el != hintsElement) {
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
el = el.parentNode;
}
}
function Widget(completion, data) {
this.id = "cm-complete-" + Math.floor(Math.random(1e6))
this.completion = completion;
this.data = data;
this.picked = false;
var widget = this, cm = completion.cm;
var ownerDocument = cm.getInputField().ownerDocument;
var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow;
var hints = this.hints = ownerDocument.createElement("ul");
// $(hints).append(`
// <h1>lalallalla</h1>
// `)
hints.setAttribute("role", "listbox")
hints.setAttribute("aria-expanded", "true")
hints.id = this.id
var theme = completion.cm.options.theme;
hints.className = "CodeMirror-hints " + theme;
this.selectedHint = data.selectedHint || 0;
var completions = data.list;
for (var i = 0; i < completions.length; ++i) {
var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i];
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
if (cur.className != null) className = cur.className + " " + className;
elt.className = className;
if (i == this.selectedHint) elt.setAttribute("aria-selected", "true")
elt.id = this.id + "-" + i
elt.setAttribute("role", "option")
if (cur.render) cur.render(elt, data, cur);
else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur)));
elt.hintId = i;
}
var container = completion.options.container || ownerDocument.body;
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
var left = pos.left, top = pos.bottom, below = true;
var offsetLeft = 0, offsetTop = 0;
if (container !== ownerDocument.body) {
// We offset the cursor position because left and top are relative to the offsetParent's top left corner.
var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1;
var offsetParent = isContainerPositioned ? container : container.offsetParent;
var offsetParentPosition = offsetParent.getBoundingClientRect();
var bodyPosition = ownerDocument.body.getBoundingClientRect();
offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft);
offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop);
}
hints.style.left = (left - offsetLeft) + "px";
hints.style.top = (top - offsetTop) + "px";
// todo 隐藏codemirror自带的提示
hints.style.display = 'none'
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth);
var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight);
//不用默认的DOM 下拉提示
// container.appendChild(hints);
hints.remove()
cm.getInputField().setAttribute("aria-autocomplete", "list")
cm.getInputField().setAttribute("aria-owns", this.id)
cm.getInputField().setAttribute("aria-activedescendant", this.id + "-" + this.selectedHint)
var box = completion.options.moveOnOverlap ? hints.getBoundingClientRect() : new DOMRect();
var scrolls = completion.options.paddingForScrollbar ? hints.scrollHeight > hints.clientHeight + 1 : false;
// Compute in the timeout to avoid reflow on init
var startScroll;
setTimeout(function () {
startScroll = cm.getScrollInfo();
});
var overlapY = box.bottom - winH;
if (overlapY > 0) {
var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
if (curTop - height > 0) { // Fits above cursor
hints.style.top = (top = pos.top - height - offsetTop) + "px";
below = false;
} else if (height > winH) {
hints.style.height = (winH - 5) + "px";
hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px";
var cursor = cm.getCursor();
if (data.from.ch != cursor.ch) {
pos = cm.cursorCoords(cursor);
hints.style.left = (left = pos.left - offsetLeft) + "px";
box = hints.getBoundingClientRect();
}
}
}
var overlapX = box.right - winW;
if (scrolls) overlapX += cm.display.nativeBarWidth;
if (overlapX > 0) {
if (box.right - box.left > winW) {
hints.style.width = (winW - 5) + "px";
overlapX -= (box.right - box.left) - winW;
}
hints.style.left = (left = Math.max(pos.left - overlapX - offsetLeft, 0)) + "px";
}
if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
node.style.paddingRight = cm.display.nativeBarWidth + "px"
// debugger
cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
moveFocus: function (n, avoidWrap) {
n === -1 ? keyboardUp && keyboardUp() : keyboardDown && keyboardDown()
widget.changeActive(widget.selectedHint + n, avoidWrap);
},
setFocus: function (n) {
widget.changeActive(n);
},
menuSize: function () {
return widget.screenAmount();
},
length: completions.length,
close: function () {
completion.close();
},
pick: function () {
widget.pick();
},
data: data
}));
if (completion.options.closeOnUnfocus) {
var closingOnBlur;
cm.on("blur", this.onBlur = function () {
closingOnBlur = setTimeout(function () {
completion.close();
}, 100);
});
cm.on("focus", this.onFocus = function () {
clearTimeout(closingOnBlur);
});
}
cm.on("scroll", this.onScroll = function () {
var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
if (!startScroll) startScroll = cm.getScrollInfo();
var newTop = top + startScroll.top - curScroll.top;
var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop);
if (!below) point += hints.offsetHeight;
if (point <= editor.top || point >= editor.bottom) return completion.close();
hints.style.top = newTop + "px";
hints.style.left = (left + startScroll.left - curScroll.left) + "px";
});
CodeMirror.on(hints, "dblclick", function (e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {
widget.changeActive(t.hintId);
widget.pick();
}
});
CodeMirror.on(hints, "click", function (e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {
widget.changeActive(t.hintId);
if (completion.options.completeOnSingleClick) widget.pick();
}
});
CodeMirror.on(hints, "mousedown", function () {
setTimeout(function () {
cm.focus();
}, 20);
});
// The first hint doesn't need to be scrolled to on init
var selectedHintRange = this.getSelectedHintRange();
if (selectedHintRange.from !== 0 || selectedHintRange.to !== 0) {
this.scrollToActive();
}
CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
return true;
}
Widget.prototype = {
close: function () {
if (this.completion.widget != this) return;
this.completion.widget = null;
if (this.hints.parentNode) this.hints.parentNode.removeChild(this.hints);
this.completion.cm.removeKeyMap(this.keyMap);
var input = this.completion.cm.getInputField()
input.removeAttribute("aria-activedescendant")
input.removeAttribute("aria-owns")
var cm = this.completion.cm;
if (this.completion.options.closeOnUnfocus) {
cm.off("blur", this.onBlur);
cm.off("focus", this.onFocus);
}
cm.off("scroll", this.onScroll);
},
disable: function () {
this.completion.cm.removeKeyMap(this.keyMap);
var widget = this;
this.keyMap = {
Enter: function () {
widget.picked = true;
}
};
this.completion.cm.addKeyMap(this.keyMap);
},
pick: function () {
this.completion.pick(this.data, this.selectedHint);
},
changeActive: function (i, avoidWrap) {
if (i >= this.data.list.length)
i = avoidWrap ? this.data.list.length - 1 : 0;
else if (i < 0)
i = avoidWrap ? 0 : this.data.list.length - 1;
if (this.selectedHint == i) return;
var node = this.hints.childNodes[this.selectedHint];
if (node) {
node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
node.removeAttribute("aria-selected")
}
node = this.hints.childNodes[this.selectedHint = i];
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
node.setAttribute("aria-selected", "true")
this.completion.cm.getInputField().setAttribute("aria-activedescendant", node.id)
this.scrollToActive()
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
},
scrollToActive: function () {
var selectedHintRange = this.getSelectedHintRange();
var node1 = this.hints.childNodes[selectedHintRange.from];
var node2 = this.hints.childNodes[selectedHintRange.to];
var firstNode = this.hints.firstChild;
if (node1.offsetTop < this.hints.scrollTop)
this.hints.scrollTop = node1.offsetTop - firstNode.offsetTop;
else if (node2.offsetTop + node2.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
this.hints.scrollTop = node2.offsetTop + node2.offsetHeight - this.hints.clientHeight + firstNode.offsetTop;
},
screenAmount: function () {
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
},
getSelectedHintRange: function () {
var margin = this.completion.options.scrollMargin || 0;
return {
from: Math.max(0, this.selectedHint - margin),
to: Math.min(this.data.list.length - 1, this.selectedHint + margin),
};
}
};
function applicableHelpers(cm, helpers) {
if (!cm.somethingSelected()) return helpers
var result = []
for (var i = 0; i < helpers.length; i++)
if (helpers[i].supportsSelection) result.push(helpers[i])
return result
}
function fetchHints(hint, cm, options, callback) {
if (hint.async) {
hint(cm, callback, options)
} else {
var result = hint(cm, options)
if (result && result.then) result.then(callback)
else callback(result)
}
}
function resolveAutoHints(cm, pos) {
var helpers = cm.getHelpers(pos, "hint"), words
if (helpers.length) {
var resolved = function (cm, callback, options) {
var app = applicableHelpers(cm, helpers);
function run(i) {
if (i == app.length) return callback(null)
fetchHints(app[i], cm, options, function (result) {
if (result && result.list.length > 0) callback(result)
else run(i + 1)
})
}
run(0)
}
resolved.async = true
resolved.supportsSelection = true
return resolved
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
return function (cm) {
return CodeMirror.hint.fromList(cm, {words: words})
}
} else if (CodeMirror.hint.anyword) {
return function (cm, options) {
return CodeMirror.hint.anyword(cm, options)
}
} else {
return function () {
}
}
}
CodeMirror.registerHelper("hint", "auto", {
resolve: resolveAutoHints
});
CodeMirror.registerHelper("hint", "fromList", function (cm, options) {
var cur = cm.getCursor(), token = cm.getTokenAt(cur)
var term, from = CodeMirror.Pos(cur.line, token.start), to = cur
if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
term = token.string.substr(0, cur.ch - token.start)
} else {
term = ""
from = cur
}
var found = [];
for (var i = 0; i < options.words.length; i++) {
var word = options.words[i];
if (word.slice(0, term.length) == term)
found.push(word);
}
if (found.length) return {list: found, from: from, to: to};
});
CodeMirror.commands.autocomplete = CodeMirror.showHint;
var defaultOptions = {
hint: CodeMirror.hint.auto,
completeSingle: true,
alignWithWord: true,
closeCharacters: /[\s()\[\]{};:>,]/,
closeOnPick: false,
closeOnUnfocus: false, //阻止提示信息 失焦关闭
// closeOnUnfocus: false,
updateOnCursorActivity: true,
completeOnSingleClick: true,
container: null,
customKeys: null,
extraKeys: null,
paddingForScrollbar: true,
moveOnOverlap: true,
};
CodeMirror.defineOption("hintOptions", null);
}

View File

@@ -0,0 +1,227 @@
//正则 向前去找关键字
/* 用于匹配关系数据 */
function matchOperator(CodeMirror, hintParams = {}) {
var editor = hintParams.editor;
var Pos = CodeMirror.Pos
var cur = hintParams.cur;
var token = hintParams.token;
if (!editor) {
return
}
var start = token.start;
var cont = true;
var leftTokenGroup = [];
while (cont) {
start = token.start;
leftTokenGroup.unshift(token);
token = editor.getTokenAt(Pos(cur.line, token.start));
cont = !(token.string.match(/^[ ]*$/) || start === 0); //只用空格做终止条件
}
var cursorLeftString = editor.getRange(Pos(cur.line, leftTokenGroup[0].start), Pos(cur.line, leftTokenGroup[leftTokenGroup.length - 1].end))
//判断是不是满足 运算符 表达式的正则
//test 如果是ig 会改变正则指针: https://my.oschina.net/jamesview/blog/5460753
var reg = /^(.*?)(=|!=|>|<|>=|<=)([^ ]*?)$/;
if (reg.test(cursorLeftString)) {
var execArr = reg.exec(cursorLeftString) || []
return {
leftTokenGroup,
cursorLeftString,
label: execArr[1],
sign: execArr[2],
value: execArr[3],
}
}
return null
}
function matchCommon(CodeMirror, hintParams = {}) {
//通用情况 QUANTILE(expr,level) 左括号右侧第一个就是 关键字
var editor = hintParams.editor;
var Pos = CodeMirror.Pos
var cur = hintParams.cur;
var token = hintParams.token;
if (!editor) {
return
}
var start = token.start;
var cont = true;
var leftTokenGroup = [];
while (cont) {
start = token.start;
leftTokenGroup.unshift(token);
token = editor.getTokenAt(Pos(cur.line, token.start));
cont = !(token.string.match(/^[ (]*$/) || start === 0); //括号或者空格为终止条件
//括号补上
if (token.string === '(') {
leftTokenGroup.unshift(token);
}
}
var cursorLeftString = editor.getRange(Pos(cur.line, leftTokenGroup[0].start), Pos(cur.line, leftTokenGroup[leftTokenGroup.length - 1].end))
//判断是不是满足 运算符 表达式的正则
var reg = /^\((.*?),([^ ]*)$/;
if (reg.test(cursorLeftString)) {
var execArr = reg.exec(cursorLeftString) || []
return {
leftTokenGroup,
cursorLeftString,
label: execArr[1],
sign: 'unknown', //没必要判断
value: execArr[2],
}
}
return null
}
function matchIn(CodeMirror, hintParams = {}) {
//in 的情况比较特殊
//通用情况 expr not in (values) expr in (values)
var editor = hintParams.editor;
var Pos = CodeMirror.Pos
var cur = hintParams.cur;
var token = hintParams.token;
if (!editor) {
return
}
var start = token.start;
var cont = true;
var leftTokenGroup = [];
//找到左括号
while (cont) {
start = token.start;
leftTokenGroup.unshift(token);
token = editor.getTokenAt(Pos(cur.line, token.start));
cont = !(token.string.match(/^[ (]*$/) || start === 0); //括号或者空格为终止条件
//左括号补上
if (token.string === '(') {
leftTokenGroup.unshift(token);
}
}
//左括号继续向右找
cont = true
while (cont) {
start = token.start;
leftTokenGroup.unshift(token);
token = editor.getTokenAt(Pos(cur.line, token.start));
cont = !(!token.string.match(/^(in|not| )/g) || start === 0); //括号或者空格为终止条件
//string-2
if (token.type === 'string-2') {
leftTokenGroup.unshift(token);
}
}
var cursorLeftString = editor.getRange(Pos(cur.line, leftTokenGroup[0].start), Pos(cur.line, leftTokenGroup[leftTokenGroup.length - 1].end))
//判断是不是满足 运算符 表达式的正则
var reg = /^(.*?)[ ]+((?:not[ ]+)?in)[ ]*\(([^ ]*?)$/i;
if (reg.test(cursorLeftString)) {
var execArr = reg.exec(cursorLeftString) || []
return {
leftTokenGroup,
cursorLeftString,
label: execArr[1],
sign: execArr[2],
value: execArr[3],
}
}
return null
}
function matchLike(CodeMirror, hintParams = {}) {
//like 的情况比较特殊 expr like value , expr not like value
var editor = hintParams.editor;
var Pos = CodeMirror.Pos
var cur = hintParams.cur;
var token = hintParams.token;
if (!editor) {
return
}
var start = token.start;
var cont = true;
var leftTokenGroup = [];
//找到左括号
while (cont) {
start = token.start;
leftTokenGroup.unshift(token);
token = editor.getTokenAt(Pos(cur.line, token.start));
cont = !(token.string.match(/^[ ]*$/) || start === 0); //括号或者空格为终止条件
}
//左括号继续向右找
cont = true
while (cont) {
start = token.start;
leftTokenGroup.unshift(token);
token = editor.getTokenAt(Pos(cur.line, token.start));
cont = !(!token.string.match(/^(like|not| )/g) || start === 0); //括号或者空格为终止条件
//string-2
if (token.type === 'string-2') {
leftTokenGroup.unshift(token);
}
}
var cursorLeftString = editor.getRange(Pos(cur.line, leftTokenGroup[0].start), Pos(cur.line, leftTokenGroup[leftTokenGroup.length - 1].end))
//判断是不是满足 运算符 表达式的正则
var reg = /^(.*?)[ ]+((?:not[ ]+)?like)[ ]*([^ ]*?)$/i;
if (reg.test(cursorLeftString)) {
var execArr = reg.exec(cursorLeftString) || []
return {
leftTokenGroup,
cursorLeftString,
label: execArr[1],
sign: execArr[2],
value: execArr[3],
}
}
return null
}
export const matchMain = (CodeMirror, params, manualParams = {}) => {
var matchRes = null
//匹配 运算符表达式
matchRes = matchOperator(CodeMirror, manualParams)
if (matchRes) {
return matchRes
}
//匹配 in表达式
matchRes = matchIn(CodeMirror, manualParams)
if (matchRes) {
return matchRes
}
//匹配 like表达式
matchRes = matchLike(CodeMirror, manualParams)
if (matchRes) {
return matchRes
}
//这里缺少一个对 count(distinct expr) 模式的匹配, 感觉大数据涉及存在缺陷, 先暂时不写
//匹配 其他表达式 (expr,value1,value2....) 模式的匹配
matchRes = matchCommon(CodeMirror, manualParams)
if (matchRes) {
// 说明这是一个 运算符表达式
return matchRes
}
return matchRes
}

View File

@@ -0,0 +1,591 @@
export default function showHint(CodeMirror) {
"use strict";
var HINT_ELEMENT_CLASS = "CodeMirror-hint";
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
// This is the old interface, kept around for now to stay
// backwards-compatible.
CodeMirror.showHint = function (cm, getHints, options) {
if (!getHints) return cm.showHint(options);
if (options && options.async) getHints.async = true;
var newOpts = {hint: getHints};
if (options) for (var prop in options) newOpts[prop] = options[prop];
return cm.showHint(newOpts);
};
CodeMirror.defineExtension("showHint", function (options) {
options = parseOptions(this, this.getCursor("start"), options);
var selections = this.listSelections()
if (selections.length > 1) return;
// By default, don't allow completion when something is selected.
// A hint function can have a `supportsSelection` property to
// indicate that it can handle selections.
if (this.somethingSelected()) {
if (!options.hint.supportsSelection) return;
// Don't try with cross-line selections
for (var i = 0; i < selections.length; i++)
if (selections[i].head.line != selections[i].anchor.line) return;
}
if (this.state.completionActive) this.state.completionActive.close();
var completion = this.state.completionActive = new Completion(this, options);
if (!completion.options.hint) return;
CodeMirror.signal(this, "startCompletion", this);
completion.update(true);
});
CodeMirror.defineExtension("closeHint", function () {
if (this.state.completionActive) this.state.completionActive.close()
})
function Completion(cm, options) {
this.cm = cm;
this.options = options;
this.widget = null;
this.debounce = 0;
this.tick = 0;
this.startPos = this.cm.getCursor("start");
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
if (this.options.updateOnCursorActivity) {
var self = this;
cm.on("cursorActivity", this.activityFunc = function () {
self.cursorActivity();
});
}
}
var requestAnimationFrame = window.requestAnimationFrame || function (fn) {
return setTimeout(fn, 1000 / 60);
};
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
Completion.prototype = {
close: function () {
if (!this.active()) return;
this.cm.state.completionActive = null;
this.tick = null;
if (this.options.updateOnCursorActivity) {
this.cm.off("cursorActivity", this.activityFunc);
}
if (this.widget && this.data) CodeMirror.signal(this.data, "close");
if (this.widget) this.widget.close();
CodeMirror.signal(this.cm, "endCompletion", this.cm);
},
active: function () {
return this.cm.state.completionActive == this;
},
pick: function (data, i) {
var completion = data.list[i], self = this;
this.cm.operation(function () {
if (completion.hint)
completion.hint(self.cm, data, completion);
else
self.cm.replaceRange(getText(completion), completion.from || data.from,
completion.to || data.to, "complete");
CodeMirror.signal(data, "pick", completion);
self.cm.scrollIntoView();
});
if (this.options.closeOnPick) {
this.close();
}
},
cursorActivity: function () {
if (this.debounce) {
cancelAnimationFrame(this.debounce);
this.debounce = 0;
}
var identStart = this.startPos;
if (this.data) {
identStart = this.data.from;
}
var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line);
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
pos.ch < identStart.ch || this.cm.somethingSelected() ||
(!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
this.close();
} else {
var self = this;
this.debounce = requestAnimationFrame(function () {
self.update();
});
if (this.widget) this.widget.disable();
}
},
update: function (first) {
if (this.tick == null) return
var self = this, myTick = ++this.tick
fetchHints(this.options.hint, this.cm, this.options, function (data) {
if (self.tick == myTick) self.finishUpdate(data, first)
})
},
finishUpdate: function (data, first) {
if (this.data) CodeMirror.signal(this.data, "update");
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
if (this.widget) this.widget.close();
this.data = data;
if (data && data.list.length) {
if (picked && data.list.length == 1) {
this.pick(data, 0);
} else {
this.widget = new Widget(this, data);
CodeMirror.signal(data, "shown");
}
}
}
};
function parseOptions(cm, pos, options) {
var editor = cm.options.hintOptions;
var out = {};
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
if (editor) for (var prop in editor)
if (editor[prop] !== undefined) out[prop] = editor[prop];
if (options) for (var prop in options)
if (options[prop] !== undefined) out[prop] = options[prop];
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
return out;
}
function getText(completion) {
if (typeof completion == "string") return completion;
else return completion.text;
}
function buildKeyMap(completion, handle) {
var baseMap = {
Up: function () {
handle.moveFocus(-1);
},
Down: function () {
handle.moveFocus(1);
},
PageUp: function () {
handle.moveFocus(-handle.menuSize() + 1, true);
},
PageDown: function () {
handle.moveFocus(handle.menuSize() - 1, true);
},
Home: function () {
handle.setFocus(0);
},
End: function () {
handle.setFocus(handle.length - 1);
},
Enter: handle.pick,
Tab: handle.pick,
Esc: handle.close
};
var mac = /Mac/.test(navigator.platform);
if (mac) {
baseMap["Ctrl-P"] = function () {
handle.moveFocus(-1);
};
baseMap["Ctrl-N"] = function () {
handle.moveFocus(1);
};
}
var custom = completion.options.customKeys;
var ourMap = custom ? {} : baseMap;
function addBinding(key, val) {
var bound;
if (typeof val != "string")
bound = function (cm) {
return val(cm, handle);
};
// This mechanism is deprecated
else if (baseMap.hasOwnProperty(val))
bound = baseMap[val];
else
bound = val;
ourMap[key] = bound;
}
if (custom)
for (var key in custom) if (custom.hasOwnProperty(key))
addBinding(key, custom[key]);
var extra = completion.options.extraKeys;
if (extra)
for (var key in extra) if (extra.hasOwnProperty(key))
addBinding(key, extra[key]);
return ourMap;
}
function getHintElement(hintsElement, el) {
while (el && el != hintsElement) {
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
el = el.parentNode;
}
}
function Widget(completion, data) {
this.id = "cm-complete-" + Math.floor(Math.random(1e6))
this.completion = completion;
this.data = data;
this.picked = false;
var widget = this, cm = completion.cm;
var ownerDocument = cm.getInputField().ownerDocument;
var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow;
var hints = this.hints = ownerDocument.createElement("ul");
// $(hints).append(`
// <h1>lalallalla</h1>
// `)
hints.setAttribute("role", "listbox")
hints.setAttribute("aria-expanded", "true")
hints.id = this.id
var theme = completion.cm.options.theme;
hints.className = "CodeMirror-hints " + theme;
this.selectedHint = data.selectedHint || 0;
var completions = data.list;
for (var i = 0; i < completions.length; ++i) {
var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i];
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
if (cur.className != null) className = cur.className + " " + className;
elt.className = className;
if (i == this.selectedHint) elt.setAttribute("aria-selected", "true")
elt.id = this.id + "-" + i
elt.setAttribute("role", "option")
if (cur.render) cur.render(elt, data, cur);
else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur)));
elt.hintId = i;
}
var container = completion.options.container || ownerDocument.body;
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
var left = pos.left, top = pos.bottom, below = true;
var offsetLeft = 0, offsetTop = 0;
if (container !== ownerDocument.body) {
// We offset the cursor position because left and top are relative to the offsetParent's top left corner.
var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1;
var offsetParent = isContainerPositioned ? container : container.offsetParent;
var offsetParentPosition = offsetParent.getBoundingClientRect();
var bodyPosition = ownerDocument.body.getBoundingClientRect();
offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft);
offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop);
}
hints.style.left = (left - offsetLeft) + "px";
hints.style.top = (top - offsetTop) + "px";
// todo 隐藏codemirror自带的提示
hints.style.display = 'none'
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth);
var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight);
//在这里 添加的DOM 元素 -- 该方案 实现复杂算了吧
container.appendChild(hints);
// debugger
// $(container).append(
// `
// <div>
// <h1>hahah</h1>
// ${ hints }
// </div>
// `
// )
cm.getInputField().setAttribute("aria-autocomplete", "list")
cm.getInputField().setAttribute("aria-owns", this.id)
cm.getInputField().setAttribute("aria-activedescendant", this.id + "-" + this.selectedHint)
var box = completion.options.moveOnOverlap ? hints.getBoundingClientRect() : new DOMRect();
var scrolls = completion.options.paddingForScrollbar ? hints.scrollHeight > hints.clientHeight + 1 : false;
// Compute in the timeout to avoid reflow on init
var startScroll;
setTimeout(function () {
startScroll = cm.getScrollInfo();
});
var overlapY = box.bottom - winH;
if (overlapY > 0) {
var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
if (curTop - height > 0) { // Fits above cursor
hints.style.top = (top = pos.top - height - offsetTop) + "px";
below = false;
} else if (height > winH) {
hints.style.height = (winH - 5) + "px";
hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px";
var cursor = cm.getCursor();
if (data.from.ch != cursor.ch) {
pos = cm.cursorCoords(cursor);
hints.style.left = (left = pos.left - offsetLeft) + "px";
box = hints.getBoundingClientRect();
}
}
}
var overlapX = box.right - winW;
if (scrolls) overlapX += cm.display.nativeBarWidth;
if (overlapX > 0) {
if (box.right - box.left > winW) {
hints.style.width = (winW - 5) + "px";
overlapX -= (box.right - box.left) - winW;
}
hints.style.left = (left = Math.max(pos.left - overlapX - offsetLeft, 0)) + "px";
}
if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling)
node.style.paddingRight = cm.display.nativeBarWidth + "px"
// debugger
cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
moveFocus: function (n, avoidWrap) {
widget.changeActive(widget.selectedHint + n, avoidWrap);
},
setFocus: function (n) {
widget.changeActive(n);
},
menuSize: function () {
return widget.screenAmount();
},
length: completions.length,
close: function () {
completion.close();
},
pick: function () {
widget.pick();
},
data: data
}));
if (completion.options.closeOnUnfocus) {
var closingOnBlur;
cm.on("blur", this.onBlur = function () {
closingOnBlur = setTimeout(function () {
completion.close();
}, 100);
});
cm.on("focus", this.onFocus = function () {
clearTimeout(closingOnBlur);
});
}
cm.on("scroll", this.onScroll = function () {
var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
if (!startScroll) startScroll = cm.getScrollInfo();
var newTop = top + startScroll.top - curScroll.top;
var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop);
if (!below) point += hints.offsetHeight;
if (point <= editor.top || point >= editor.bottom) return completion.close();
hints.style.top = newTop + "px";
hints.style.left = (left + startScroll.left - curScroll.left) + "px";
});
CodeMirror.on(hints, "dblclick", function (e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {
widget.changeActive(t.hintId);
widget.pick();
}
});
CodeMirror.on(hints, "click", function (e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {
widget.changeActive(t.hintId);
if (completion.options.completeOnSingleClick) widget.pick();
}
});
CodeMirror.on(hints, "mousedown", function () {
setTimeout(function () {
cm.focus();
}, 20);
});
// The first hint doesn't need to be scrolled to on init
var selectedHintRange = this.getSelectedHintRange();
if (selectedHintRange.from !== 0 || selectedHintRange.to !== 0) {
this.scrollToActive();
}
CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
return true;
}
Widget.prototype = {
close: function () {
if (this.completion.widget != this) return;
this.completion.widget = null;
if (this.hints.parentNode) this.hints.parentNode.removeChild(this.hints);
this.completion.cm.removeKeyMap(this.keyMap);
var input = this.completion.cm.getInputField()
input.removeAttribute("aria-activedescendant")
input.removeAttribute("aria-owns")
var cm = this.completion.cm;
if (this.completion.options.closeOnUnfocus) {
cm.off("blur", this.onBlur);
cm.off("focus", this.onFocus);
}
cm.off("scroll", this.onScroll);
},
disable: function () {
this.completion.cm.removeKeyMap(this.keyMap);
var widget = this;
this.keyMap = {
Enter: function () {
widget.picked = true;
}
};
this.completion.cm.addKeyMap(this.keyMap);
},
pick: function () {
this.completion.pick(this.data, this.selectedHint);
},
changeActive: function (i, avoidWrap) {
if (i >= this.data.list.length)
i = avoidWrap ? this.data.list.length - 1 : 0;
else if (i < 0)
i = avoidWrap ? 0 : this.data.list.length - 1;
if (this.selectedHint == i) return;
var node = this.hints.childNodes[this.selectedHint];
if (node) {
node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
node.removeAttribute("aria-selected")
}
node = this.hints.childNodes[this.selectedHint = i];
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
node.setAttribute("aria-selected", "true")
this.completion.cm.getInputField().setAttribute("aria-activedescendant", node.id)
this.scrollToActive()
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
},
scrollToActive: function () {
var selectedHintRange = this.getSelectedHintRange();
var node1 = this.hints.childNodes[selectedHintRange.from];
var node2 = this.hints.childNodes[selectedHintRange.to];
var firstNode = this.hints.firstChild;
if (node1.offsetTop < this.hints.scrollTop)
this.hints.scrollTop = node1.offsetTop - firstNode.offsetTop;
else if (node2.offsetTop + node2.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
this.hints.scrollTop = node2.offsetTop + node2.offsetHeight - this.hints.clientHeight + firstNode.offsetTop;
},
screenAmount: function () {
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
},
getSelectedHintRange: function () {
var margin = this.completion.options.scrollMargin || 0;
return {
from: Math.max(0, this.selectedHint - margin),
to: Math.min(this.data.list.length - 1, this.selectedHint + margin),
};
}
};
function applicableHelpers(cm, helpers) {
if (!cm.somethingSelected()) return helpers
var result = []
for (var i = 0; i < helpers.length; i++)
if (helpers[i].supportsSelection) result.push(helpers[i])
return result
}
function fetchHints(hint, cm, options, callback) {
if (hint.async) {
hint(cm, callback, options)
} else {
var result = hint(cm, options)
if (result && result.then) result.then(callback)
else callback(result)
}
}
function resolveAutoHints(cm, pos) {
var helpers = cm.getHelpers(pos, "hint"), words
if (helpers.length) {
var resolved = function (cm, callback, options) {
var app = applicableHelpers(cm, helpers);
function run(i) {
if (i == app.length) return callback(null)
fetchHints(app[i], cm, options, function (result) {
if (result && result.list.length > 0) callback(result)
else run(i + 1)
})
}
run(0)
}
resolved.async = true
resolved.supportsSelection = true
return resolved
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
return function (cm) {
return CodeMirror.hint.fromList(cm, {words: words})
}
} else if (CodeMirror.hint.anyword) {
return function (cm, options) {
return CodeMirror.hint.anyword(cm, options)
}
} else {
return function () {
}
}
}
CodeMirror.registerHelper("hint", "auto", {
resolve: resolveAutoHints
});
CodeMirror.registerHelper("hint", "fromList", function (cm, options) {
var cur = cm.getCursor(), token = cm.getTokenAt(cur)
var term, from = CodeMirror.Pos(cur.line, token.start), to = cur
if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
term = token.string.substr(0, cur.ch - token.start)
} else {
term = ""
from = cur
}
var found = [];
for (var i = 0; i < options.words.length; i++) {
var word = options.words[i];
if (word.slice(0, term.length) == term)
found.push(word);
}
if (found.length) return {list: found, from: from, to: to};
});
CodeMirror.commands.autocomplete = CodeMirror.showHint;
var defaultOptions = {
hint: CodeMirror.hint.auto,
completeSingle: true,
alignWithWord: true,
closeCharacters: /[\s()\[\]{};:>,]/,
closeOnPick: true,
closeOnUnfocus: true,
updateOnCursorActivity: true,
completeOnSingleClick: true,
container: null,
customKeys: null,
extraKeys: null,
paddingForScrollbar: true,
moveOnOverlap: true,
};
CodeMirror.defineOption("hintOptions", null);
}

View File

@@ -0,0 +1,372 @@
import {cloneDeep} from 'lodash'
import {matchMain} from './matchRelatedInfo'
export default function (CodeMirror,
{
dataset,
hinthook, //生成提示的hook 拦截
keywordshook //关键字hook 在关键字里面
}
) {
// "use strict";
var tables;
var defaultTable;
var keywords;
var identifierQuote;
var CONS = {
QUERY_DIV: ";",
ALIAS_KEYWORD: "AS"
};
var Pos = CodeMirror.Pos, cmpPos = CodeMirror.cmpPos;
function isArray(val) {
return Object.prototype.toString.call(val) == "[object Array]"
}
function getKeywords(editor) {
var mode = editor.doc.modeOption;
if (mode === "sql") mode = "text/x-sql";
keywords = CodeMirror.resolveMode(mode).keywords;
if (keywordshook) {
keywords = keywordshook(keywords, CodeMirror.resolveMode(mode)) || keywords
}
return keywords
}
function getIdentifierQuote(editor) {
var mode = editor.doc.modeOption;
if (mode === "sql") mode = "text/x-sql";
return CodeMirror.resolveMode(mode).identifierQuote || "`";
}
function getText(item) {
return typeof item == "string" ? item : item.text;
}
function wrapTable(name, value) {
if (isArray(value)) value = {columns: value}
if (!value.text) value.text = name
return value
}
function parseTables(input) {
//table 名称变大写 统一变成对象格式 columns text
var result = {}
if (isArray(input)) {
for (var i = input.length - 1; i >= 0; i--) {
var item = input[i]
result[getText(item).toUpperCase()] = wrapTable(getText(item), item)
}
} else if (input) {
for (var name in input)
result[name.toUpperCase()] = wrapTable(name, input[name])
}
return result
}
function getTable(name) {
return tables[name.toUpperCase()]
}
function shallowClone(object) {
var result = {};
for (var key in object) if (object.hasOwnProperty(key))
result[key] = object[key];
return result;
}
function match(string, word) {
var len = string.length;
var sub = getText(word).substr(0, len);
return string.toUpperCase() === sub.toUpperCase();
}
function addMatches(result, search, wordlist, formatter) {
if (isArray(wordlist)) {
for (var i = 0; i < wordlist.length; i++)
if (match(search, wordlist[i])) result.push(formatter(wordlist[i]))
} else {
for (var word in wordlist) if (wordlist.hasOwnProperty(word)) {
var val = wordlist[word]
if (!val || val === true)
val = word
else
val = val.displayText ? {text: val.text, displayText: val.displayText} : val.text
if (match(search, val)) result.push(formatter(val))
}
}
}
function cleanName(name) {
// Get rid name from identifierQuote and preceding dot(.)
if (name.charAt(0) == ".") {
name = name.substr(1);
}
// replace duplicated identifierQuotes with single identifierQuotes
// and remove single identifierQuotes
var nameParts = name.split(identifierQuote + identifierQuote);
for (var i = 0; i < nameParts.length; i++)
nameParts[i] = nameParts[i].replace(new RegExp(identifierQuote, "g"), "");
return nameParts.join(identifierQuote);
}
function insertIdentifierQuotes(name) {
var nameParts = getText(name).split(".");
for (var i = 0; i < nameParts.length; i++)
nameParts[i] = identifierQuote +
// duplicate identifierQuotes
nameParts[i].replace(new RegExp(identifierQuote, "g"), identifierQuote + identifierQuote) +
identifierQuote;
var escaped = nameParts.join(".");
if (typeof name == "string") return escaped;
name = shallowClone(name);
name.text = escaped;
return name;
}
function getLeftpart(cur, token, result, editor) {
var nameParts = [];
var start = token.start;
var cont = true;
while (cont) {
//全是空格 或者 是操作符 就接着往前找
cont = ((token.type === 'operator' || token.string.match(/^[ ]*$/)) && start !== 0);
start = token.start;
nameParts.unshift(token.string);
token = editor.getTokenAt(Pos(cur.line, token.start));
if (token.type === 'operator') {
cont = true;
token = editor.getTokenAt(Pos(cur.line, token.start));
}
}
return nameParts[0]
}
function isRightpart(cur, token, result, editor) {
token = editor.getTokenAt(Pos(cur.line, token.start));
return token.type === 'operator'
}
function nameCompletion(cur, token, result, editor) {
// Try to complete table, column names and return start position of completion
var useIdentifierQuotes = false;
var nameParts = [];
var start = token.start;
var cont = true;
while (cont) {
cont = (token.string.charAt(0) == ".");
useIdentifierQuotes = useIdentifierQuotes || (token.string.charAt(0) == identifierQuote);
start = token.start;
nameParts.unshift(cleanName(token.string));
token = editor.getTokenAt(Pos(cur.line, token.start));
if (token.string == ".") {
cont = true;
token = editor.getTokenAt(Pos(cur.line, token.start));
}
}
// Try to complete table names
var string = nameParts.join(".");
addMatches(result, string, tables, function (w) {
return useIdentifierQuotes ? insertIdentifierQuotes(w) : w;
});
// Try to complete columns from defaultTable
addMatches(result, string, defaultTable, function (w) {
return useIdentifierQuotes ? insertIdentifierQuotes(w) : w;
});
// Try to complete columns
string = nameParts.pop();
var table = nameParts.join(".");
var alias = false;
var aliasTable = table;
// Check if table is available. If not, find table by Alias
if (!getTable(table)) {
var oldTable = table;
table = findTableByAlias(table, editor);
if (table !== oldTable) alias = true;
}
var columns = getTable(table);
if (columns && columns.columns)
columns = columns.columns;
if (columns) {
addMatches(result, string, columns, function (w) {
var tableInsert = table;
if (alias == true) tableInsert = aliasTable;
if (typeof w == "string") {
w = tableInsert + "." + w;
} else {
w = shallowClone(w);
w.text = tableInsert + "." + w.text;
}
return useIdentifierQuotes ? insertIdentifierQuotes(w) : w;
});
}
return start;
}
function eachWord(lineText, f) {
var words = lineText.split(/\s+/)
for (var i = 0; i < words.length; i++)
if (words[i]) f(words[i].replace(/[`,;]/g, ''))
}
function findTableByAlias(alias, editor) {
var doc = editor.doc;
var fullQuery = doc.getValue();
var aliasUpperCase = alias.toUpperCase();
var previousWord = "";
var table = "";
var separator = [];
var validRange = {
start: Pos(0, 0),
end: Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).length)
};
//add separator
var indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV);
while (indexOfSeparator != -1) {
separator.push(doc.posFromIndex(indexOfSeparator));
indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV, indexOfSeparator + 1);
}
separator.unshift(Pos(0, 0));
separator.push(Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).text.length));
//find valid range
var prevItem = null;
var current = editor.getCursor()
for (var i = 0; i < separator.length; i++) {
if ((prevItem == null || cmpPos(current, prevItem) > 0) && cmpPos(current, separator[i]) <= 0) {
validRange = {start: prevItem, end: separator[i]};
break;
}
prevItem = separator[i];
}
if (validRange.start) {
var query = doc.getRange(validRange.start, validRange.end, false);
for (var i = 0; i < query.length; i++) {
var lineText = query[i];
eachWord(lineText, function (word) {
var wordUpperCase = word.toUpperCase();
if (wordUpperCase === aliasUpperCase && getTable(previousWord))
table = previousWord;
if (wordUpperCase !== CONS.ALIAS_KEYWORD)
previousWord = word;
});
if (table) break;
}
}
return table;
}
CodeMirror.registerHelper("hint", "sql", function (editor, options) {
tables = parseTables(options && options.tables);
var defaultTableName = options && options.defaultTable; //默认table 名称
var disableKeywords = options && options.disableKeywords; //禁用的keyword
defaultTable = defaultTableName && getTable(defaultTableName);
keywords = getKeywords(editor); //获取 定义defineMIME 时候的关键字参数
identifierQuote = getIdentifierQuote(editor); //获取 引用标识符
if (defaultTableName && !defaultTable)
defaultTable = findTableByAlias(defaultTableName, editor);
defaultTable = defaultTable || [];
if (defaultTable.columns)
defaultTable = defaultTable.columns;
var cur = editor.getCursor(); //line 当前行 ch 索引 sticky ??
var result = [];
var token = editor.getTokenAt(cur), start, end, search;
if (token.end > cur.ch) {
token.end = cur.ch;
token.string = token.string.slice(0, cur.ch - token.start);
}
//start end search 赋值
// todo 此处允许字符串包含 .
// if (token.string.match(/^[.`"'\w@][\w$#]*/g)) {
if (token.string.match(/^[.`"'\w@][\w#]*/g)) {
search = token.string;
start = token.start;
end = token.end;
} else {
start = end = cur.ch;
search = "";
}
//对引用标识符 . 的使用,关联table 列
if (search.charAt(0) == "." || search.charAt(0) == identifierQuote) {
start = nameCompletion(cur, token, result, editor);
} else {
var objectOrClass = function (w, className) {
if (typeof w === "object") {
w.className = className;
} else {
w = {text: w, className: className};
}
return w;
};
addMatches(result, search, defaultTable, function (w) {
return objectOrClass(w, "CodeMirror-hint-table CodeMirror-hint-default-table");
});
addMatches(
result,
search,
tables, function (w) {
return objectOrClass(w, "CodeMirror-hint-table");
}
);
if (!disableKeywords)
addMatches(result, search, keywords, function (w) {
return objectOrClass(w.toUpperCase(), "CodeMirror-hint-keyword");
});
}
//写入一个 钩子,在这里决定提示的内容
if (hinthook) {
var params = {
search, //搜索 关键字
keywords, //关键字列表
};
if (token.type === 'operator') {
params.leftpart = getLeftpart(cur, token, result, editor) || ''
}
if (isRightpart(cur, token, result, editor)) {
params.rightpart = token.string
}
/* 之后的参数 是为manualHinthook 预备的 */
var manualParams = {
search, //搜索 关键字
// keywords, //关键字列表 没啥用
from: Pos(cur.line, start),
to: Pos(cur.line, end),
leftpart: getLeftpart(cur, token, result, editor) || '',
rightpart: params.rightpart,
list: cloneDeep(result),
editor,
token,
cur
}
var refField = matchMain(CodeMirror, params, manualParams);
manualParams.refField = refField
manualParams.leftpart = refField?.label || manualParams.leftpart;
params.leftpart = refField?.label || params.leftpart;
result = hinthook(result, params, manualParams) || result
}
return {list: result, from: Pos(cur.line, start), to: Pos(cur.line, end)};
});
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,70 @@
export default {
enDefault: {
description () {
return (<div className='default-tips'>
<div class='default-tips-header'>How To Search</div>
<p class='show-hint-tips__p'> You can write queries to retrieve entities, including their basic information, activity levels, network performance, threat events, relationships with other entities, and so on. A query has three basic parts: fields, operators, and values.</p>
<code>[Field + operator + value] keyword [operator(Field)]</code>
<ul style="padding: 0">
<li className='show-hint-tips__p'>Field - Fields are different types of traffic attributes in the system.
Fields include ip, domain, app, and so on.
</li>
<li className='show-hint-tips__p'>Operator - Operators are the foundation of the query. They relate the field
to the value and build a query condition. Common operators include =, IN, Like, and so on.
</li>
<li className='show-hint-tips__p'>Value - Values are the actual data in the query.</li>
<p className='show-hint-tips__p'>Use the percent(%) wildcard substitutes for one or more characters in a
string. Such as: </p>
<code>domain like '%google.com'</code>
<p className='show-hint-tips__p'>Strings containing spaces must be enclosed in single quotes ('). Such as:</p>
<code>ip=192.168.10.53</code>
<code>ip.country='United States'</code>
<li className='show-hint-tips__p'>Keyword - Keywords are specific words in the query. You can specify the AND
and OR to create more complex query conditions.
</li>
<p className='show-hint-tips__p'>Currently only support AND.</p>
</ul>
<p class='show-hint-tips__p'>There are two input modes, which can be switched by clicking the button on the right side of the input box.</p>
<div class='default-tips-title'> 1. Text Mode</div>
<p class='show-hint-tips__p'>In text mode, you will write your query with the help of input suggestions.</p>
<div class='default-tips-title'> 2. Tag Mode </div>
<p class='show-hint-tips__p'>In tag mode, you will be guided through preset steps to write a query.</p>
</div>)
}
},
zhDefault: {
description () {
return (<div className='default-tips'>
<div class='default-tips-header'>如何搜索</div>
<p class='show-hint-tips__p'>您可以编写查询来检索实体。查询具有三个基本部分:字段、运算符和值。</p>
<code>[字段 + 运算符 + 值] 关键字 [运算符(字段)]</code>
<ul style="padding: 0">
<li className='show-hint-tips__p'>字段 - 字段是系统中不同类型的属性。字段包括 ip、domain、app 等。</li>
<li className='show-hint-tips__p'>运算符 - 运算符是查询的基础。他们将字段与值相关联并构建查询条件。常见的运算符包括
=、IN、Like 等。
</li>
<li className='show-hint-tips__p'>值 - 值是查询中的实际数据。</li>
<p className='show-hint-tips__p'>使用百分号(%)通配符替换字符串中的一个或多个字符。例如:</p>
<code>domain like '%google.com'</code>
<p className='show-hint-tips__p'>包含空格的字符串必须用单引号(')括住例如</p>
<code>ip=192.168.10.53</code>
<code>ip.country='United States'</code>
<li className='show-hint-tips__p'>关键字 - 关键字是查询中的特定单词您可以指定 AND OR
来创建更复杂的查询条件
</li>
<p className='show-hint-tips__p'>暂时只支持AND</p>
</ul>
<p class='show-hint-tips__p'>有两种输入模式通过点击输入框右侧的按钮进行切换</p>
<div class='default-tips-title'> 1. 文本模式</div>
<p class='show-hint-tips__p'>文本模式中您将在输入建议的帮助下编写查询语句</p>
<div class='default-tips-title'> 2. 标签模式 </div>
<p class='show-hint-tips__p'>标签模式中您将在引导下按预设好的步骤编写查询</p>
</div>)
}
}
}

View File

@@ -0,0 +1,26 @@
import i18n from '@/i18n'
export function fieldRender (h, { fieldInfo = {}, funcReference = [], operatorReference = [], operates = [], funs = [] }) {
return (<div className='field-tips'>
<div class='default-tips-header'>{fieldInfo.label}</div>
<p class='show-hint-tips__p'> {i18n.global.t('overall.name')}: {fieldInfo.name}</p>
<p class='show-hint-tips__p'> {i18n.global.t('overall.type')}: {typeof fieldInfo.type === 'object' ? fieldInfo.type.type : fieldInfo.type} </p>
<p class='show-hint-tips__p'> {i18n.global.t('overall.operators')}: {operates.join(',')} </p>
{/* <p> Function: {funs.join(',')} </p> */}
<p> {fieldInfo.options && (
'Options: ' + fieldInfo.options.map(item => {
return `${item.displayText}(${item.text})`
}).join(',')
)} </p>
<div class='default-tips-header'>{i18n.global.t('overall.enableOperators')}</div>
{/* 提示超过一屏幕 明显不合理 --- 换成简单形式 */}
<ul style="padding-left: 0;margin: 12px 0;">
{operatorReference.map(item => {
return <li>
<span>{item.name}</span>
<code> {item.function} </code>
</li>
})}
</ul>
</div>)
}

View File

@@ -0,0 +1,109 @@
import i18n from '@/i18n'
import { EN, storageKey, ZH } from '@/utils/constants'
export const helpInfo = [
{
operator: 'AND',
zhDescribe: '搜索包含所有搜索字段的实体。',
enDescribe: 'Search entities that contain all the search fields. ',
example: [
"ip = '192.168.10.53' AND domain = 'google.com'",
"domain = 'google.com' AND app = 'google'"
]
}
// {
// // operator: '= , !=',
// operator: ' = ',
// describe: 'Search logs that do EQUALS or NOT EQUALS the search value. ',
// example: [
// "Client IP = '192.168.10.53' "
// // 'Server Port != 443'
// ]
// },
// {
// operator: '> , < , >= , <=',
// describe: 'Search logs greater than and equal or less than and equal a value , or whtin a range. This operator cant be used with string fields. ',
// example: [
// 'Bytes Sent >= 751',
// 'Bytes Sent >= 751 and Bytes Sent <= 1024'
// ]
// },
// {
// operator: ' LIKE ',
// // operator: 'LIKE , NOT LIKE',
// describe: 'You can use wildcard searches for value that contain or not contain search fields. Using percent (%) wildcard substitutes for one or more characters in a string. Using underscore (_) wildcard substitutes for exactly one character in a string. ',
// example: [
// "SSL.SNI LIKE '%google.com' ",
// "Client IP LIKE '192.168.10.1_'"
// // "SSL.SNI NOT LIKE '%google.com'",
// // "Client IP NOT LIKE '192.168%'"
// ]
// },
// {
// operator: ' IN ',
// // operator: 'IN , NOT IN',
// describe: 'Specify or exclude multiple values for search fields. IN IN condition you can use when you need to use multiple OR condition. ',
// example: [
// "l7 Protocol IN ('HTTP', 'HTTPS')"
// // 'Server Port NOT IN (443,80)'
// ]
// },
// {
// operator: 'EMPTY , NOT EMPTY',
// describe: 'Specify or exclude empty value for search fields. A string or array is considered non-empty if it contains at least one byte, even if this is a space or a null byte. ',
// example: [
// 'NOT EMPTY(SSL.SNI)',
// 'EMPTY(Application Label)'
// ]
// },
// {
// operator: 'HAS',
// describe: 'Search logs that has element value for array type. Example:',
// example: [
// 'HAS(FQDN Category, music)'
// ]
// }
// {
// operator: 'bitAnd',
// describe: 'A bitwise And(&) is a binary operation that compares each bit of the first operand to the corresponding bit of the second operand. Both expressions must have integral types. Examples:',
// example: [
// 'bitAnd(Flags, Asymmetric|Download) = Asymmetric|Download',
// 'bitAnd(Flags, Asymmetric|Download) >0'
// ]
// }
]
const renderData = [
helpInfo[0]
]
export const filterList = renderData
function main () {
const sqlTips = {}
const language = localStorage.getItem(storageKey.language) || EN
renderData.forEach((item, index) => {
const data = item // 这是个闭包
sqlTips[item.operator] = {
name: item.operator,
// syntax: item.syntax,
type: 'filter',
description () {
return (<div className='filter-tips'>
<div class='default-tips-header'>{data.operator}</div>
<div class='default-tips-title'> {i18n.global.t('overall.remark')}: </div>
<p class='show-hint-tips__p'> {language === ZH ? data.zhDescribe : data.enDescribe}</p>
<div class='default-tips-title'>{i18n.global.t('overall.examples')}:</div>
<ul style="padding-left: 0;">
{item.example.map(v => {
return <li>
<code>{v}</code>
</li>
})}
</ul>
</div>)
}
}
})
return sqlTips
}
const filterTips = main()
export default filterTips

View File

@@ -0,0 +1,341 @@
var renderData = [
{
name: 'COUNT',
syntax: 'count(expr)',
description: 'Aggregate function is used to count the number of rows',
example: [
{
purpose: 'Total count of all logs :',
code: 'count(*)'
},
{
purpose: 'Counts the occurrences of a Client IP :',
code: 'count(client_ip)'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>
You can use COUNT function by count(*), count(1) or count(field). But there are something difference:
<ul>
<li>count(*) and count(1) will count all the rows in the table, including NULL values.</li>
<li>count(field) will count all the rows in the specified field while excluding NULL values.</li>
</ul>
</div>
}
},
{
name: 'COUNT_DISTINCT',
syntax: 'count(distinct expr)',
description: 'Aggregate function is used to count only distinct(unique) rows in the specified field',
example: [
{
purpose: 'Counts the number of different Client IP :',
code: 'count(distinct client_ip)'
},
{
purpose: `Counts the number of different "Server IP" and "Server port" :`,
code: 'count(distinct server_ip, server_port)'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>The COUNT DISTINCT function returns the number of unique values in the field or multiple fields.
System will uses an adaptive sampling algorithm to perform fast count distinct operations.</div>
}
},
{
name: 'AVG',
syntax: 'avg(expr)',
description: 'Aggregate function is used to calculate the arithmetic mean in the specified field. EXPR must be Integer,Float or Decimal and returned value as Float.',
example: [
{
purpose: `Calculates the average(mean) "Byte sent (sent_bytes)" field:`,
code: 'avg(sent_bytes)'
},
{
purpose: `Calculates the average(mean) "Bytes" , rounded to 2 decimal points:`,
code: 'round(avg(sent_bytes+received_bytes),2)'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>You can use ROUND(expr[,decimal_places]) or FLOOR(expr[,decimal_places]) function that rounds or
floors a value to a specified number of decimal places.</div>
}
},
{
name: 'SUM',
syntax: 'sum(expr)',
description: 'Aggregate function is used to sum of the values of the specified field. EXPR must be Integer,Float or Decimal.',
example: [
{
purpose: `The sum of the "Byte sent (sent_bytes)" field:`,
code: 'sum(sent_bytes)'
},
{
purpose: `The sum of the "sent_bytes" and "received_bytes" fields , and rename as "Bytes ":`,
code: 'sum(sent_bytes+received_bytes) as Bytes'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>You can rename the field using the AS keyword.</div>
}
},
{
name: 'MAX',
syntax: 'max(expr)',
description: 'Aggregate function is used to return the maximum value of the specified field.',
example: [
{
purpose: `Returns the maximum value of the "Byte sent (sent_bytes)" field:`,
code: 'max(sent_bytes)'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>The <b>MAX</b> aggregate function can also be used with the DateTime data type, where it will sort the
DateTime values and return the last value from the sorted logs.</div>
}
},
{
name: 'MIN',
syntax: 'min(expr)',
description: 'Aggregate function is used to return the minimum value of the specified field.',
example: [
{
purpose: `Returns the minimum value of the "Byte sent (sent_bytes)" field:`,
code: 'min(sent_bytes)'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>The MIN aggregate function can also be used with the DateTime data type, where it will sort the
DateTime values and return the minimum value from the sorted logs.</div>
}
},
{
name: 'TIME_FLOOR_WITH_FILL',
syntax: 'TIME_FLOOR_WITH_FILL(<timestamp_expr>, <period>[,<fill>])',
description: 'Rounds down a timestamp, returning it as a new timestamp,optionally from some reference fill, and fills time gaps and impute missing values.',
example: [
{
purpose: `Round the recv_time down to a 5 minutes increment and fill time gaps and impute zero value.`,
code: 'TIME_FLOOR_WITH_FILL(recv_time,\'PT5M\',\'zero\')'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>
<p>The TIME_FLOOR_WITH_FILL function as Timeseries granularity is used for time-based grouping.</p>
<ul>
<li> timestamp_expr - Unix Timestamp field</li>
<li>period - can be any ISO8601 period, like P3M (quarters) or PT12H (half-days)</li>
<li>
<span>fill - optionnal. Includes none, null, zero, previous, next value.</span>
<ul class="sub-url">
<li>none: empty string ""</li>
<li>null"NULL" expression</li>
<li>zerozero "0"</li>
<li>previousprevious value</li>
<li>nextnext value</li>
</ul>
</li>
</ul>
</div>
}
},
{
name: 'UNIX_TIMESTAMP',
syntax: `UNIX_TIMESTAMP(date)`,
description: `Returns a Unix timestamp the value of the argument as seconds since '1970-01-01 00:00:00' UTC.`,
example: [
{
purpose: `Specify a datetime string "2019-06-06 19:11:12", calculate the Unix timestamp:`,
code: 'UNIX_TIMESTAMP(\'2019-06-06 19:11:12\')'
},
{
purpose: `Specify a ISO8601 datetime string with time zone information "2019-10-12T14:20:50+08:00", calculate the Unix timestamp:`,
code: 'UNIX_TIMESTAMP(\'2019-10-12T14:20:50+08:00\')'
},
{
purpose: `Specify a ISO8601 datetime string with UTC+0 time zone information "2019-10-12T14:20:50Z", calculate the Unix timestamp:`,
code: 'UNIX_TIMESTAMP(\'2019-10-12T14:20:50Z\')'
},
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>
<p>The date argument may be a DATE, DATETIME or TIMESTAMP string, or a number in YYMMDD, YYMMDDhhmmss, YYYYMMDD,
or YYYYMMDDhhmmss format.</p>
<ul>
<li> Standard datetime string(UTC+0) : UNIX_TIMESTAMP('2019-06-06 19:11:12')</li>
<li>ISO8601 datetime stringUNIX_TIMESTAMP('2019-10-12T14:20:50Z') or
UNIX_TIMESTAMP('2019-10-12T14:20:50+08:00')
</li>
<li>Date: UNIX_TIMESTAMP(DATE('2019-06-06 19:11:12'))</li>
</ul>
</div>
}
},
{
name: 'FROM_UNIXTIME',
syntax: `FROM_UNIXTIME(unix_timestamp)`,
description: `Returns a representation of unix_timestamp as a datetime or character string value. The value returned is expressed using the UTC+0 time zone.`,
example: [
{
purpose: `Specify a Unix Timestamp "1570881546", calculate the datetime string:`,
code: 'FROM_UNIXTIME(1570881546)'
},
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>The unix_timestamp is an internal timestamp value representing seconds since '1970-01-01 00:00:00'
UTC.</div>
}
},
{
name: 'DATE_FORMAT',
syntax: 'DATE_FORMAT(date, format)',
description: `Formats the date value according to the format string.`,
example: [
{
purpose: `Specify a Unix Timestamp "1570881546", calculate the datetime string with format "%Y-%m-%d %H:%i:%s":`,
code: 'DATE_FORMAT(FROM_UNIXTIME(1570881546), \'%Y-%m-%d %H:%i:%s\')'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>
<p>The DATE_FORMAT function accepts two parameters as given below :</p>
<ul>
<li>date Specified date to be formatted.</li>
<li>
<span>format Specified format. This list of formats used in this function are listed below:</span>
<ul class="sub-url">
<li>%Y - Year, numeric, four digits</li>
<li>%y - Year, numeric (two digits)</li>
<li>%M - Month name (January..December)</li>
<li>%m - Month, numeric (00..12)</li>
<li>%D - Day of the month with English suffix (0th, 1st, 2nd, 3rd, )</li>
<li>%d - Day of the month, numeric (00..31)</li>
<li>%H - Hour (00..23)</li>
<li>%h - Hour (01..12)</li>
<li>%i - Minutes, numeric (00..59)</li>
<li>%s - Seconds (00..59)</li>
<li>%w - Day of the week (0=Sunday..6=Saturday)</li>
</ul>
</li>
</ul>
</div>
}
},
{
name: 'CONVERT_TZ',
syntax: `CONVERT_TZ(dt, from_tz, to_tz)`,
description: `Converts a datetime value dt from the time zone given by from_tz to the time zone given by to_tz and returns the resulting value.`,
example: [
{
purpose: `Specify a datetime string "2021-11-11 00:00:00", converted from GMT(Greenwich Mean Time) to Asia/Shanghai time zone:`,
code: 'CONVERT_TZ(\'2021-11-11 00:00:00\',\'GMT\',\'Asia/Shanghai\')'
},
{
purpose: `Specify a Unix timestamp "1636588800", converted from GMT(Greenwich Mean Time) to Asia/Shanghai time zone:`,
code: 'CONVERT_TZ(FROM_UNIXTIME(1636588800),\'GMT\',\'Asia/Shanghai\')'
},
{
purpose: `Specify a Unix timestamp "1636588800", converted from Europe/London to America/New_York time zone:`,
code: 'CONVERT_TZ(DATE_FORMAT(FROM_UNIXTIME(1636588800), \'%Y-%m-%d %H:%i:%s\'),\'Europe/London\',\'America/New_York\')'
},
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>
<p>The CONVERT_TZ function accepts a three-parameter:</p>
<ul>
<li>dt - The given DateTime which we want to convert.</li>
<li>from_tz - The time zone from which we want to convert DateTime.</li>
<li>to_tz - The time zone in which we want to convert DateTime.</li>
</ul>
</div>
}
},
{
name: 'MEDIAN',
syntax: `MEDIAN(<expr>)`,
description: `Aggregate function is used to calculate median value. expr must be Integer, Float or Decimal.`,
example: [
{
purpose: `Calculates the median "TCP Handshake Latency (tcp_handshake_latency_ms)" field:`,
code: 'MEDIAN(tcp_handshake_latency_ms)'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>In Traffic logs analysis, the function can be useful in calculating the median of certain numbers,
e.g. median SSL Handshake Latency or TCP Handshake Latency.</div>
}
},
{
name: 'QUANTILE',
syntax: `QUANTILE(<expr>[, <level>])`,
description: `Aggregate function is used to calculate an approximate quantile of a numeric data sequence.`,
example: [
{
purpose: `Calculates the 90th percentile "TCP Handshake Latency (tcp_handshake_latency_ms)" field:`,
code: 'QUANTILE(tcp_handshake_latency_ms, 0.9)'
}
],
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>
<p>The QUANTILE function accepts a two-parameter:</p>
<ul>
<li>expr - The column values resulting in integer, Flot or Decimal.</li>
<li>level - Level of quantile. Optional parameter. Constant floating-point number from 0 to 1. We recommend
using a level value in the range of [0.01, 0.99]. Default value is 0.5. At level=0.5 the function calculates
MEDIAN.
</li>
</ul>
</div>
}
},
]
function main () {
var functionTips = {}
renderData.forEach((item, index) => {
var data = item // 这是个闭包
functionTips[item.name] = {
name: item.name,
syntax: item.syntax,
type: 'Function',
description () {
return (<div className="function-tips">
<h2>{data.name}</h2>
<h3>Syntax:<span>{data.syntax}</span></h3>
<h3> Description: </h3>
<p> {data.description}</p>
<h3>Examples:</h3>
<ul>
{item.example.map(v => {
return <li>
<span>{v.purpose}</span>
<code>{v.code}</code>
</li>
})}
</ul>
<h3> Details: </h3>
{Object.prototype.toString.call(data.details) === '[object Function]' ?
<renderer renderFun={data.details}></renderer> : <p>{data.details} </p>}
</div>)
}
}
})
return functionTips
}
export const functionList = renderData
var functionTips = main()
export default functionTips

View File

@@ -0,0 +1,111 @@
import i18n from '@/i18n'
import { EN, storageKey, ZH } from '@/utils/constants'
const renderData = [
{
name: 'EQUALS',
syntax: '=',
primaryKey: '=',
zhDescription: '搜索指定字段的值与指定值完全匹配的实体。',
enDescription: 'Search for entities where the value of a specified field exactly matches a specified value.',
example: [
{
zhPurpose: '通过192.168.10.53查询ip实体:',
enPurpose: 'Search entity of ip by 192.168.10.53:',
code: 'ip=\'192.168.10.53\''
},
{
zhPurpose: '通过google.com查询domain实体',
enPurpose: 'Search entity of domain by google.com',
code: 'domain=\'google.com\''
}
]
},
{
name: 'IN',
syntax: 'in',
primaryKey: 'IN',
zhDescription: '搜索指定字段的值是多个指定值之一的实体。',
enDescription: 'Search for entities where the value of a specified field is one of multiple specified values.',
example: [
{
zhPurpose: '通过192.168.10.53 或 192.168.10.54查询ip实体',
enPurpose: 'Search entity by 192.168.10.53 or 192.168.10.54',
code: 'ip in (\'192.168.10.53\', \'192.168.10.54\')'
},
{
zhPurpose: '通过appName1 或 appName2查询app实体:',
enPurpose: 'Search entity by appName1 or appName2:',
code: 'app in (\'appName1\', \'appName2\')'
}
]
},
{
name: 'LIKE',
syntax: 'like',
primaryKey: 'LIKE',
zhDescription: '搜索使用通配符搜索包含搜索字段的值的实体。此运算符与字符串字段一起使用。',
enDescription: 'Search for entities where use wildcard searches for value that contain search fields. This operator is used with array fields.',
example: [
{
zhPurpose: '通过google.com查询domain实体:',
enPurpose: 'Search entity of domain by google.com:',
code: 'domain like \'%google.com\''
},
{
zhPurpose: '通过appName查询app实体:',
enPurpose: 'Search entity of app by appName:',
code: 'app like \'%appName%\''
}
]
},
{
name: 'HAS',
syntax: 'has',
primaryKey: 'HAS',
zhDescription: '搜索指定字段的值是指定值之一的实体。此运算符与数组字段一起使用。',
enDescription: 'Search for entities where the values of a specified field is one of specified value. This operator is used with array fields.',
example: [
{
zhPurpose: '通过ADNS查询tag实体:',
enPurpose: 'Search entity of tag by ADNS:',
code: 'has(tag,\'ADNS\')'
}
]
}
]
export const operatorList = renderData
function main () {
const operatorTips = {}
const language = localStorage.getItem(storageKey.language) || EN
renderData.forEach((item, index) => {
const data = item // 这是个闭包
operatorTips[item.primaryKey] = {
name: item.name,
syntax: item.syntax,
type: 'Operators',
description () {
return (<div className="operator-tips">
<div class='default-tips-header'>{data.name}</div>
<div class='default-tips-title'>{i18n.global.t('overall.syntax')}: <span>{data.syntax}</span></div>
<div class='default-tips-title'>{i18n.global.t('overall.remark')}: </div>
<p class='show-hint-tips__p'> {language === ZH ? data.zhDescription : data.enDescription}</p>
<div class='default-tips-title'>{i18n.global.t('overall.examples')}: </div>
<ul style="padding-left: 0;">
{item.example.map(v => {
return <li>
<span>{language === ZH ? v.zhPurpose : v.enPurpose}</span>
<code>{v.code}</code>
</li>
})}
</ul>
</div>)
}
}
})
return operatorTips
}
const operatorTips = main()
export default operatorTips

View File

@@ -0,0 +1,97 @@
var renderData = [
{
name: 'FROM',
syntax: `FROM [db.]table |$log_type`,
description: {
title: `The table name of logs. If you type $log_type, the variable value is the current table name of logs.`
}
},
{
name: 'SELECT',
syntax: `Optional. The selected columns(also known as dimensions and metrics).`,
description: {
title: '可选,获取列',
list: [
`aggregate_function(field) - Aggregate functions, default is count.`,
`as field - Use as to specify a aliases for a field or expression.`
]
}
},
{
name: 'GROUP BY',
syntax: `GROUP BY <field-list>`,
description: {
title: 'Aggregate data. GROUP BY clause switches the SELECT query into an aggregation mode',
list: [
`The list of fields known as "grouping key", while each individual expression be referred to as a "key expression".`,
`All the expressions in the SELECT, HAVING and ORDER BY , must be "key expression" or on aggregate functions.`,
`The result of aggregating SELECT query will return unique values of "grouping key" in log type.`
]
}
},
{
name: 'HAVING',
syntax: `HAVING <expression-list>`,
description: {
title: `Optional. HAVING clause filtering the aggregation results retrieved by GROUP BY. It is difference is that WHERE is performed before aggregation, while HAVING is performed after it.
Note: HAVING can't be performed if GROUP BY is not performed.`
}
},
{
name: 'LIMIT',
syntax: `LIMIT [n, ]m`,
description: {
title: `Select the m rows from the aggregate results after skipping the first n rows. Default is 10 rows.`
}
},
{
name: 'ORDER BY',
syntax: `ORDER BY <sort-field> [ASC|DESC]`,
description: {
title: `Sort all of the results by the specified fields.`
}
},
{
name: 'WHERE',
syntax: `where $filter [and <expression-list>]`,
description: {
title: `Filter the data.`,
list: [
`$filter - Default global filter clause. Include Time period, Vsys ID, and other expressions, etc`,
`and <expression-list> - filter clauses`
]
}
}
]
export const sqlList = renderData
function main () {
var sqlTips = {}
renderData.forEach((item, index) => {
var data = item // 这是个闭包
sqlTips[item.name] = {
name: item.name,
syntax: item.syntax,
type: 'sql',
description () {
return (<div className="sql-tips">
<h2>{data.name}</h2>
<h3>Syntax: <span>{data.syntax}</span></h3>
<h3> Description: </h3>
<p> {data.description.title}</p>
{
data.description.list && <ul>
{data.description.list.map(v => {
return <li>{v} </li>
})}
</ul>
}
</div>)
}
}
})
return sqlTips
}
var sqlTips = main()
export default sqlTips

View File

@@ -0,0 +1,48 @@
var renderData = [
{
name: '$LOG_TYPE',
description: 'A variable is a symbolic representation of data that enables you to access a value without having to enter it manually wherever you need it. You can use $ to reference variables throughout Advanced Search. ',
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>
$log_type: The table name of logs.
</div>
}
},
{
name: '$FILTER',
description: 'A variable is a symbolic representation of data that enables you to access a value without having to enter it manually wherever you need it. You can use $ to reference variables throughout Advanced Search. ',
details () {
// 支持jsx 嵌套写法,万一测试要关键字加重呢
return <div>
$filter: The default filter clauses. such as time period, Vsys ID, and other default expressions, etc.
</div>
}
}
]
function main () {
const varTips = {}
renderData.forEach((item, index) => {
const data = item // 这是个闭包
varTips[item.name] = {
name: item.name,
type: 'Variables',
description () {
return (<div className="var-tips">
<h2>{data.name}</h2>
<h3> Description: </h3>
<p> {data.description}</p>
<h3> Details: </h3>
{Object.prototype.toString.call(data.details) === '[object Function]' ?
<renderer renderFun={data.details}></renderer> : <p>{data.details} </p>}
</div>)
}
}
})
return varTips
}
export const varList = renderData
const varTips = main()
export default varTips

View File

@@ -0,0 +1,129 @@
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/eclipse.css'
import 'codemirror/mode/sql/sql'
import 'codemirror/theme/ambiance.css'
// import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/display/placeholder'
import 'codemirror/addon/hint/anyword-hint' // 关键字
import 'codemirror/addon/comment/comment'
import 'codemirror/keymap/sublime'
import 'codemirror/addon/edit/closebrackets'
import CodeMirror from 'codemirror'
import createMode from '@/components/advancedSearch/showhint/TextSearch/sql'
import createHint from '@/components/advancedSearch/showhint/TextSearch/createHint'
import { functionList } from '@/components/advancedSearch/showhint/const/functionTips'
import { cloneDeep } from 'lodash'
const codeMirrorMixins = {
data () {
return {
CodeMirror,
initData: {},
hintSearch: '',
hintList: [],
hintParams: {},
completion: null
}
},
methods: {
initShowHint () {
const dataset = this.dataset
createMode(CodeMirror, {
dataset,
modehook: this.modehook
})
const hintHook = this.manualHinthook
const vm = this
createHint('manual', CodeMirror, {
dataset,
hinthook: hintHook,
callback (completion, defaultOptions) {
vm.completion = completion.completion
},
keyboardUp () {
// 键盘向上
vm.hintVm?.handleUp()
},
keyboardDown () {
// 键盘向下
vm.hintVm?.handleDown()
},
keyboardEnter () {
if (!vm.hintVisible) {
return
}
// 回车 选中值
vm.hintVm?.triggerSelect()
}
})
},
manualHinthook (result, params, manualParams) {
this.hintParams = manualParams
this.hintSearch = manualParams.search
this.hintList = []
const list = this.hinthook(result, params)
this.hintList = cloneDeep(list)
return list
},
hinthook (result, { search, keywords, leftpart, rightpart }) {
if (leftpart) {
const options = this.matchOptions(leftpart)
if (options) {
return options
}
}
result = this.dataset.getHintList(search, null, null, this.value)
return result
},
matchOptions (leftpart) {
// 用于匹配下拉选
const fieldInfo = this.dataset.getFieldInfo(leftpart)
if (fieldInfo && fieldInfo.options) {
const options = []
options.push(
{
type: 'abstract',
text: '',
displayText: 'Options',
className: 'divider hint-title'
}
)
const fieldOptions = cloneDeep(fieldInfo.options)
// eslint-disable-next-line no-return-assign
fieldOptions.forEach(item => item.displayText = `${item.displayText}(${item.text})`)
options.push(...fieldOptions)
return options
}
return null
},
modehook (CodeMirror, set, { client, keywords, builtin, hookVar }) {
// 自定义 过滤器类型
const clientWords = this.dataset.getClientwords()
let clientWordsStr = ''
clientWordsStr = clientWords.join(' ')
const dateFuns = functionList.map(v => v.name.toLowerCase())
CodeMirror.defineMIME('text/x-filter', {
name: 'sql',
client: set(clientWordsStr + ' ' + client), // 客户端解析指令
builtin: set(builtin + ' like in not empty notempty has bitand'),
keywords: set(keywords + ' and or ' + dateFuns.join(' ')),
atoms: set('false true null unknown'),
operatorChars: /^[*+\-%<>!=&|^]/,
dateSQL: set('date time timestamp '),
support: set('ODBCdotTable doubleQuote binaryNumber hexNumber'), // 支持的数据编码 ODBC double 二进制 16进制
hooks: {
$: hookVar
}
})
}
}
}
export default codeMirrorMixins

View File

@@ -0,0 +1,288 @@
// 改js 用户获取初始化的 SchemaData
import { Scheme } from './service/Scheme'
import i18n from '@/i18n'
export class Dataset {
constructor ({ operatesList, filtersList, operatesDic, funcDic, operatorManual, fields, doc }) {
this.sourceData = {}
this.sourceData = this.keepSourcedata(operatesList, funcDic, filtersList, operatesDic, operatorManual, fields, doc)
// 存储格式化的数据
this.hintData = {}
this.hintData.operatesList = this.formatOperates(operatesList)
this.hintData.filtersList = this.formatFilters(filtersList)
}
getHintList (keyword, sqlKeywordsOptions = null, callback, completeFilter) {
// 获取提示列表
const operatesList = this.matchFilter(sqlKeywordsOptions || this.hintData.operatesList || [], keyword)
const filtersList = this.matchFilter(this.hintData.filtersList || [], keyword)
const hintList = [...operatesList, ...filtersList]
callback && callback(operatesList, filtersList, hintList)
return hintList
}
getClientwords () {
// 获取高亮的 可查询的 字段
const filtersList = this.sourceData.filtersList || []
const clientwords = []
filtersList.forEach((item) => {
// 可以在这里增加 过滤逻辑
// todo client由name改为label
// clientwords.push(item.name)
clientwords.push(item.label)
})
return clientwords
}
matchFilter (list, keyword) {
// 用于 匹配过滤器
const results = list.filter((item) => {
if (item.type === 'abstract') {
return true
}
let displayTextLow = item.displayText + '' ? (item.displayText + '').toLowerCase() : item.displayText
let keywordLow = keyword + '' ? (keyword + '').toLowerCase() : keyword
const reg = /[ '"]/ig
displayTextLow = (displayTextLow + '').replace(reg, '').toLowerCase()
keywordLow = (keywordLow + '').replace(reg, '').toLowerCase()
return displayTextLow && displayTextLow.indexOf(keywordLow) !== -1
})
const hasEnable = results.some((item) => {
return item.type !== 'abstract'
})
return hasEnable ? results : []
}
formatFilters (list) {
// 格式化 过滤器
if (!(list && list.length > 0)) {
return
}
const tempTitle = {
type: 'abstract',
text: '',
// displayText: "Filter",
displayText: i18n.global.t('overall.fields'),
className: 'divider hint-title',
hint (cm, callback, options) {
}
}
const results = list.map((item) => {
return {
// text: item.name,
// displayText: `${item.label}(${item.name})`,
text: item.label,
displayText: `${item.label}`,
className: 'filter-item el-dropdown-menu__item relative-item'
}
})
results.unshift(tempTitle)
return results
}
formatOperates (list) {
// 格式化 操作符
if (!(list && list.length > 0)) {
return
}
const tempTitle = {
type: 'abstract',
text: '',
// displayText: "Operator",
displayText: i18n.global.t('overall.keyword'),
className: 'divider hint-title'
// hint(cm, callback, options) {}
}
const results = list.map((item) => {
return {
text: item.name,
displayText: item.name,
className: 'operates-item el-dropdown-menu__item relative-item'
// hint(cm, callback, options) {}
}
})
results.unshift(tempTitle)
return results
}
keepSourcedata (operatesList, funcDic, filtersList, operatesDic, operatorManual, fields, doc) {
// 初始化原始数据 方法(存放的是 全量原始数据)
const sourceData = {}
// 支持的逻辑运算符
sourceData.operatesList = operatesList || []
// 过滤器列表
sourceData.filtersList = filtersList || []
sourceData.operatesDic = operatesDic || []
sourceData.operatorReference = doc?.functions?.operator || []
sourceData.operatorManual = operatorManual || []
// 添加全量数据
sourceData.fields = fields || []
sourceData.doc = doc || {}
// 存储function 的原始变量
sourceData.funcDic = funcDic || []
const aggregation = doc?.functions?.aggregation || []
const dateFuns = doc?.functions?.date || []
sourceData.funcReference = [...dateFuns, ...aggregation]
return sourceData
}
matchHightlight (keyWord) {
// 匹配高亮
// var reg = new RegExp(keyWord, 'i')
let matchFlag = false
// reg.test(val)
matchFlag = this.sourceData.operatesList.some((item) => {
return keyWord === item.name
})
if (matchFlag) {
return 'keyword'
}
if (matchFlag) {
return 'variable-2'
}
matchFlag = this.sourceData.filtersList.some((item) => {
return keyWord === item.name
})
if (matchFlag) {
return 'comment'
}
matchFlag = this.sourceData.specialCharacters.some((item) => {
return keyWord === item.name
})
if (matchFlag) {
return 'atom'
}
}
getOperates (type, item) {
// 获取 当前类型支持的操作符
let operatorFunctions = ''
if (item && item.doc && item.doc.constraints && item.doc.constraints.operator_functions) {
operatorFunctions = item.doc.constraints.operator_functions
} else {
const functions = this.sourceData.operatesDic.find(item => {
return item.type === type
})
// eslint-disable-next-line no-mixed-operators
operatorFunctions = functions && functions.functions || ''
}
const funList = operatorFunctions.split(',')
return funList.map((item) => {
return {
text: item,
displayText: item,
className: 'filter-item el-dropdown-menu__item relative-item'
}
})
}
getFunctions (type, item) {
// 获取 当前类型支持的操作符
let functionsInfo = ''
// 这里肯定有问题
if (item && item.doc && item.doc.constraints && item.doc.constraints.aggregation_functions) {
functionsInfo = item.doc.constraints.aggregation_functions
} else {
const functions = this.sourceData.funcDic.find(item => {
return item.type === type
})
functionsInfo = (functions && functions.functions) || ''
}
const funList = functionsInfo.split(',')
let result = []
result = funList.map((item) => {
return {
text: item,
displayText: item,
className: 'filter-item el-dropdown-menu__item relative-item'
}
})
return result
}
getFieldInfo (keywords) {
// 获取字段的相关信息 1.下拉数据是需要获取的 2.支持的运算符 是不是需要呢 ???
if (!keywords || !keywords.toLowerCase) {
return
}
keywords = (keywords.trim && keywords.trim()) || keywords
const fieldInfo = {}
const matchItem = this.sourceData.filtersList.find((item) => {
// const itemName = item.name && item.name.toLowerCase()
// 左侧面板的options值即枚举的值
const itemName = item.label && item.label.toLowerCase()
return keywords.toLowerCase() === itemName
})
if (!matchItem) {
return null
}
// 存在下拉值 候选项
if (matchItem && matchItem.doc && matchItem.doc.data) {
fieldInfo.options = matchItem.doc.data.map(item => {
return {
text: matchItem.type === 'string' ? `'${item.code}'` : item.code,
displayText: item.value,
className: 'filter-item el-dropdown-menu__item relative-item'
}
})
fieldInfo.operateType = 'select'
}
// 这是一个 远程下拉值
if (matchItem && matchItem.doc && matchItem.doc.dict_location) {
fieldInfo.operateType = 'remoteSelect'
}
fieldInfo._matchItem = matchItem
fieldInfo.name = matchItem.name
fieldInfo.type = matchItem.type
fieldInfo.label = matchItem.label
return fieldInfo
}
getFieldList () {
// 获取字段列表
const fieldList = (this.sourceData && this.sourceData.filtersList) || []
return fieldList
}
getOperatorItem (code) {
const operators = this.sourceData.operatorManual || []
const matchItem = operators.find(item => {
return item.name === code.trim()
})
if (matchItem && !matchItem.label) {
matchItem.label = matchItem.name
}
return matchItem || null
}
dispose () {
this.sourceData = null
this.hintData.operatesList = null
this.hintData.filtersList = null
this.hintData = null
}
}
// 获取数据集
export function getDataset (component, params, list) {
return new Promise((resolve, reject) => {
const schemeInstance = new Scheme(component, params, list)
schemeInstance.getFormatedData((schemeData) => {
const dataset = new Dataset(schemeData)
resolve(dataset, () => {
schemeInstance.dispose()
dataset.dispose()
})
})
})
}

View File

@@ -0,0 +1,94 @@
import {dataTemplate, fieldTemplate} from './template'
export class DeviceTag {
constructor(context, params) {
//先从缓存获取数据
this.queryparams = params
this.context = context
}
filterQueryData(list) {
return list
}
getDataFromRemote() {
//从 远程,也就是请求接口获取数据
return this.context.$get('/deviceTag', this.queryparams).then((res) => {
this.context.initDataReadyCb && this.context.initDataReadyCb(res.data || null); //组件 请求 文件成功回调
if (res.code === 200) {
var data = res.data && res.data.list;
return data
} else {
// this.context.$message({
// message: '请求 DeviceTag数据失败',
// type: 'error',
// showClose: true
// });
}
return null
}).catch((err) => {
console.error(err)
})
}
formatData(data) {
//格式化 获取的数据
const resultData = {
operatesList: [
{
"name": "AND",
"function": "A AND B",
type: "abstract",
label: "AND"
}
],
filtersList: data ? this.filterQueryData(data.fields || []) : [],
//操作符仓库 用于记录 类型 和 操作符之间的映射关系
operatesDic: data ? data.doc.schema_query.references.operator || [] : [],
//operator 记录运算符的label 和 value 映射 以及操作符的使用方法
operatorManual: data ? data.doc.functions.operator || [] : []
}
return resultData
}
organizeData(data=[]) {
var fields = []
var res = JSON.parse(JSON.stringify(dataTemplate));
if (!data ) {
return
}
data.forEach((item, index) => {
var fieldItem = JSON.parse(JSON.stringify(fieldTemplate));
fieldItem.name = item.tagValue
fieldItem.type = "Array"
fieldItem.label = item.tagName
fieldItem.doc.data = (item.subTags || []).map(tagItem => {
return {
code: `'${tagItem.tagValue}'`, //匹配SQL 的时候,这个Code 要加 ''
value: tagItem.tagName,
type: tagItem.tagType
}
})
fields.push(fieldItem)
})
res.fields = fields
return res
}
getFormatedData(callback) {
this.getDataFromRemote().then(data => {
//组织数据 模拟scama
var organizedData = this.organizeData(data);
//格式化数据
this.data = this.formatData(organizedData)
//获取scameData的时候 查询映射字段
callback && callback(this.data)
})
}
dispose(){
this.context=null
this.queryparams=null
this.data=null
}
}

View File

@@ -0,0 +1,150 @@
export class Scheme {
constructor (context, params, list) {
// 先从缓存获取数据
this.queryparams = params
this.context = context
this.columnList = list
this.schemeData = null
this.myCacheData = {
doc: {
functions: {
aggregation: [],
date: [],
operator: [
{
name: '=',
label: '=',
function: 'expr = value'
},
{
name: 'has',
label: 'HAS',
function: 'has(expr, value)'
},
{
name: 'in',
label: 'IN',
function: 'expr in (values)'
},
{
name: 'like',
label: 'LIKE',
function: 'expr like value'
}
]
},
schema_query: {
references: {
aggregation: [
{
type: 'int',
functions: ''
},
{
type: 'string',
functions: ''
},
{
type: 'array',
functions: ''
}
],
operator: [
{
type: 'int',
functions: '=,in,like,has'
},
{
type: 'string',
functions: '=,in,like,has'
},
{
type: 'array',
functions: '=,in,like,has'
}
]
}
}
}
}
}
filterQueryData (list) {
// 仅仅过滤掉 无用的数据 allow_query 为false 的数据 visibility==hidden/disabled DoS日志补充 start_time
const denyList = ['recv_time', 'start_time']
let result
result = list.filter((item, index) => {
// return (item.doc && item.doc.allow_query === 'true') && !denyList.includes(item.name)
// 在前端禁止搜索字段中 或者 接口不允许搜索得字段 过滤掉
return !(item.doc && item.doc.allow_query === 'false') && !denyList.includes(item.name) && !(item.doc && (item.doc.visibility == 'disabled' || item.doc.visibility == 'hidden'))
})
return result || []
}
formatSchemaData (data) {
// 格式化 获取的数据
const formatedData = {
operatesList: [
{
name: 'AND',
function: 'A AND B',
type: 'abstract',
label: 'AND'
}
// {
// name: 'OR',
// function: 'A OR B',
// type: 'abstract',
// label: 'OR'
// }
],
filtersList: data ? this.filterQueryData(data.fields || []) : [],
// 操作符仓库 用于记录 类型 和 操作符之间的映射关系
operatesDic: data ? (data.doc.schema_query && data.doc.schema_query.references.operator) || [] : [],
// 操作符仓库 用于记录 类型 和 聚合函数之间的映射关系
funcDic: data ? (data.doc.schema_query && data.doc.schema_query.references.aggregation) || [] : [],
// operator 记录运算符的label 和 value 映射 以及操作符的使用方法
operatorManual: data ? data.doc.functions.operator || [] : [],
// 全部附属信息 这些是需要的
doc: data.doc,
// 全部字段信息 fields
fields: data.fields
}
// date 和 timestamp 支持的方法 需要特殊处理 ,补充事件函数处理
const dateFuns = (data?.doc?.functions?.date || []).map(item => {
return item.name
})
formatedData.funcDic.forEach(item => {
if (['date', 'timestamp'].includes(item.type)) {
// eslint-disable-next-line no-unused-expressions
dateFuns.length > 0 ? item.functions = item.functions + ',' + dateFuns.join(',') : ''
}
})
return formatedData
}
async getFormatedData (callback) {
const cacheData = this.myCacheData
cacheData.fields = this.columnList
if (this.columnList) {
this.schemeData = this.formatSchemaData(cacheData)
// this.getRemoteOptions()
callback && callback(this.schemeData)
return
}
callback && callback(this.schemeData)
return this.schemeData
}
dispose () {
this.context = null
this.schemeData = null
this.queryparams = null
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,156 @@
export const dataTemplate = {
"doc": {
"functions": {
"aggregation": [
{
"name": "COUNT",
"function": "count(expr)",
"label": "COUNT"
}, {
"name": "COUNT_DISTINCT",
"function": "count(distinct expr)",
"label": "COUNT_DISTINCT"
}, {
"name": "AVG",
"function": "avg(expr)",
"label": "AVG"
}, {
"name": "SUM",
"function": "sum(expr)",
"label": "SUM"
}, {
"name": "MAX",
"function": "max(expr)",
"label": "MAX"
}, {
"name": "MIN",
"function": "min(expr)",
"label": "MIN"
}],
"operator": [
{
"name": "=",
"function": "expr = value",
"label": "="
}, {
"name": "!=",
"function": "expr != value",
"label": "!="
}, {
"name": ">",
"function": "expr > value",
"label": ">"
}, {
"name": "<",
"function": "expr < value",
"label": "<"
}, {
"name": ">=",
"function": "expr >= value",
"label": ">="
}, {
"name": "<=",
"function": "expr <= value",
"label": "<="
}, {
"name": "has",
"function": "has(expr, value)",
"label": "HAS"
}, {
"name": "in",
"function": "expr in (values)",
"label": "IN"
}, {
"name": "not in",
"function": "expr not in (values)",
"label": "NOT IN"
}, {
"name": "like",
"function": "expr like value",
"label": "LIKE"
}, {
"name": "not like",
"function": "expr not like value",
"label": "NOT LIKE"
}, {
"name": "notEmpty",
"function": "notEmpty(expr)",
"label": "NOT EMPTY"
}, {
"name": "empty",
"function": "empty(expr)",
"label": "EMPTY"
}]
},
"schema_query": {
"references": {
"aggregation": [
{
"type": "int",
"functions": "COUNT,COUNT_DISTINCT,AVG,SUM,MAX,MIN"
}, {
"type": "long",
"functions": "COUNT,COUNT_DISTINCT,AVG,SUM,MAX,MIN"
}, {
"type": "float",
"functions": "COUNT,COUNT_DISTINCT,AVG,SUM,MAX,MIN"
}, {
"type": "double",
"functions": "COUNT,COUNT_DISTINCT,AVG,SUM,MAX,MIN"
}, {
"type": "string",
"functions": "COUNT,COUNT_DISTINCT"
}, {
"type": "date",
"functions": "COUNT,COUNT_DISTINCT,MAX,MIN"
}, {
"type": "timestamp",
"functions": "COUNT,COUNT_DISTINCT,MAX,MIN"
}
],
"operator": [
{
"type": "int",
"functions": "=,!=,>,<,>=,<=,in,not in"
}, {
"type": "long",
"functions": "=,!=,>,<,>=,<=,in,not in"
}, {
"type": "float",
"functions": "=,!=,>,<,>=,<="
}, {
"type": "double",
"functions": "=,!=,>,<,>=,<="
}, {
"type": "string",
"functions": "=,!=,in,not in,like,not like,notEmpty,empty"
}, {
"type": "date",
"functions": "=,!=,>,<,>=,<="
}, {
"type": "timestamp",
"functions": "=,!=,>,<,>=,<="
}, {
"type": "array",
"functions": "has"
}]
}
}
},
"fields": []
}
export const fieldTemplate = {
"name": "",
"label": "",
"doc": {
"allow_query": "true",
"visibility": null,
"constraints": {
"type": "tag",
"operator_functions": "in,not in"
},
"data": []
},
"type": "Array"
}

View File

@@ -1,35 +1,39 @@
<template>
<div class="pagination" >
<el-pagination
ref="page"
@size-change="size"
@prev-click="prev"
@next-click="next"
@current-change="current"
:current-page="pageObj.pageNo"
:page-sizes="pageSizes?pageSizes:[20, 50, 100]"
:page-size="Number(pageObj.pageSize)"
:layout="layout"
:total="pageObj.total"
v-bind="bind"
>
<el-select v-model="pageSize" :placeholder="pageSize+$t('pageSize')" size="mini"
:popper-append-to-body="appendToBody" class="pagination-size-select" @change="size"
:popper-class="popClass" @visible-change="popperVisible">
<el-option v-for="(item, index) in pageSizes" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
<el-config-provider :locale="locale">
<el-pagination
ref="page"
@size-change="size"
@prev-click="prev"
@next-click="next"
@current-change="current"
:current-page="pageObj.pageNo"
:page-sizes="pageSizes?pageSizes:[20, 50, 100]"
:page-size="Number(pageObj.pageSize)"
:layout="layout"
:total="pageObj.total"
v-bind="bind"
>
<el-select v-model="pageSize" :placeholder="pageSize+$t('pageSize')" size="mini"
:popper-append-to-body="appendToBody" class="pagination-size-select" @change="size"
:popper-class="popClass" @visible-change="popperVisible">
<el-option v-for="(item, index) in pageSizes" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-pagination>
</el-pagination>
</el-config-provider>
</div>
</template>
<script>
import { defaultPageSize, storageKey } from '@/utils/constants'
import { defaultPageSize, storageKey, ZH, EN } from '@/utils/constants'
import { urlParamsHandler, overwriteUrl } from '@/utils/tools'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { parseInt } from 'lodash'
import ElConfigProvider from 'element-plus'
import cn from 'element-plus/lib/locale/lang/zh-cn'
import en from 'element-plus/lib/locale/lang/en'
export default {
name: 'pagination',
@@ -60,9 +64,15 @@ export default {
const { query } = useRoute()
const pageSize = ref(defaultPageSize)
const currentPageNo = ref(props.storePageNoOnUrl ? (query.pageNo || (props.pageObj.pageNo || 1)) : (props.pageObj.pageNo || 1))
const language = localStorage.getItem(storageKey.language) || EN // 初始未选择默认 en 英文
let locale = en
if (language === ZH) {
locale = cn
}
return {
pageSize,
currentPageNo
currentPageNo,
locale
}
},
data () {

View File

@@ -2,10 +2,10 @@
<div v-ele-click-outside="changeDropdown" style="position: relative;" class="date-range-box">
<div @click="showDropdown" class="date-range-text" :class="myClass" :style="style">
<div class="calendar-popover-text"><i class="cn-icon cn-icon-Data"></i></div>
<div class="calendar-popover-text" style="display: flex" v-if="isCustom">
<div class="calendar-popover-text">{{ dateFormatByAppearance(getMillisecond(startTime)) }}</div>
<div class="calendar-popover-text"> -</div>
<div class="calendar-popover-text">{{ dateFormatByAppearance(getMillisecond(endTime)) }}</div>
<div class="calendar-popover-text" style="display: flex" v-if="isCustom" :title="`${dateFormatByAppearance(getMillisecond(startTime))} -${dateFormatByAppearance(getMillisecond(endTime))}`">
<div class="calendar-popover-text" :style="showPosition === 'left' ? 'width: 90px;text-overflow: ellipsis;overflow: hidden;' : ''" >{{ dateFormatByAppearance(getMillisecond(startTime)) }}</div>
<div class="calendar-popover-text" :style="showPosition === 'left' ? 'display:none;' : ''"> -</div>
<div class="calendar-popover-text" :style="showPosition === 'left' ? 'display:none;' : ''">{{ dateFormatByAppearance(getMillisecond(endTime)) }}</div>
</div>
<div class="calendar-popover-text" v-else>
{{ showDetail }}
@@ -14,32 +14,36 @@
<i class="cn-icon cn-icon-dropdown" :class="dropdownFlag ? 'cn-icon-up' : ''"></i>
</div>
</div>
<transition name="el-zoom-in-top" style="z-index: 4;">
<div v-if="dropdownFlag" class="date-range-panel">
<transition name="el-zoom-in-top" style="z-index: 100004;">
<div v-if="dropdownFlag" class="date-range-panel" :style="showPosition === 'left' ? leftStyle : rightStyle">
<el-row class="date-range-panel-top" style="position: relative">
<el-col :span="16" class="date-range-panel-content date-range-panel-content-left">
<div class="date-range-title" style="padding-left: 0">Absolute time range</div>
<el-date-picker
v-model="newDateValue"
ref="newDatePicker"
popper-class="my-date-picker"
style="position: absolute;top: -53px;left: -536px;"
:clearable="false"
:default-time="defaultTime"
:unlink-panels="true"
type="datetimerange"
@change="timeArrChange"
/>
<div class="content-title">From</div>
<div class="date-range-title" style="padding-left: 0">{{$t('dateTime.absoluteTimeRange')}}</div>
<el-config-provider :locale="locale">
<el-date-picker
v-model="newDateValue"
:key="keyValue"
ref="newDatePicker"
popper-class="my-date-picker"
:style="showPosition === 'left' ? datePickerLeftStyle : datePickerRightStyle"
:clearable="false"
:default-time="defaultTime"
:unlink-panels="true"
type="datetimerange"
@blur="datePickerVisibleChange"
@change="timeArrChange"
/>
</el-config-provider>
<div class="content-title">{{$t('dateTime.from')}}</div>
<div @click="myDatePickerShow" tabindex="1" class="content-input">
{{ dateFormatByAppearance(getMillisecond(myStartTime)) }}
</div>
<div class="content-title">To</div>
<div class="content-title">{{$t('dateTime.to')}}</div>
<div @click="myDatePickerShow" tabindex="2" class="content-input">
{{ dateFormatByAppearance(getMillisecond(myEndTime)) }}
</div>
<div class="date-range-title" style="padding-left: 0">Recently used absolute ranges</div>
<div class="date-range-title" style="padding-left: 0">{{$t('dateTime.recentlyUsedRanges')}}</div>
<div class="date-range-history">
<div v-for="(item, index) in rangeHistoryArr" :key="index" class="date-range-history-item"
@click="historyChange(item)">
@@ -53,7 +57,7 @@
:span="8"
class="date-range-panel-content date-range-panel-content-right"
style="border-left: 1px solid rgba(0,0,0,0.09);">
<div class="date-range-title">Relatime time ranges</div>
<div class="date-range-title">{{$t('dateTime.relativeTimeRanges')}}</div>
<ul class="date-range-item">
<li v-for="item in dateRangeArr"
@click="quickChange(item.value)"
@@ -68,7 +72,7 @@
</ul>
</el-col>
</el-row>
<el-row class="date-range-panel-bottom">
<el-row class="date-range-panel-bottom" >
<el-col :span="12">{{ address }}</el-col>
<el-col :span="12" class="utc-str">{{ utcStr }}</el-col>
</el-row>
@@ -79,9 +83,12 @@
<script>
import { ref, computed, watch, reactive } from 'vue'
import { storageKey } from '@/utils/constants'
import { EN, storageKey, ZH } from '@/utils/constants'
import { getMillisecond, millTimestampDiffFromTz, timestampToList } from '@/utils/date-util'
import { useStore } from 'vuex'
import ElConfigProvider from 'element-plus'
import cn from 'element-plus/lib/locale/lang/zh-cn'
import en from 'element-plus/lib/locale/lang/en'
export default {
name: 'DateTimeRange',
@@ -102,9 +109,49 @@ export default {
},
style: {
type: String
},
showPosition: {
type: String,
default: 'right'
}
},
emits: ['change'],
data () {
return {
dateRangeArr: [
{ value: 5, name: this.$t('dateTime.last5Mins') }, // 'last 5 mins'
{ value: 15, name: this.$t('dateTime.last15Mins') },
{ value: 30, name: this.$t('dateTime.last30Mins') },
{ value: 60, name: this.$t('dateTime.last1Hour') }, // dateTime.last1Hour
{ value: 180, name: this.$t('dateTime.last3Hours') },
{ value: 360, name: this.$t('dateTime.last6Hours') },
{ value: 720, name: this.$t('dateTime.last12Hours') },
{ value: 1440, name: this.$t('dateTime.last1Day') }, // dateTime.last2Days
{ value: 2880, name: this.$t('dateTime.last2Days') }
]
}
},
computed: {
showDetail () {
let str = ''
if (this.dateRangeValue !== -1) {
const rangeItem = this.dateRangeArr.find(item => item.value === this.dateRangeValue)
str = rangeItem ? rangeItem.name : this.dateRangeArr[0].name
}
return str
}
},
methods: {
/**
* 时间选择器失去焦点之后就会隐藏此时Left展示类型的就需要重新设置下拉框的位置
*/
datePickerVisibleChange () {
if (this.showPosition === 'left') {
this.leftStyle = this.leftStyleBefore
// this.dropdownFlag = true
}
}
},
setup (props, ctx) {
// data
const store = useStore()
@@ -124,24 +171,18 @@ export default {
const rangeHistory = ref(localStorage.getItem(storageKey.dataRangeHistory) ? JSON.parse(localStorage.getItem(storageKey.dataRangeHistory)) : [])
const dateRangeValue = props.dateRange ? ref(props.dateRange) : ref(60)
const isCustom = ref(dateRangeValue.value === -1)
const dateRangeArr = [
{ value: 5, name: 'last 5 mins' },
{ value: 15, name: 'last 15 mins' },
{ value: 30, name: 'last 30 mins' },
{ value: 60, name: 'last 1 hour' },
{ value: 180, name: 'last 3 hours' },
{ value: 360, name: 'last 6 hours' },
{ value: 720, name: 'last 12 hours' },
{ value: 1440, name: 'last 1 day' },
{ value: 2880, name: 'last 2 days' }
]
const dropdownFlag = ref(false)
// 默认日历选择时间即开始时间YYYY-MM-DD 00:00:00,结束时间YYYY-MM-DD 59:59:59
const defaultTime = ref([
new Date(2023, 1, 1, 0, 0, 0),
new Date(2023, 1, 2, 23, 59, 59)
])
const rightStyle = 'position: absolute;top: 32px;right: 0px;'
const leftStyleBefore = 'position: absolute;top: 32px;left: 0px;'
const leftStyleAfter = 'position: absolute;top: 32px;left: 660px;'
const datePickerRightStyle = 'position: absolute;top: -53px;left: -536px;'
const datePickerLeftStyle = 'position: absolute;top: -53px;left: -536px;'
const leftStyle = ref('position: absolute;top: 32px;left: 0px;')
// computed
const utcStr = computed(() => {
let str = 'UTC '
@@ -159,13 +200,6 @@ export default {
str += ':00 '
return str
})
const showDetail = computed(() => {
let str = ''
if (dateRangeValue.value !== -1) {
str = dateRangeArr.find(item => item.value === dateRangeValue.value).name
}
return str
})
const rangeHistoryArr = rangeHistory
// refs
@@ -186,6 +220,17 @@ export default {
}
})
/**
* 监测下拉框,一旦隐藏,则设置其位置靠最左边
* */
watch(() => dropdownFlag.value, (newVal) => {
if (!newVal) {
if (props.showPosition === 'left') {
leftStyle.value = leftStyleBefore
}
}
})
// methods
/**
* 打开/关闭时间面板
@@ -204,14 +249,14 @@ export default {
if (dropdownFlag.value) {
dropdownFlag.value = false
}
if (dropdownFlag.value) {
dropdownFlag.value = false
}
}
/**
* 打开时间选择器,从时间面板的“开始时间”、“结束时间”调用
*/
const myDatePickerShow = () => {
if (props.showPosition === 'left') {
leftStyle.value = leftStyleAfter
}
newDateValue.value = [
new Date(...timestampToList(myStartTime.value)),
new Date(...timestampToList(myEndTime.value))
@@ -289,6 +334,14 @@ export default {
})
}
}
const language = localStorage.getItem(storageKey.language) || EN // 初始未选择默认 en 英文
let locale = en
if (language === ZH) {
locale = cn
}
const keyValue = window.$dayJs.tz().valueOf()
return {
myStartTime,
myEndTime,
@@ -297,23 +350,28 @@ export default {
utcStr,
rangeEchartsData,
address,
dateRangeArr,
defaultTime,
dateRangeValue,
isCustom,
newDateValue,
newDatePicker,
showDetail,
rangeHistory,
rangeHistoryArr,
getMillisecond,
datePickerLeftStyle,
datePickerRightStyle,
leftStyle,
leftStyleBefore,
rightStyle,
keyValue,
myDatePickerShow,
showDropdown,
changeDropdown,
timeArrChange,
returnValue,
quickChange,
historyChange
historyChange,
locale
}
}
}

View File

@@ -58,7 +58,7 @@
<script>
import axios from 'axios'
import { storageKey } from '@/utils/constants'
import { storageKey, EN } from '@/utils/constants'
export default {
name: 'TopToolMoreOptions',
@@ -108,7 +108,7 @@ export default {
if (this.paramsType) {
form.append('type', this.paramsType)
}
form.append('language', localStorage.getItem(storageKey.language) ? localStorage.getItem(storageKey.language) : 'en')
form.append('language', localStorage.getItem(storageKey.language) ? localStorage.getItem(storageKey.language) : EN)
axios.post(this.importUrl, form, { 'Content-Type': 'multipart/form-data' }).then(response => {
if (response.status === 200 && response.data.msg === 'success') {
this.$message({ duration: 2000, type: 'success', message: this.$t('tip.importSuccess') })
@@ -128,7 +128,7 @@ export default {
this.importFile = null
},
downloadTemplate () {
const language = localStorage.getItem(storageKey.language) || 'en' // 初始未选择默认 en 英文
const language = localStorage.getItem(storageKey.language) || EN // 初始未选择默认 en 英文
const fileName = this.exportFileName + '-' + this.$t('overall.template') + '-' + this.getTimeString() + '.json'
let url = null
@@ -159,7 +159,7 @@ export default {
})
}
params.pageSize = -1
params.language = localStorage.getItem(storageKey.language) || 'en'
params.language = localStorage.getItem(storageKey.language) || EN
this.export(this.exportUrl, params, this.exportFileName + '-' + this.getTimeString() + '.json')
this.closeDialog()

View File

@@ -13,12 +13,12 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<div id="header-to-english" :style="language === 'en'?'color:#0091ff':''" @click="changeLocal('en')">
<div id="header-to-english" :style="language === EN?'color:#0091ff':''" @click="changeLocal(EN)">
English
</div>
</el-dropdown-item>
<el-dropdown-item>
<div id="header-to-chinese" :style="language === 'cn'?'color:#0091ff':''" @click="changeLocal('cn')">
<div id="header-to-chinese" :style="language === ZH?'color:#0091ff':''" @click="changeLocal(ZH)">
中文
</div>
</el-dropdown-item>
@@ -42,11 +42,11 @@
</div>
<div class="cn-header__nav">
<i class="cn-icon cn-icon-a-NetworkAnalytics"></i>
<el-breadcrumb class="header__left-breadcrumb" separator=">">
<el-breadcrumb-item class="header__left-breadcrumb-item" :id="`breadcrumb${item.value}`" :title="item.value"
<el-breadcrumb class="header__left-breadcrumb" separator=">" v-if="route.startsWith('/panel')">
<el-breadcrumb-item class="header__left-breadcrumb-item" :id="`breadcrumb${item.value}`" :title="index===3?item.value:''"
v-for="(item,index) in breadcrumb" :key="item.value">
<template v-if="index===3">
<div class="header__left-breadcrumb-item-select">
<template v-if="index===3" >
<div class="header__left-breadcrumb-item-select" >
<el-popover placement="bottom-start"
ref="breadcrumbPopover"
:show-arrow="false"
@@ -113,6 +113,13 @@
</template>
</el-breadcrumb-item>
</el-breadcrumb>
<el-breadcrumb class="header__left-breadcrumb" separator=">" v-else>
<el-breadcrumb-item class="header__left-breadcrumb-item" :id="`breadcrumb${item.value}`"
v-for="(item,index) in breadcrumb" :key="item.value">
<span v-if="item.clickable" class="route-menu" @click="jumpOther(item.route,index)">{{ item.value }}</span>
<span v-else>{{ item.value }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 菜单 -->
@@ -125,7 +132,7 @@
:show-close="false"
>
<div class="cn-menu__left" v-if="otherMenu">
<div class="left-menu" v-for="menu in otherMenu" :key="menu.id" @click="jump(menu.route,'','',0)">
<div class="left-menu" v-for="menu in otherMenu" :key="menu.id" @click="jumpOther(menu.route,'','',0)">
<i :class="menu.icon"></i>
<span>{{ $t(menu.i18n || menu.name) }}</span>
<i class="cn-icon cn-icon-right"></i>
@@ -196,7 +203,9 @@ import {
networkTable,
operationType,
storageKey,
wholeScreenRouterMapping
wholeScreenRouterMapping,
ZH,
EN
} from '@/utils/constants'
import { api } from '@/utils/api'
import { ref } from 'vue'
@@ -234,7 +243,7 @@ export default {
return {
username: localStorage.getItem(storageKey.username),
nickName: localStorage.getItem(storageKey.nickName),
language: localStorage.getItem(storageKey.language) ? localStorage.getItem(storageKey.language) : 'en',
language: localStorage.getItem(storageKey.language) ? localStorage.getItem(storageKey.language) : EN,
showChangePin: false,
from: '', // entity类型
changePassForm: {
@@ -296,7 +305,9 @@ export default {
curTabState: curTabState,
urlChangeParams: {},
wholeScreenRouterMapping,
logo: 'images/logo-header.svg'
logo: 'images/logo-header.svg',
ZH,
EN
}
},
computed: {
@@ -304,7 +315,7 @@ export default {
return this.$store.getters.menuList.find(menu => menu.code === 'networkAnalytics')
},
otherMenu () {
return this.$store.getters.menuList.filter(menu => ['networkAnalytics', 'chart', 'I18N', 'entityDetail', 'temp', 'entityGraph', 'detectionPolicy'].indexOf(menu.code) === -1)
return this.$store.getters.menuList.filter(menu => ['networkAnalytics', 'I18N', 'entityDetail', 'entityGraph', 'detectionPolicy'].indexOf(menu.code) === -1)
/* function excludeButton (menu) {
for (let i = 0; i < menu.length; i++) {
@@ -322,15 +333,17 @@ export default {
breadcrumb () {
const breadcrumb = []
this.generateBreadcrumb(breadcrumb, this.$store.getters.menuList)
// 写死一级和二级菜单是否可以点击跳转
if (breadcrumb[0]) {
if (['knowledgeBase'].indexOf(breadcrumb[0].code) > -1) {
breadcrumb[0].clickable = true
}
if (breadcrumb[1]) {
if (breadcrumb[1].route && breadcrumb[1].route.indexOf('/panel/') === 0) {
breadcrumb[1].clickable = true
}
if (breadcrumb) {
// panel菜单是否可以点击跳转一级菜单不可点击二级菜单可以点击
if (breadcrumb[0] && breadcrumb[1] && breadcrumb[1].route &&
breadcrumb[1].route.indexOf('/panel/') === 0) {
breadcrumb[1].clickable = true
} else { // 除panel外的菜单是否可以点击跳转:除了新增、编辑,其它均可点击
breadcrumb.forEach(item => {
if (item.value !== 'Create' && item.value !== 'Edit') {
item.clickable = true
}
})
}
}
@@ -366,7 +379,7 @@ export default {
},
async breadcrumb (n) {
this.curTabProp = this.$route.query.dimensionType ? this.$route.query.dimensionType : null
if (this.$route.params.typeName === fromRoute.dnsServiceInsights) {
if (this.$route.path.replace('/panel/', '') === fromRoute.dnsServiceInsights) {
if (this.dnsQtypeMapData.size === 0) {
this.dnsQtypeMapData = await getDnsMapData('dnsQtype')
}
@@ -384,7 +397,7 @@ export default {
async mounted () {
this.from = Object.keys(this.entityType)[0]
// 是否需要dns的qtype和rcode的数据字典
if (this.$route.params.typeName === fromRoute.dnsServiceInsights) {
if (this.$route.path.replace('/panel/', '') === fromRoute.dnsServiceInsights) {
if (this.dnsQtypeMapData.size === 0) {
this.dnsQtypeMapData = await getDnsMapData('dnsQtype')
}
@@ -405,10 +418,10 @@ export default {
const endTimeParam = query.endTime
// 若url携带了使用携带的值否则使用默认值。
const dateRangeValue = rangeParam ? parseInt(query.range) : 60
const dateRangeValue = rangeParam ? parseInt(rangeParam) : 60
const chartTimeFilter = ref({ dateRangeValue })
if (!startTimeParam || !endTimeParam) {
const { startTime, endTime } = getNowTime(60)
const { startTime, endTime } = getNowTime(dateRangeValue)
chartTimeFilter.value.startTime = startTime
chartTimeFilter.value.endTime = endTime
} else {
@@ -423,39 +436,6 @@ export default {
},
methods: {
generateBreadcrumb (breadcrumb, menus) {
if (this.route === '/entityDetail') {
const entityMenu = menus.find(m => m.route === '/entityExplorer')
const entityDetailMenu = menus.find(m => m.route === '/entityDetail')
breadcrumb.push({
code: entityMenu.code,
value: entityMenu.i18n ? this.$t(entityMenu.i18n) : entityMenu.name,
route: entityMenu.route,
type: entityMenu.type
})
breadcrumb.push({
code: entityDetailMenu.code,
value: entityDetailMenu.i18n ? this.$t(entityDetailMenu.i18n) : entityDetailMenu.name,
route: entityDetailMenu.route,
type: entityDetailMenu.type
})
return true
} else if (this.route === '/entityGraph') {
const entityMenu = menus.find(m => m.route === '/entityExplorer')
const entityGraphMenu = menus.find(m => m.route === '/entityGraph')
breadcrumb.push({
code: entityMenu.code,
value: entityMenu.i18n ? this.$t(entityMenu.i18n) : entityMenu.name,
route: entityMenu.route,
type: entityMenu.type
})
breadcrumb.push({
code: entityGraphMenu.code,
value: entityGraphMenu.i18n ? this.$t(entityGraphMenu.i18n) : entityGraphMenu.name,
route: entityGraphMenu.route,
type: entityGraphMenu.type
})
return true
}
const menu = menus.find(m => m.route === this.route)
if (menu) {
breadcrumb.unshift({
@@ -507,7 +487,7 @@ export default {
},
getCurTabByLabel (label) {
let curTab = null
const tableType = this.$route.params ? this.$route.params.typeName : 'networkOverview'
const tableType = this.$route.path.replace('/panel/', '') || 'networkOverview'
const curTableInCode = networkTable[tableType] ? networkTable[tableType] : networkTable.networkOverview
if (curTableInCode && curTableInCode.tabList) {
curTab = curTableInCode.tabList.find(item => item.label === label)
@@ -520,7 +500,7 @@ export default {
const currentValue = document.getElementById('breadcrumbValue') ? document.getElementById('breadcrumbValue').innerText : ''
const columnName = this.getUrlParam(this.curTabState.thirdMenu, '')
let type = 'ip'
const tableType = this.$route.params ? this.$route.params.typeName : 'networkOverview'
const tableType = this.$route.path.replace('/panel/', '') || 'networkOverview'
const curTableInCode = networkTable[tableType] ? networkTable[tableType] : networkTable.networkOverview
if (curTableInCode && curTableInCode.tabList) {
const curTab = curTableInCode.tabList.find(item => item.label === columnName)
@@ -544,7 +524,7 @@ export default {
axios.get(curTableInCode.url.drilldownList, { params }).then(async response => {
if (response.status === 200) {
this.breadcrumbColumnValueListShow = response.data.data.result
if (this.$route.params.typeName === fromRoute.dnsServiceInsights) {
if (this.$route.path.replace('/panel/', '') === fromRoute.dnsServiceInsights) {
if (this.dnsQtypeMapData.size === 0) {
this.dnsQtypeMapData = await getDnsMapData('dnsQtype')
}
@@ -611,21 +591,28 @@ export default {
if (curTab.prop === 'protocolPort') {
const valueGroup = value.split(':')
if (valueGroup) {
queryCondition.push('common_l7_protocol=\'' + valueGroup[0] + '\'')
queryCondition.push('common_server_port=' + valueGroup[1])
queryCondition.push('l7_protocol=\'' + valueGroup[0] + '\'')
queryCondition.push('server_port=' + valueGroup[1])
}
// console.log(queryCondition.join(' AND '))
this.urlChangeParams[this.curTabState.queryCondition] = queryCondition.join(' AND ')
this.urlChangeParams[this.curTabState.lineQueryCondition] = queryCondition.join(' AND ')
} else {
searchProps.forEach(item => {
queryCondition.push(item + '=\'' + handleSpecialValue(value) + '\'')
})
if (curTab.queryCondition) {
curTab.queryCondition.forEach(item => {
queryCondition.push(item.replaceAll('$param', value))
})
} else if (searchProps) {
searchProps.forEach(item => {
queryCondition.push(item + '=\'' + handleSpecialValue(value) + '\'')
})
}
this.urlChangeParams[this.curTabState.queryCondition] = queryCondition.join(' OR ')
const lineQueryCondition = []
if (curTab.lineQueryCondition) {
curTab.lineQueryCondition.forEach(item => {
lineQueryCondition.push(item.replaceAll('$param', value))
lineQueryCondition.push(item.replaceAll('$param', handleSpecialValue(value)))
})
this.urlChangeParams[this.curTabState.lineQueryCondition] = lineQueryCondition.join(' OR ')
}
@@ -675,7 +662,7 @@ export default {
},
async handleCurDrilldownTableConfig (thirdMenu) {
// const userId = localStorage.getItem(storageKey.userId)
const tableType = this.$route.params ? this.$route.params.typeName : 'networkOverview'
const tableType = this.$route.path.replace('/panel/', '') || 'networkOverview'
const metric = this.getUrlParam(this.curTabState.tableMetric, 'Bits/s')
const drillDownTableConfigs = await combineDrilldownTableWithUserConfig()
const currentTableConfig = drillDownTableConfigs.find(config => config.route === tableType)
@@ -696,7 +683,26 @@ export default {
}
}
},
// 仅处理除panel外的相关路径的导航
async jumpOther (route, index) {
route = route.replace('redirect:', '')
this.showMenu = false
if (route === this.route && index > 0) { // 当前只有一级菜单时,点击不进行刷新,重新跳转
this.refresh()
return
}
if (route) {
this.$router.push({
path: route,
query: {
t: +new Date()
}
})
}
},
// 仅处理panel相关路径的导航
async jump (route, columnName, columnValue, opeType) {
route = route.replace('redirect:', '')
if (route === '/panel/linkMonitor' && opeType === 3) {
return true
}
@@ -716,7 +722,7 @@ export default {
this.$store.commit('setNetworkOverviewTabList', [])
}
// 清空网络概况的特殊面包屑
const tableType = this.$route.params ? this.$route.params.typeName : 'networkOverview'
const tableType = this.$route.path.replace('/panel/', '') || 'networkOverview'
const metric = this.getUrlParam(this.curTabState.tableMetric, 'Bits/s')
const curTab = await getDefaultCurTab(tableType, metric, columnName)
this.$store.getters.menuList.forEach(menu => {
@@ -728,11 +734,6 @@ export default {
child.columnName = columnName
this.urlChangeParams[this.curTabState.thirdMenu] = columnName
this.urlChangeParams[this.curTabState.fourthMenu] = columnValue
// const tabObjGroup = networkOverviewTabList.filter(item => item.label == columnName)
// let curTab = this.getCurTabByLabel()
// const type = curTab ? curTab.prop : ''
// this.curTabProp = this.$route.query.dimensionType ? this.$route.query.dimensionType : null
// this.urlChangeParams[this.curTabState.dimensionType] = type
this.urlChangeParams[this.curTabState.panelName] = columnValue
} else if (columnName) { // 点击的为列名
child.columnValue = ''
@@ -791,10 +792,10 @@ export default {
})
return
}
/* if (route === this.route) {
if (route === this.route) {
this.refresh()
return
} */
}
if (route) {
this.$router.push({
path: route,

View File

@@ -25,7 +25,7 @@
@change="timeConfigTypeChange"
>
<template v-for="time in timeRuleList" :key="time.value">
<el-option :label="time.name" :value="time.value"></el-option>
<el-option :label="$t(time.name)" :value="time.value"></el-option>
</template>
</el-select>
<template v-if="editObject.config.timeConfig.type === 'this'">
@@ -38,7 +38,7 @@
size="small"
@change="()=>{ this.$forceUpdate() }">
<template v-for="time in timeUnitList" :key="time.value">
<el-option :label="time.name" :value="time.value"></el-option>
<el-option :label="$t(time.name)" :value="time.value"></el-option>
</template>
</el-select>
</template>
@@ -56,7 +56,7 @@
size="small"
@change="()=>{ this.$forceUpdate() }">
<template v-for="time in timeUnitList" :key="time.value">
<el-option :label="time.name" :value="time.value"></el-option>
<el-option :label="$t(time.name)" :value="time.value"></el-option>
</template>
</el-select>
</div>
@@ -154,7 +154,6 @@
<el-checkbox v-for="item in dateList" :key="item" :label="item"/>
</el-checkbox-group>
</div>
<div class="enable-month-value">*一月三月五月七月八月十月和12月含31天;闰年二月含29天*</div>
</template>
<!-- 按周 -->
<template v-else-if="monthScheduleType === 'weekly'">
@@ -191,6 +190,7 @@
:format="dateFormat"
prefix-icon="cn-icon cn-icon-shijian"
type="datetime"
@change="onChangeSchedulerStart"
placeholder=" "
/>
</div>
@@ -207,6 +207,7 @@
:format="dateFormat"
prefix-icon="cn-icon cn-icon-shijian"
type="datetime"
@change="onChangeSchedulerEnd"
placeholder=" "
/>
</div>
@@ -243,7 +244,7 @@
placeholder=" "
filterable
:disabled="!!editObject.id"
popper-class="right-box-select-dropdown right-box-select-report "
popper-class="search-select right-box-select-dropdown right-box-select-report "
size="small"
>
<template #prefix>
@@ -277,7 +278,7 @@ import { storageKey, report } from '@/utils/constants'
import { api } from '@/utils/api'
import _ from 'lodash'
import axios from 'axios'
import { dateFormat, getMillisecond } from '@/utils/date-util'
import { dateFormat, getMillisecond, millTimestampDiffFromTz } from '@/utils/date-util'
import { ref, getCurrentInstance } from 'vue'
import i18n from '@/i18n'
export default {
@@ -293,10 +294,12 @@ export default {
const startTime = ref('')
const endTime = ref('')
function endTimeChange (val) {
endTime.value = val
// endTime.value = val + millTimestampDiffFromTz()
endTime.value = getMillisecond(val) + millTimestampDiffFromTz()
}
function startTimeChang (val) {
startTime.value = val
// startTime.value = val + millTimestampDiffFromTz()
startTime.value = getMillisecond(val) + millTimestampDiffFromTz()
}
const endDisabledDate = (time) => {
if (time.getTime() > new Date()) {
@@ -333,7 +336,7 @@ export default {
}
const startTimeValidator = (rule, value, callback) => {
const form = proxy.$refs.reportForm
if (form.model.config.endTime) {
if (form.model && form.model.config && form.model.config.endTime) {
form.validateField('config.endTime', () => null)
}
callback()
@@ -353,9 +356,6 @@ export default {
categoryId: [
{ required: true, message: i18n.global.t('validate.required'), trigger: 'change' }
],
schedulerStart: [
{ required: true, message: i18n.global.t('validate.required'), trigger: 'change' }
],
'config.startTime': [
{ required: true, message: i18n.global.t('validate.required'), trigger: 'change' },
{ validator: startTimeValidator, trigger: 'change' }
@@ -369,15 +369,53 @@ export default {
{ validator: paramValidator, message: i18n.global.t('validate.required'), trigger: 'blur' }
]
}
const mySchedulerStart = ref('')
const mySchedulerEnd = ref('')
const onChangeSchedulerStart = (val) => {
mySchedulerStart.value = getMillisecond(val) + millTimestampDiffFromTz()
}
const onChangeSchedulerEnd = (val) => {
mySchedulerEnd.value = getMillisecond(val) + millTimestampDiffFromTz()
}
return {
endDisabledDate,
startDisabledDate,
startTimeChang,
endTimeChange,
rules
rules,
startTime,
endTime,
mySchedulerStart,
mySchedulerEnd,
onChangeSchedulerStart,
onChangeSchedulerEnd
}
},
data () {
const schedulerStartTimeValidator = (rule, value, callback) => {
if (this.editObject.schedulerEnd) {
if (this.$refs.reportForm) {
this.$refs.reportForm.validateField('schedulerEnd')
}
}
callback()
}
const schedulerEndTimeValidator = (rule, value, callback) => {
if (value && this.editObject.schedulerStart && (getMillisecond(this.editObject.schedulerStart) >= getMillisecond(value))) {
callback(new Error(this.$t('config.user.timeVerification')))
} else {
callback()
}
}
this.rules.schedulerStart = [
{ required: true, message: i18n.global.t('validate.required'), trigger: 'change' },
{ validator: schedulerStartTimeValidator, trigger: 'change' }
]
this.rules.schedulerEnd = [
{ validator: schedulerEndTimeValidator, trigger: 'change' }
]
return {
url: api.reportTemp,
@@ -404,7 +442,9 @@ export default {
monthWeekdayCheckedAll: false,
monthWeekdayIsIndeterminate: false,
paramsOptions: []
paramsOptions: [],
newParamsOptions: [],
rangeNumber: 10
}
},
watch: {
@@ -415,6 +455,7 @@ export default {
scheduleChecked (n) {
this.editObject.config.isScheduler = n ? 1 : 0
this.cleanScheduleConfig()
this.initDateCalendarPreIcon()
},
monthScheduleType (n) {
this.cleanScheduleConfig()
@@ -496,6 +537,14 @@ export default {
}
}
if (n.config) {
if (n.config.startTime) {
this.editObject.config.startTime = getMillisecond(n.config.startTime) - millTimestampDiffFromTz()
}
if (n.config.endTime) {
this.editObject.config.endTime = getMillisecond(n.config.endTime) - millTimestampDiffFromTz()
}
}
if (n.schedulerStart) {
this.editObject.schedulerStart = dateFormat(this.editObject.schedulerStart, this.dateFormat)
}
@@ -522,10 +571,24 @@ export default {
}
}
},
mounted () {
this.initDateCalendarPreIcon()
},
methods: {
initDateCalendarPreIcon () {
this.$nextTick(() => {
const datePrefixIcon = document.getElementsByClassName('el-input__prefix-inner')
if (datePrefixIcon && datePrefixIcon.length > 0) {
Array.prototype.forEach.call(datePrefixIcon, function (element) {
element.innerHTML = '<i class="el-input__icon cn-icon cn-icon-shijian"></i>'
})
}
})
},
loadParamOptions () {
if (_.isArray(this.editObject.categoryParams) && !_.isEmpty(this.editObject.categoryParams)) {
this.editObject.categoryParams.forEach(param => {
// this.paramsOptions = _.cloneDeep(this.newParamsOptions)
if (!this.paramsOptions.some(p => p.key === param.key)) {
axios.get(api.dict, { params: { type: param.key, pageSize: -1 } }).then(response => {
if (response.status === 200) {
@@ -533,6 +596,7 @@ export default {
key: param.key,
options: response.data.data.list.map(d => d.value)
})
// this.newParamsOptions = this.paramsOptions
}
})
}
@@ -570,6 +634,7 @@ export default {
if (val === 'customize') {
this.scheduleChecked = false
}
this.initDateCalendarPreIcon()
},
scheduleTypeChange (val) {
this.scheduleType = val
@@ -585,10 +650,12 @@ export default {
let schedulerStart = ''
let schedulerEnd = ''
if (this.editObject.config && this.editObject.config.startTime) {
startTime = getMillisecond(this.editObject.config.startTime)
// startTime = getMillisecond(this.editObject.config.startTime)
startTime = this.startTime ? this.startTime : this.object.config.startTime
}
if (this.editObject.config && this.editObject.config.endTime) {
endTime = getMillisecond(this.editObject.config.endTime)
// endTime = getMillisecond(this.editObject.config.endTime)
endTime = this.endTime ? this.endTime : this.object.config.endTime
}
if (this.editObject.config && this.editObject.config.schedulerConfig) {
if (['day', 'week', 'month'].indexOf(this.editObject.config.schedulerConfig.type) > -1) {
@@ -598,10 +665,12 @@ export default {
}
}
if (this.editObject.schedulerStart) {
schedulerStart = getMillisecond(this.editObject.schedulerStart)
// schedulerStart = getMillisecond(this.editObject.schedulerStart)
schedulerStart = this.mySchedulerStart ? this.mySchedulerStart : this.object.schedulerStart
}
if (this.editObject.schedulerEnd) {
schedulerEnd = getMillisecond(this.editObject.schedulerEnd)
// schedulerEnd = getMillisecond(this.editObject.schedulerEnd)
schedulerEnd = this.mySchedulerEnd ? this.mySchedulerEnd : this.object.schedulerEnd
}
const copyObject = _.cloneDeep(this.editObject)
@@ -722,6 +791,36 @@ export default {
const checkedCount = val.length
this.monthWeekdayCheckedAll = checkedCount === this.weekdayList.length
this.monthWeekdayIsIndeterminate = checkedCount > 0 && checkedCount < this.weekdayList.length
},
/**
* params的option筛选方法组件自带的方法在paramsOptions的options长度超过1512后会保留部分值
* @param filterVal
*/
filterMethod (filterVal) {
const key = this.editObject.categoryParams[0].key
if (filterVal) {
const obj = this.newParamsOptions.find(d => d.key === key)
if (_.isString(filterVal) && obj) {
const filterArr = _.cloneDeep(obj.options).filter(d => d.toLowerCase().includes(filterVal.toLowerCase()))
const paramsOptionsObj = this.paramsOptions.find(d => d.key === key)
paramsOptionsObj.options = filterArr
} else if (_.isNumber(filterVal) && obj) {
const filterArr = _.cloneDeep(obj.options).filter(d => d.toLowerCase().includes(filterVal))
const paramsOptionsObj = this.paramsOptions.find(d => d.key === key)
paramsOptionsObj.options = filterArr
}
} else {
this.paramsOptions = _.cloneDeep(this.newParamsOptions)
this.rangeNumber = 10
}
},
loadMore () {
this.rangeNumber += 10
},
onVisibleChange (flag) {
if (!flag) {
this.rangeNumber = 0
}
}
}
}

View File

@@ -23,7 +23,8 @@
<button type="button" class="cn-btn cn-btn-size-small-new cn-btn-style-light-new option-btn" style="margin-left: 0px;" @click="expandAllOrNone" :class="{'btn-active':expandAllFlag}">展开/收缩</button>
<button type="button" class="cn-btn cn-btn-size-small-new cn-btn-style-light-new option-btn" @click="selectAllOrNone" :class="{'btn-active':selectAllFlag}"><span ><i class="cn-icon cn-icon-delete"></i></span></button>
</div>-->
<el-tree :data="menus" :default-expand-all="expandAllFlag" :props="{label:labelFormatter}" @check-change="selectChange" class="tree-border" node-key="id" ref="menuTree" show-checkbox id="role-box-input-menus">
<el-checkbox v-model="isCheckAll" :label="$t('overall.all')" @change="checkAll"></el-checkbox>
<el-tree :data="menus" :default-expand-all="expandAllFlag" check-strictly="true" :props="{label:labelFormatter}" @check-change="selectChange" class="tree-border" node-key="id" ref="menuTree" show-checkbox id="role-box-input-menus">
<template #default="{ data }">
<span>
<i v-if="data.type === '1'" class="el-icon-menu"></i>
@@ -90,7 +91,8 @@ export default {
menus: [],
selectedIds: [],
selectAllFlag: false,
expandAllFlag: true
expandAllFlag: true,
isCheckAll: false
}
},
watch: {
@@ -150,9 +152,59 @@ export default {
labelFormatter: function (data, node) {
return data && data.i18n ? this.$t(data.i18n) : data.name
},
selectChange: function (data, isCheck, childIsCheck) {
getChildNodes (menu) {
let nodeGroup = []
if (menu.children && menu.children.length > 0) {
nodeGroup = menu.children
const _this = this
menu.children.forEach(node => {
const childNodes = _this.getChildNodes(node)
if (childNodes && childNodes.length > 0) {
nodeGroup = nodeGroup.concat(childNodes)
}
})
}
return nodeGroup
},
checkAll () {
if (this.$refs.menuTree) {
this.editRole.menuIds = this.$refs.menuTree.getCheckedKeys(true)
if (this.isCheckAll) {
let nodeGroup = this.menus
const _this = this
this.menus.forEach(menu => {
const childNodes = _this.getChildNodes(menu)
if (childNodes && childNodes.length > 0) {
nodeGroup = nodeGroup.concat(childNodes)
}
})
this.$refs.menuTree.setCheckedNodes(nodeGroup)
} else {
this.$refs.menuTree.setCheckedNodes([])
}
}
},
checkParentNode (node) {
if (node && this.$refs.menuTree.getNode(node)) {
const parent = this.$refs.menuTree.getNode(node).parent
const parentNode = parent.data
if (parentNode && parentNode.id && parentNode.id !== 0) {
this.$refs.menuTree.setChecked(parentNode, true, false)
this.checkParentNode(parentNode)
}
}
},
selectChange: function (data, isCheck, childIsCheck) {
if (isCheck) { // 如果是选中节点,则同步选中所有的父辈节点(有全选和半选两种状态)
this.checkParentNode(data)
} else { // 如果是取消节点,则同步取消选中所有子节点
if (data.children && data.children.length > 0) {
data.children.forEach(node => {
this.$refs.menuTree.setChecked(node, false, true)
})
}
}
if (this.$refs.menuTree) {
this.editRole.menuIds = this.$refs.menuTree.getCheckedKeys(false)
}
},
selectAllOrNone: function () {

View File

@@ -125,16 +125,16 @@ export default {
mixins: [rightBoxMixin],
data () {
const validatePin = (rule, value, callback) => { // 确认密码
if (value.length < 5) {
if (value && value.length < 5) {
callback(new Error(this.$t('validate.atLeastFive')))
} else {
callback()
}
}
const validateConfirmPin = (rule, value, callback) => { // 确认密码的二次校验
if (value === '' && this.editObject.pin) {
if (_.isEmpty(value) && !_.isEmpty(this.editObject.pin)) { // 密码有内容,确认密码没内容
callback(new Error(this.$t('config.user.confirmPin')))
} else if (value !== this.editObject.pin) {
} else if (!_.isEmpty(value) && value !== this.editObject.pin) { // 密码有内容,确认密码也有内容,内容不一致
callback(new Error(this.$t('config.user.confirmPinErr')))
} else {
callback()
@@ -207,7 +207,7 @@ export default {
],
pinChange: [
{ validator: validateConfirmPin, trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9]{5,64}$/, message: this.$t('validate.atLeastFive') }
{ validator: validatePin, trigger: 'blur' }
],
roleIds: [
{ required: true, message: this.$t('validate.required'), trigger: 'blur' }

View File

@@ -52,13 +52,17 @@
</template>
<template v-else-if="item.prop === 'status'">
<el-switch
v-if="scope.row.id"
v-if="scope.row.id && hasPermission('editUser')"
v-model="scope.row.status"
active-value="1"
:disabled="(scope.row.username === loginName) || (scope.row.username==='admin' && scope.row.id===1) || scope.row.buildIn === 1"
inactive-value="0"
@change="()=>{statusChange(scope.row)}">
</el-switch>
<template v-else>
<span v-if="scope.row.status === '1'">{{$t('detection.create.enabled')}}</span>
<span v-else>{{$t('detection.create.disabled')}}</span>
</template>
</template>
<span v-else>{{scope.row[item.prop] || '-'}}</span>
</template>

View File

@@ -11,7 +11,7 @@
<div class="block-mode-title">{{ $t('detection.policy.indicatorMatch') }}</div>
<div class="block-mode-content">
{{ $t('detection.policy.indicatorMatchIntroduce') }}
<div v-if="language==='cn'" style="color: rgba(0,0,0,0)">0</div>
<div v-if="language===ZH" style="color: rgba(0,0,0,0)">0</div>
</div>
<div :class="settingObj.ruleType===detectionRuleType.indicator?'block-mode-btn-active':'block-mode-btn'"
@click="selectMode(detectionRuleType.indicator)">{{ $t('overall.select') }}
@@ -59,7 +59,7 @@
<el-option
v-for="item in eventTypeList"
:key="item.value"
:label="$t(item.label)"
:label="item.label"
:value="item.value"
/>
</el-select>
@@ -109,7 +109,7 @@
</template>
<script>
import { detectionRuleType, storageKey, detectionUnitList } from '@/utils/constants'
import { detectionRuleType, storageKey, detectionUnitList, ZH, EN } from '@/utils/constants'
import { switchStatus } from '@/utils/tools'
export default {
@@ -160,7 +160,8 @@ export default {
}
]
},
language: 'en'
language: EN,
ZH
}
},
watch: {
@@ -183,7 +184,7 @@ export default {
methods: {
switchStatus,
initData () {
this.language = localStorage.getItem(storageKey.language) || 'en'
this.language = localStorage.getItem(storageKey.language) || EN
this.categoryList = detectionUnitList.categoryList
this.eventTypeList = detectionUnitList.eventTypeList
},
@@ -196,6 +197,8 @@ export default {
if (this.settingObj.ruleType && this.settingObj.category && this.settingObj.eventType && this.settingObj.name) {
this.settingObj.settingNoContinue = true
this.onContinue()
} else {
this.$emit('setSettingForm', this.settingObj)
}
},
/** 点击继续,进行第二步 */

View File

@@ -507,6 +507,8 @@ export default {
this.$refs.form2.validate(valid2 => {
if (valid2) {
this.getConditions()
this.indicatorRuleObj.editFlag = false
this.indicatorRuleObj.saveFlag = true
this.$emit('setRuleObj', this.thresholdRuleObj)
}
})
@@ -534,6 +536,9 @@ export default {
if (this.indicatorRuleObj.dataSource && this.indicatorRuleObj.knowledgeId && this.indicatorRuleObj.level) {
this.indicatorRuleObj.ruleNoContinue = true
this.onContinue()
} else {
this.indicatorRuleObj.editFlag = true
this.$emit('setRuleObj', this.indicatorRuleObj)
}
}
}

View File

@@ -61,7 +61,7 @@
<div class="reference-tag__group">
<span class="reference-tag" v-for="(refer, index) in scope.row[item.prop].slice(0,2)" >{{refer}}</span>
</div>
<div class="reference-more">+{{scope.row[item.prop].length - 2}} more</div>
<div class="reference-more">+{{scope.row[item.prop].length - 2}} {{$t('overall.more')}}</div>
</div>
</template>
<div class="reference-tag__tip">
@@ -70,9 +70,11 @@
</el-popover>
</templage>
<template v-else>
<template v-for="(refer, index) in scope.row[item.prop]">
<div class="type-tag">{{refer}}</div>
</template>
<div class="reference-tag__show">
<div class="reference-tag__group">
<span class="reference-tag" v-for="(refer, index) in scope.row[item.prop]" >{{refer}}</span>
</div>
</div>
</template>
</template>
<template v-else-if="item.prop === 'opTime' || item.prop === 'ctime'">
@@ -96,6 +98,7 @@
</template>
<template v-else-if="item.prop === 'status'">
<el-switch v-model="scope.row.status"
v-if="hasPermission('editUserDefinedLibrary')"
active-color="#38ACD2"
inactive-color="#C0CEDB"
:active-value="1"
@@ -103,6 +106,10 @@
@change="changeStatus($event,scope.row.knowledgeId)"
>
</el-switch>
<template v-else>
<span v-if="scope.row.status === 1">{{$t('detection.create.enabled')}}</span>
<span v-else>{{$t('detection.create.disabled')}}</span>
</template>
</template>
<template v-else-if="item.prop === 'color'">
<div class="knowledge-color">
@@ -122,7 +129,7 @@
<script>
import table from '@/mixins/table'
import { knowledgeBaseCategory, knowledgeBaseSource } from '@/utils/constants'
import { knowledgeBaseCategory, knowledgeBaseSource, knowledgeBaseColor } from '@/utils/constants'
export default {
name: 'KnowledgeBaseTableForRow',
props: {
@@ -156,7 +163,7 @@ export default {
}, {
label: this.$t('knowledge.reference'),
prop: 'reference',
width: 180,
width: 190,
show: true
}, {
label: this.$t('overall.color'),
@@ -200,23 +207,7 @@ export default {
width: 80
}
],
knowledgeBaseColor: [
{
label: this.$t('knowledge.info'),
value: 'rgb(119,131,145)',
name: 'info'
},
{
label: this.$t('knowledge.benign'),
value: 'rgb(116,159,77)',
name: 'benign'
},
{
label: this.$t('knowledge.malicious'),
value: 'rgb(226,97,84)',
name: 'malicious'
}
]
knowledgeBaseColor
}
},
watch: {
@@ -253,7 +244,7 @@ export default {
const vm = this
return function (color) {
const t = vm.knowledgeBaseColor.find(t => t.value === color)
return t ? t.label : vm.knowledgeBaseColor[0].label
return t ? vm.$t(t.label) : vm.$t(vm.knowledgeBaseColor[0].label)
}
},
colorName () {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
<template>
<el-table
id="pluginTable"
ref="dataTable"
:data="tableData"
:height="height"
empty-text=" "
border
class="plugin"
@header-dragend="dragend"
@sort-change="tableDataSort"
@selection-change="selectionChange"
>
<el-table-column
v-for="(item, index) in customTableTitles"
:key="item.prop+index"
:fixed="item.fixed"
:label="item.label"
:min-width="`${item.minWidth}`"
:prop="item.prop"
:resizable="true"
:sort-orders="['ascending', 'descending']"
:sortable="item.sortable"
:width="`${item.width}`"
>
<template #header>
<span class="data-column__span">{{item.label}}</span>
<div class="col-resize-area"></div>
</template>
<template #default="scope" :column="item">
<template v-if="item.prop === 'triggerStatus'">
<el-switch
v-model="scope.row.triggerStatus"
active-value="1"
inactive-value="0"
@change="()=>{statusChange(scope.row)}">
</el-switch>
</template>
<template v-else-if="item.prop === 'type'">
<span class="type-tag" v-for="type in scope.row.type">{{type}}</span>
</template>
<template v-else-if="item.prop === 'name'">
<div class="plugin-name">
<div class="icon-background"><img class="plugin-name-icon" :src="scope.row['iconUrl']"/></div>
{{scope.row['name']}}
</div>
</template>
<template v-else-if="item.prop === 'description'">
<div class="two-line" :title="$t(scope.row['desc'])">{{$t(scope.row['desc'])}}</div>
</template>
<template v-else-if="item.prop === 'schedule'">
<div class="two-line" >{{$t(scope.row['schedule'])}}</div>
</template>
<span v-else>{{scope.row[item.prop] || '-'}}</span>
</template>
</el-table-column>
<template v-slot:empty >
<div class="table-no-data" v-if="isNoData">
<div class="table-no-data__title">{{ $t('npm.noData') }}</div>
</div>
</template>
</el-table>
</template>
<script>
import table from '@/mixins/table'
import axios from 'axios'
import { api } from '@/utils/api'
import { pluginBasicInfo } from '@/utils/constants'
export default {
name: 'pluginTable',
props: {
isNoData: {
type: Boolean,
default: false
}
},
mixins: [table],
data () {
return {
tableTitle: [ // 原始table列
{
label: this.$t('overall.name'),
prop: 'name',
show: true,
minWidth: 100
}, {
label: this.$t('overall.remark'),
prop: 'description',
show: true,
minWidth: 150
}, {
label: this.$t('overall.type'),
prop: 'type',
show: true,
minWidth: 150
}, {
label: this.$t('config.plugin.schedule'),
prop: 'schedule',
show: true,
minWidth: 150
}, {
label: this.$t('overall.status'),
prop: 'triggerStatus',
show: true,
minWidth: 200
}
]
}
},
computed: {
},
methods: {
statusChange (plugin) {
let triggerStatus = plugin.triggerStatus
let statusUrl = triggerStatus === '1' ? api.pluginStatusEnable : api.pluginStatusDisable
statusUrl = statusUrl.replace('{{id}}', plugin.id)
axios.post(statusUrl).then(response => {
if (response.status === 200) {
this.$message({ duration: 1000, type: 'success', message: this.$t('tip.operateSuccess') })
} else {
this.$message.error(response.data.message)
}
this.$emit('reload')
}).catch(e => {
console.error(e)
this.$message.error(this.errorMsgHandler(e))
})
}
}
}
</script>

View File

@@ -1,16 +1,17 @@
import { createI18n } from 'vue-i18n/index'
import { storageKey } from '@/utils/constants'
import { storageKey, EN } from '@/utils/constants'
import { getI18n } from '@/utils/api'
import store from '@/store'
const i18n = createI18n({
locale: localStorage.getItem(storageKey.language) || 'en'
locale: localStorage.getItem(storageKey.language) || EN
})
export async function loadI18n () {
if (!store.state.i18n) {
const items = await getI18n()
if (items) {
store.commit('loadI18n')
store.state.i18nObj = items
Object.keys(items).forEach(lang => {
i18n.global.mergeLocaleMessage(lang, items[lang])
})

View File

@@ -5,9 +5,8 @@ import router from '@/router'
import store from '@/store'
import App from '@/App.vue'
import '@/utils/http.js'
import { hasPermission } from '@/permission'
import commonMixin from '@/mixins/common'
import { cancelWithChange, noData } from '@/utils/tools'
import { cancelWithChange, noData, myHighLight } from '@/utils/tools'
import { ClickOutside } from 'element-plus/lib/directives'
import i18n from '@/i18n'
// import '@/mock/index.js'
@@ -22,6 +21,7 @@ import DateTimeRange from '@/components/common/TimeRange/DateTimeRange'
import TimeRefresh from '@/components/common/TimeRange/TimeRefresh'
import PanelChartList from '@/views/charts/PanelChartList'
import Error from '@/components/common/Error'
import Renderer from '@/components/advancedSearch/showhint/Hint/Renderer'
import 'lib-flexible'
const emitter = new bus()
@@ -37,10 +37,10 @@ app.use(i18n)
app.use(hljsVuePlugin)
app.use(VueGridLayout)
app.directive('has', hasPermission) // 注册指令
app.directive('ele-click-outside', ClickOutside)
app.directive('cancel', cancelWithChange)
app.directive('no-data', noData)
app.directive('high-light', myHighLight)
app.config.globalProperties.$_ = _
app.mixin(commonMixin)
@@ -49,6 +49,7 @@ app.component('date-time-range', DateTimeRange)
app.component('time-refresh', TimeRefresh)
app.component('panel-chart-list', PanelChartList)
app.component('chart-error', Error)
app.component('Renderer', Renderer)
app.mount('#app')

View File

@@ -1,4 +1,4 @@
import { hasButton } from '@/permission'
import { hasPermission } from '@/permission'
import { dateFormatByAppearance } from '@/utils/date-util'
import { commonErrorTip } from '@/utils/constants'
export default {
@@ -28,9 +28,7 @@ export default {
}
},
methods: {
hasButton (code) {
return hasButton(this.$store.getters.buttonList, code)
},
hasPermission,
errorMsgHandler (axiosError) {
if (axiosError.response) {
if (axiosError.response.data) {

View File

@@ -353,7 +353,9 @@ export default {
},
dragend () {
this.$nextTick(() => {
if (this.$refs.dataTable && this.$refs.dataTable.$refs.dataTable) {
if (this.$refs.dataTable && this.$refs.dataTable.$refs &&
this.$refs.dataTable.$refs.dataTable
) {
this.$refs.dataTable.$refs.dataTable.doLayout()
}
})

View File

@@ -17,7 +17,7 @@ export default {
// 请求数据 relationshipUrlOne => 路由 refOne => ref
getRelatedServerDataOne (relationshipUrlOne, refOne) {
this.loadingRelationshipOne = true
axios.get(relationshipUrlOne, { params: this.getQueryParams() }).then(response => {
axios.get(relationshipUrlOne, { params: this.getQueryParams(DEFAULT_TIME_FILTER_RANGE.entity.relatedEntity) }).then(response => {
if (response.status === 200) {
const relationshipDataOne = []
if (response.data.data.result.length > 0) {
@@ -33,7 +33,7 @@ export default {
},
getRelatedServerDataTwo (relationshipUrlTow, refTow) {
this.loadingRelationshipTwo = true
axios.get(relationshipUrlTow, { params: this.getQueryParams() }).then(response => {
axios.get(relationshipUrlTow, { params: this.getQueryParams(DEFAULT_TIME_FILTER_RANGE.entity.relatedEntity) }).then(response => {
if (response.status === 200) {
const relationshipDataTwo = []
if (response.data.data.result.length > 0) {

View File

@@ -1,4 +1,4 @@
import { chartTableOrderOptionsMapping, storageKey, knowledgeCategoryValue } from '@/utils/constants'
import { chartTableOrderOptionsMapping, storageKey, knowledgeCategoryValue, ZH } from '@/utils/constants'
import { getWidthByLanguage } from '@/utils/tools'
import { api } from '@/utils/api'
import axios from 'axios'
@@ -58,7 +58,7 @@ export default {
const language = localStorage.getItem(storageKey.language)
// 文字所占宽度一个英文字母占7px中文16px
let num = getWidthByLanguage(language) || 7
if (language !== 'cn') {
if (language !== ZH) {
num = num + 1 // 最后一位加空格
}

View File

@@ -32,14 +32,19 @@ router.beforeEach(async (to, from, next) => {
store.commit('setMenuList', menuList)
store.commit('setButtonList', buttonList)
store.commit('setRoleList', roleList)
}
if (to.path) {
next()
/* if (hasMenu(store.getters.menuList, to.path)) {
const homeRoute = {
path: '/',
name: 'home',
component: () => import('@/components/layout/Home'),
children: []
}
handleRoutes(menuList, homeRoute.children)
router.addRoute(homeRoute)
next({ ...to, replace: true })
} else {
if (to.path) {
next()
} else {
ElMessage.error('No access') // TODO 国际化
} */
}
}
}
} else {
@@ -60,14 +65,14 @@ router.beforeEach(async (to, from, next) => {
}
})
// menuList中是否包含route权限
export function hasMenu (menuList, route) {
// menuList中是否包含code
export function hasMenu (menuList, code) {
return menuList.some(menu => {
if (menu.route === route) {
if (menu.code === code) {
return true
} else {
if (menu.children) {
if (hasMenu(menu.children, route)) {
if (hasMenu(menu.children, code)) {
return true
}
}
@@ -96,36 +101,9 @@ export function hasButton (buttonList, code) {
return buttonList.some(button => button === code)
}
// 用法 v-has="code" | v-has="[code...]" 任意匹配一个 | v-has:all="[code...]" 全匹配
export const hasPermission = {
beforeMount (el, binding) {
// 节点权限处理
const buttonCode = binding.value
const arg = binding.arg
if (buttonCode) {
if (buttonCode instanceof Array) {
let has = true
if (arg && arg === 'all') { // 全匹配
buttonCode.forEach(button => {
if (has) {
has = hasButton(store.getters.buttonList, button)
}
})
} else { // 任意匹配
has = buttonCode.some(button => {
return hasButton(store.getters.buttonList, button)
})
}
if (!has) {
el.parentNode.removeChild(el)
}
} else { // 单个匹配
if (!hasButton(store.getters.buttonList, buttonCode)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
}
}
// 根据code从menuList和buttonList中判断是否有权限
export function hasPermission (code) {
return hasButton(store.getters.buttonList, code) || hasMenu(store.getters.menuList, code)
}
// 根据orderNum排序
@@ -160,3 +138,106 @@ export function getWelcomeMenu (menu) {
}
}
}
export function handleComponent (code) {
switch (code) {
case 'networkOverview':
case 'networkAppPerformance':
case 'dnsServiceInsights':
case 'linkMonitor':
return () => import('@/views/charts2/Panel')
case 'entity':
return () => import('@/views/entityExplorer/EntityExplorer')
case 'entityDetail':
return () => import('@/views/entityExplorer/EntityDetail')
case 'entityGraph':
return () => import('@/views/entityExplorer/EntityGraph')
case 'securityEvents':
case 'performanceEvents':
return () => import('@/views/detections/Index')
case 'detectionPolicy':
return () => import('@/views/detections/detectionPolicies/Index')
case 'createDetectionPolicy':
case 'editDetectionPolicy':
return () => import('@/views/detections/detectionPolicies/PolicyForm')
case 'report':
return () => import('@/views/report/Report')
case 'knowledgeBase':
return () => import('@/views/setting/KnowledgeBase')
case 'userDefinedLibrary':
return () => import('@/views/setting/KnowledgeBaseUserDefinedList')
case 'createUserDefinedLibrary':
case 'editUserDefinedLibrary':
return () => import('@/views/setting/KnowledgeBaseForm')
case 'administration':
return () => import('@/views/administration/Index')
case 'user':
return () => import('@/views/administration/User')
case 'role':
return () => import('@/views/administration/Roles')
case 'operationLog':
return () => import('@/views/administration/OperationLog')
case 'appearance':
return () => import('@/views/administration/Appearance')
case 'license':
return () => import('@/views/administration/License')
case 'I18N':
return () => import('@/views/administration/I18n')
case 'system':
return () => import('@/views/system/Index')
case 'plugin':
return () => import('@/views/system/Plugin')
default:
return null
}
}
export function handleRoutes (menus, routes) {
menus.forEach(menu => {
if (menu.route === '' && (!menu.children || menu.children.length < 0)) {
return false
}
// administration的路由使用了嵌套其他的是平铺
if (menu.pid === 0 && (menu.code === 'administration' || menu.code === 'system')) {
const path = menu.route.replace('redirect:', '')
if (menu.children && menu.children.length > 0) {
const route = {
name: menu.name,
path,
redirect: menu.children[0].route,
component: handleComponent(menu.code),
children: []
}
menu.children.forEach(c => {
route.children.push({
name: c.name,
path: c.route,
component: handleComponent(c.code)
})
})
routes.push(route)
}
} else {
if (menu.route && menu.route.startsWith('redirect:')) {
const path = menu.route.replace('redirect:', '')
if (menu.children && menu.children.length > 0) {
routes.push({
name: menu.name,
path,
redirect: menu.children[0].route
})
handleRoutes(menu.children, routes)
}
} else {
routes.push({
name: menu.code,
path: menu.route,
component: handleComponent(menu.code)
})
}
if (menu.children && menu.children.length > 0) {
handleRoutes(menu.children, routes)
}
}
})
}

View File

@@ -5,106 +5,6 @@ const routes = [
{
path: '/login',
component: () => import('@/Login')
},
{
path: '/',
component: () => import('@/components/layout/Home'),
children: [
{
name: 'panel',
path: '/panel/:typeName',
component: () => import('@/views/charts2/Panel')
},
{
path: '/report/builtIn',
component: () => import('@/views/report/Report')
},
{
path: '/entityExplorer',
component: () => import('@/views/entityExplorer/EntityExplorer')
},
{
path: '/entityDetail',
component: () => import('@/views/entityExplorer/EntityDetail')
},
{
path: '/entityGraph',
component: () => import('@/views/entityExplorer/EntityGraph')
},
{
path: '/detection',
redirect: '/detection/securityEvent'
},
{
path: '/detection/:typeName',
component: () => import('@/views/detections/Index')
},
{
path: '/businessLog/viewer',
component: () => import('@/views/businessLog/Viewer')
},
{
path: '/knowledgeBase',
component: () => import('@/views/setting/KnowledgeBase')
},
{
path: '/knowledgeBase/userDefinedLibrary',
component: () => import('@/views/setting/KnowledgeBase')
},
{
path: '/knowledgeBase/userDefinedLibrary/create',
component: () => import('@/views/setting/KnowledgeBaseForm')
},
{
path: '/knowledgeBase/userDefinedLibrary/edit',
component: () => import('@/views/setting/KnowledgeBaseForm')
},
{
name: 'Administration',
path: '/administration',
redirect: '/administration/user',
component: () => import('@/views/administration/Index'),
children: [
{
name: 'User',
path: '/administration/user',
component: () => import('@/views/administration/User')
},
{
name: 'Role',
path: '/administration/role',
component: () => import('@/views/administration/Roles')
},
{
name: 'OperationLog',
path: '/administration/operationLog',
component: () => import('@/views/administration/OperationLog')
},
{
name: 'Appearance',
path: '/administration/appearance',
component: () => import('@/views/administration/Appearance')
}
]
},
{
name: 'I18n',
path: '/i18n',
component: () => import('@/views/administration/I18n')
},
{
path: '/detectionPolicy',
component: () => import('@/views/detections/detectionPolicies/Index')
},
{
path: '/detectionPolicy/create',
component: () => import('@/views/detections/detectionPolicies/PolicyForm')
},
{
path: '/detectionPolicy/edit',
component: () => import('@/views/detections/detectionPolicies/PolicyForm')
}
]
}
]

View File

@@ -12,7 +12,8 @@ const store = createStore({
i18n: false,
showEntityTypeSelector: false, // 在entity explore页面时控制header显示实体类型选择框
from: '', // entity type
test: 'jest' // 用于单测的demo
test: 'jest', // 用于单测的demo
i18nObj: {} // 存放i18n的值用于搜索组件切换环境时参数包含其他语言时使用的
}
},
getters: {

View File

@@ -1,6 +1,6 @@
import axios from 'axios'
import router from '@/router'
import { getWelcomeMenu, sortByOrderNum } from '@/permission'
import { getWelcomeMenu, sortByOrderNum, handleRoutes } from '@/permission'
import { ElMessage } from 'element-plus' // dependent on utc plugin
import { dbDrilldownTableConfig, storageKey } from '@/utils/constants'
import { getConfigVersion } from '@/utils/tools'
@@ -64,7 +64,14 @@ const user = {
store.commit('setMenuList', menuList)
store.commit('setButtonList', res2.data.buttons)
store.commit('setRoleList', res2.data.roles)
const homeRoute = {
path: '/',
name: 'home',
component: () => import('@/components/layout/Home'),
children: []
}
handleRoutes(menuList, homeRoute.children)
router.addRoute(homeRoute)
if (res.loginSuccessPath) {
let tempArr = res.loginSuccessPath.split('?')
const path = tempArr[0]
@@ -112,11 +119,21 @@ const user = {
}
}
})
axios.get(api.config, { params: { ckey: 'link_info' } }).then(response => {
axios.get(`${api.knowledgeBase}/13?pageSize=-1`).then(response => {
const res = response.data
if (response.status === 200 && res.page.list && res.page.list.length > 0) {
localStorage.setItem(storageKey.linkInfo, res.page.list[0].cvalue)
if (response.status === 200 && res.data.itemList && res.data.itemList.length > 0) {
res.data.itemList.sort((a, b) => {
if (a.inLinkId !== b.inLinkId) {
return a.inLinkId - b.inLinkId
}
return a.outLinkId - b.outLinkId
})
localStorage.setItem(storageKey.linkInfo, JSON.stringify(res.data.itemList))
} else {
localStorage.setItem(storageKey.linkInfo, '')
}
}).catch(e => {
localStorage.setItem(storageKey.linkInfo, '')
})
axios.get(api.config, { params: { ckey: 'schema_explore' } }).then(response => {
const res = response.data

View File

@@ -15,6 +15,9 @@ export const api = {
logout: '/logout',
pin: 'sys/user/pin',
appearance: '/sys/appearance',
license: '/sys/license/detail',
licenseStatus: '/sys/license/status',
downloadLicenseC2v: '/sys/license/download',
permissions: '/sys/user/permissions',
operationLog: '/sys/log',
login: '/sys/login',
@@ -44,6 +47,10 @@ export const api = {
updateKnowledgeUrl: apiVersion + '/knowledgeBase/items/batch',
knowledgeBaseLog: apiVersion + '/knowledgeBase/audit/log',
knowledgeBaseTimedistribution: apiVersion + '/knowledgeBase/{{knowledgeId}}/{{type}}/timedistribution',
// 插件
pluginList: apiVersion + '/plugin/intelligence-learning/list',
pluginStatusEnable: apiVersion + '/plugin/intelligence-learning/{{id}}/start',
pluginStatusDisable: apiVersion + '/plugin/intelligence-learning/{{id}}/stop',
// 报告相关
reportJob: '/report/job',
@@ -275,6 +282,9 @@ export const api = {
domainNameResolutionAboutAppsOfDomain: apiVersion + '/entity/detail/domain/relate/apps',
domainNameResolutionAboutIpsOfDomain: apiVersion + '/entity/detail/domain/relate/ips',
domainNameResolutionAboutFQDNsOfDomain: apiVersion + '/entity/detail/domain/relate/fqdns',
// subscriber
subscriberKpi: apiVersion + '/entity/detail/traffic/overview/subscriber',
subscriberTopApp: apiVersion + '/entity/detail/subscriber/relate/apps',
// 开放端口ip、domain、app相关
openPortOfIp: apiVersion + '/entity/detail/ip/relate/ports',
openPortOfDomain: apiVersion + '/entity/detail/domain/relate/ports',
@@ -282,6 +292,9 @@ export const api = {
basicInfo: apiVersion + '/entity/detail/basic',
tags: apiVersion + '/entity/detail/kb/intelligence/tag',
informationAggregation: apiVersion + '/entity/detail/kb/intelligence/list',
deviceInformation: apiVersion + '/entity/detail/subscriber/device', // 暂时写的值
accountInformation: apiVersion + '/entity/detail/subscriber/account', // 暂时写的值
locationTrack: apiVersion + '/entity/detail/subscriber/track',
// 实体关系
entityGraph: {
basicInfo: apiVersion + '/entity/graph/relation/basic',

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More