Compare commits
160 Commits
dev-23.11
...
dev-24.01-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c4993cf12 | ||
|
|
b8ee685230 | ||
|
|
30b1328576 | ||
|
|
740e7bbae6 | ||
|
|
f020fe33b5 | ||
|
|
75b47d7ba7 | ||
|
|
a26b0c848d | ||
|
|
d2ad042cf9 | ||
|
|
a844d44d1d | ||
|
|
ad603339de | ||
|
|
08f0cd4522 | ||
|
|
0c20757d07 | ||
|
|
2c7c7293b4 | ||
|
|
d035677c2f | ||
|
|
50cdbde8bf | ||
|
|
d27738b9ec | ||
|
|
85456c1e47 | ||
|
|
327cbde233 | ||
|
|
cbf71eb1b9 | ||
|
|
4e0eb3f143 | ||
|
|
a17c68bb26 | ||
|
|
d1ae513124 | ||
|
|
91915996c0 | ||
|
|
8f17647752 | ||
|
|
29a319609e | ||
|
|
08ae827e31 | ||
|
|
854a846296 | ||
|
|
42aa7671a3 | ||
|
|
fb89134f95 | ||
|
|
584ad8d538 | ||
|
|
c43a17c984 | ||
|
|
df5decc3b4 | ||
|
|
cca54359a8 | ||
|
|
9e8b3e9d64 | ||
|
|
8e5f5b49cb | ||
|
|
4441bdef10 | ||
|
|
849df8c8d8 | ||
|
|
475b112835 | ||
|
|
b286861137 | ||
|
|
e67ec0905a | ||
|
|
7f697658f4 | ||
|
|
ff17e52bc2 | ||
|
|
b13e313114 | ||
|
|
dbf5885b72 | ||
|
|
ab10f7ff0f | ||
|
|
6d00137260 | ||
|
|
ddafbf44f2 | ||
|
|
096666ce69 | ||
|
|
bf34b4321c | ||
|
|
83c8f400e2 | ||
|
|
cb93658be4 | ||
|
|
e546f39c93 | ||
|
|
a3e770963f | ||
|
|
8e4d5bff88 | ||
|
|
877c008fe7 | ||
|
|
fc85069227 | ||
|
|
9ac9cd93e1 | ||
|
|
426301fce0 | ||
|
|
b8c90fee71 | ||
|
|
9ae4453587 | ||
|
|
734bfb33d3 | ||
|
|
ada66ee9d2 | ||
|
|
facbd0f17c | ||
|
|
471e5f2342 | ||
|
|
ad800637fd | ||
|
|
03400f1fab | ||
|
|
0e51d49e15 | ||
|
|
a3ab4530b7 | ||
|
|
a8ecdd48bd | ||
|
|
6bdefa5497 | ||
|
|
e0a8815d4a | ||
|
|
f4bf9a1f31 | ||
|
|
2661b5e69d | ||
|
|
0e13925908 | ||
|
|
2219bba781 | ||
|
|
80027fbd46 | ||
|
|
8b81181ae2 | ||
|
|
a9215469fb | ||
|
|
467b2c27b7 | ||
|
|
ca3b9d926c | ||
|
|
160f708596 | ||
|
|
3d3ad53395 | ||
|
|
c874665397 | ||
|
|
f40e9ae759 | ||
|
|
1f33834146 | ||
|
|
cbb27e2538 | ||
|
|
1d5bd39f54 | ||
|
|
2e113df17b | ||
|
|
ec0b3b28d4 | ||
|
|
83971fa08b | ||
|
|
fc24f6bbfb | ||
|
|
d7bd49a9ac | ||
|
|
37238377d2 | ||
|
|
b42bfded33 | ||
|
|
50e4112181 | ||
|
|
70ab0a46e6 | ||
|
|
cb6d0e4765 | ||
|
|
15b90c11e9 | ||
|
|
3f97c3e97f | ||
|
|
e70fa5d569 | ||
|
|
5ba4d8a1fe | ||
|
|
4ae22a1ea0 | ||
|
|
e08a7a2434 | ||
|
|
2dbf5ee84e | ||
|
|
b7c5638c04 | ||
|
|
d1e8430e37 | ||
|
|
2028861b26 | ||
|
|
21eafd4087 | ||
|
|
899b0e1e9d | ||
|
|
eb7a9f875c | ||
|
|
0f2f5ecdde | ||
|
|
164089f99e | ||
|
|
abab03eb12 | ||
|
|
19160c0da1 | ||
|
|
56ad79bd0d | ||
|
|
a2a7bdcd14 | ||
|
|
1d67584c9a | ||
|
|
8b72a37489 | ||
|
|
e13c9afe78 | ||
|
|
8001d66ca8 | ||
|
|
d2cb42687e | ||
|
|
a3c8baea5c | ||
|
|
f5f857bcb4 | ||
|
|
3d1fbfa5fd | ||
|
|
3d5c69d87b | ||
|
|
e1a26b60ae | ||
|
|
a7dfa33da2 | ||
|
|
67189ade10 | ||
|
|
6c9a05f98d | ||
|
|
720754bee2 | ||
|
|
27c1d28870 | ||
|
|
535d2646b2 | ||
|
|
51d6c1d4be | ||
|
|
742cf5d6f2 | ||
|
|
a1ae084216 | ||
|
|
bca51683e5 | ||
|
|
da2ac78ea7 | ||
|
|
c55a205003 | ||
|
|
f54ca30a58 | ||
|
|
ab30ddf49b | ||
|
|
8cd3f87c3e | ||
|
|
5106f23c2b | ||
|
|
27e59586ed | ||
|
|
da9ae6831e | ||
|
|
2f315bf52c | ||
|
|
2b3f6e6b31 | ||
|
|
74d95f110e | ||
|
|
2c12284415 | ||
|
|
60d06792b4 | ||
|
|
8bbebc3a8b | ||
|
|
d43a9c8bba | ||
|
|
d996769f7e | ||
|
|
a9e84fd714 | ||
|
|
6450e8e050 | ||
|
|
f25805ea0a | ||
|
|
5d42334074 | ||
|
|
b30a8a8a25 | ||
|
|
bfb1d93d95 | ||
|
|
0fa7aeebe6 | ||
|
|
6e8566db75 |
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,20 +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: 60, // 所有dashboard
|
||||
entity: {
|
||||
list: 60,
|
||||
trafficLine: 60,
|
||||
informationAggregation: 0,
|
||||
relatedEntity: 60 * 24 * 7,
|
||||
openPort: 60 * 24 * 7,
|
||||
securityEvent: 60 * 24 * 7,
|
||||
performanceEvent: 60 * 24 * 7,
|
||||
behaviorPattern: 60 * 24 * 7
|
||||
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
|
||||
detection: 60 // 事件日志列表
|
||||
}
|
||||
|
||||
BIN
public/images/entity-detail/track-point.png
Normal file
|
After Width: | Height: | Size: 881 B |
BIN
public/images/knowledge-base-logo/cyber-ghost.png
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
public/images/knowledge-base-logo/gecko.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/images/knowledge-base-logo/hotspot-vpn.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/knowledge-base-logo/ip-vanish.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/knowledge-base-logo/ivacy.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/images/knowledge-base-logo/protonvpn.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/images/knowledge-base-logo/turbo.png
Normal file
|
After Width: | Height: | Size: 704 B |
BIN
public/images/knowledge-base-logo/vpnunlimited.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/images/knowledge-base-logo/windscribe.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
162
src/Login.vue
@@ -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 {
|
||||
@@ -55,7 +80,11 @@ export default {
|
||||
loading: false,
|
||||
username: '',
|
||||
pin: '',
|
||||
language: ''
|
||||
language: '',
|
||||
licenseStatus: 1,
|
||||
licenseStatusErrMsg: '',
|
||||
downloadC2vUrl: api.downloadLicenseC2v,
|
||||
supportID: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -69,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 => {
|
||||
@@ -103,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) {
|
||||
@@ -128,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([])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,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);
|
||||
@@ -198,6 +300,7 @@ export default {
|
||||
|
||||
.title {
|
||||
margin-top: 65px;
|
||||
margin-bottom: 44px;
|
||||
text-align: center;
|
||||
}
|
||||
.title > img {
|
||||
@@ -208,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;
|
||||
@@ -220,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;
|
||||
@@ -234,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>
|
||||
|
||||
@@ -175,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
|
||||
@@ -356,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
.error-component {
|
||||
position: absolute;
|
||||
//width: 100%;
|
||||
//height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
.error-block {
|
||||
display: inline-block;
|
||||
|
||||
@@ -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';
|
||||
|
||||
61
src/assets/css/components/views/administration/License.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
61
src/assets/css/components/views/system/Plugin.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "cn-icon"; /* Project id 2614877 */
|
||||
src: url('iconfont.woff2?t=1699411209748') format('woff2'),
|
||||
url('iconfont.woff?t=1699411209748') format('woff'),
|
||||
url('iconfont.ttf?t=1699411209748') 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,62 @@
|
||||
-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";
|
||||
}
|
||||
@@ -337,7 +393,7 @@
|
||||
content: "\e7b4";
|
||||
}
|
||||
|
||||
.cn-icon-a-GeneralSettings:before {
|
||||
.cn-icon-general-setting:before {
|
||||
content: "\e7b5";
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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"
|
||||
@@ -96,7 +147,7 @@
|
||||
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>
|
||||
@@ -106,7 +157,7 @@
|
||||
<span v-show="metaList.length>0" class="search__suffix search__suffix-close" @click="cleanMetaList">
|
||||
<i class="el-icon-error"></i>
|
||||
</span>
|
||||
<span class="search__suffix" @click="search">
|
||||
<span test-id="tag-search" class="search__suffix" @click="search">
|
||||
<i class="el-icon-search"></i>
|
||||
</span>
|
||||
</div>
|
||||
@@ -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来确定,一般为=,IN,tags操作符为has
|
||||
}
|
||||
},
|
||||
emits: ['changeMode', 'search'],
|
||||
inject: ['myHighLight'],
|
||||
methods: {
|
||||
// 新增条件
|
||||
addCondition (meta) {
|
||||
@@ -218,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)
|
||||
@@ -248,27 +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.column.show = true
|
||||
meta.column.isFullText = true
|
||||
meta.operator.show = false
|
||||
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
|
||||
},
|
||||
@@ -279,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 in,column的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 {
|
||||
@@ -292,10 +430,17 @@ export default {
|
||||
}
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
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,所以是数组
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
operatorClick (meta) {
|
||||
@@ -441,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(() => {
|
||||
if (this.$refs.valueInput) {
|
||||
this.$refs.valueInput[0].focus()
|
||||
}
|
||||
if (this.$refs.valuesSelect.length > 0) {
|
||||
// 触发focus后,select弹窗并没有生效
|
||||
this.$refs.valuesSelect[0].focus(meta)
|
||||
}
|
||||
})
|
||||
},
|
||||
// 判断是否是用户自己添加的内容,用于判断是否是全局搜索
|
||||
@@ -462,21 +623,19 @@ export default {
|
||||
search () {
|
||||
if (this.metaList.length > 0) {
|
||||
const parser = new Parser(this.columnList)
|
||||
const errorList = parser.validateMeta(this.metaList)
|
||||
const keywordList = []
|
||||
this.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 })
|
||||
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)
|
||||
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]))
|
||||
@@ -642,16 +801,18 @@ export default {
|
||||
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
|
||||
}
|
||||
|
||||
if (!this.isUnitTesting) {
|
||||
this.emitter.on('advanced-search', function () {
|
||||
vm.search()
|
||||
})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
convertMetaList: {
|
||||
|
||||
@@ -1,37 +1,56 @@
|
||||
<template>
|
||||
<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">
|
||||
<div class="search__suffixes search__suffixes--text-mode" :class="showList ? '' : 'entity-explorer-home'" style="padding-left: 1px;background: #fff;">
|
||||
<!--切换text、tag模式图标-->
|
||||
<span class="search__suffix">
|
||||
<el-popover
|
||||
popper-class="my-popper-class"
|
||||
placement="top"
|
||||
trigger="hover"
|
||||
:content="$t('entity.switchToAdvancedSearch')"
|
||||
:content="$t('overall.switchToTag')"
|
||||
>
|
||||
<template #reference>
|
||||
<i class="cn-icon cn-icon-filter" @click="changeMode"></i>
|
||||
<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" @click="search">
|
||||
<!--搜索图标-->
|
||||
<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'
|
||||
@@ -39,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('')
|
||||
@@ -66,52 +127,74 @@ export default {
|
||||
this.reloadUrl(routeQuery, 'cleanOldParams')
|
||||
},
|
||||
initCodeMirror () {
|
||||
this.codeMirror = CodeMirror.fromTextArea(this.$refs.textSearch, {
|
||||
mode: {
|
||||
name: 'sql'
|
||||
},
|
||||
let option = {
|
||||
mode: 'sql',
|
||||
placeholder: '',
|
||||
lineNumbers: false
|
||||
})
|
||||
}
|
||||
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 = CodeMirror.fromTextArea(this.$refs.textSearch, option)
|
||||
if (this.codeMirror) {
|
||||
this.codeMirror.setOption('extraKeys', {
|
||||
Enter: (cm) => {}
|
||||
})
|
||||
this.codeMirror.on('focus', () => {
|
||||
if (this.codeMirror.getValue().trim() !== '') {
|
||||
this.isEdit = true
|
||||
this.isCloseIcon = true
|
||||
this.setCodemirrorValue()
|
||||
this.initEvent()
|
||||
this.initHint()
|
||||
}
|
||||
})
|
||||
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
|
||||
})
|
||||
},
|
||||
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 = []
|
||||
metaList.forEach(item => {
|
||||
if (item.column && item.column.type === columnType.fullText) {
|
||||
keywordList.push({ type: item.column.type, value: item.column.label })
|
||||
} else if (item.column && item.column.type === columnType.string) {
|
||||
keywordList.push({ type: item.column.type, value: item.value.value })
|
||||
}
|
||||
})
|
||||
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, keywordList: keywordList })
|
||||
// 补全模糊搜索
|
||||
if (!this.isUnitTesting) {
|
||||
toRaw(this.codeMirror).setValue(parser.handleEntityTypeByStr(str))
|
||||
}
|
||||
// 注:参数str,1.是用户搜索框的内容在补全模糊搜索后的内容;2.部分参数是用户主观可见,但格式不符合接口原则的,如status='Active',接口需要status=0
|
||||
this.$emit('search', { ...parser.parseStr(enumKey), str: parser.handleEntityTypeByStr(str), keywordList: keywordList })
|
||||
} else {
|
||||
this.$message.error(handleErrorTip(errorList[0]))
|
||||
}
|
||||
@@ -122,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()
|
||||
@@ -158,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') {
|
||||
@@ -167,7 +256,11 @@ export default {
|
||||
current = `${current ? current + ' AND ' : ''}${param.column}${handleOperatorSpace(param.operator)}${this.handleValue(param.value, column, param.operator)}`
|
||||
}
|
||||
})
|
||||
if (!this.isUnitTesting) {
|
||||
toRaw(this.codeMirror).setValue(current.trim())
|
||||
} else {
|
||||
this.myUnitTestStr = current
|
||||
}
|
||||
},
|
||||
removeParams (params) {
|
||||
let current = this.codeMirror.getValue()
|
||||
@@ -204,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: {
|
||||
@@ -227,42 +488,23 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// 如果地址栏包含参数q,则将参数q回显到搜索栏内
|
||||
let { q } = this.$route.query
|
||||
if (this.isShowHint) {
|
||||
this.$nextTick(() => {
|
||||
// dataset是避免数据未初始化完成注册失败,ref是因为组件加载2次,避免第二次时dom丢失导致数据挂载失败
|
||||
if (this.dataset && this.$refs.textSearch) {
|
||||
this.initShowHint()
|
||||
this.initCodeMirror()
|
||||
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)
|
||||
}
|
||||
}
|
||||
toRaw(this.codeMirror).setValue(q)
|
||||
} else {
|
||||
this.isCloseIcon = false
|
||||
}
|
||||
|
||||
const vm = this
|
||||
this.emitter.on('advanced-search', function () {
|
||||
vm.search()
|
||||
})
|
||||
} else if (this.$refs.textSearch) {
|
||||
this.initCodeMirror()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.el-popper.search-show-hint-popover {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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.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 if (q && (q.indexOf('has(') > -1)) {
|
||||
return {
|
||||
key: q,
|
||||
isKey: true
|
||||
}
|
||||
} 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
|
||||
@@ -1414,8 +1482,8 @@ export default class Parser {
|
||||
} else if (i.toLowerCase() === 'tag') {
|
||||
lastObj[i] = `has(${i},${commonObj[i]})`
|
||||
} else {
|
||||
// 单独存在的,直接保留 todo 后续观察当初添加单引号动机和问题
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 使用单引号包裹
|
||||
|
||||
171
src/components/advancedSearch/showhint/Hint/HelperInfo.vue
Normal 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>
|
||||
35
src/components/advancedSearch/showhint/Hint/Hint.vue
Normal 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>
|
||||
127
src/components/advancedSearch/showhint/Hint/HintInfo.vue
Normal 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>
|
||||
16
src/components/advancedSearch/showhint/Hint/Renderer.vue
Normal 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>
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
591
src/components/advancedSearch/showhint/TextSearch/show-hint.js
Normal 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);
|
||||
}
|
||||
372
src/components/advancedSearch/showhint/TextSearch/sql-hint.js
Normal 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)};
|
||||
});
|
||||
}
|
||||
480
src/components/advancedSearch/showhint/TextSearch/sql.js
Normal file
70
src/components/advancedSearch/showhint/const/defaultTips.js
Normal 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>)
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/components/advancedSearch/showhint/const/fieldTips.js
Normal 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>)
|
||||
}
|
||||
109
src/components/advancedSearch/showhint/const/filterTips.js
Normal 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 can‘t 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
|
||||
341
src/components/advancedSearch/showhint/const/functionTips.js
Normal 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>zero:zero "0"</li>
|
||||
<li>previous:previous value</li>
|
||||
<li>next:next 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 string:UNIX_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
|
||||
111
src/components/advancedSearch/showhint/const/operatorTips.js
Normal 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
|
||||
97
src/components/advancedSearch/showhint/const/sqlTips.js
Normal 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
|
||||
48
src/components/advancedSearch/showhint/const/varTips.js
Normal 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
|
||||
129
src/components/advancedSearch/showhint/myCodeMirror.js
Normal 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
|
||||
288
src/components/advancedSearch/showhint/packages/getDataset.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,21 +14,23 @@
|
||||
<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">{{$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="position: absolute;top: -53px;left: -536px;"
|
||||
:style="showPosition === 'left' ? datePickerLeftStyle : datePickerRightStyle"
|
||||
:clearable="false"
|
||||
:default-time="defaultTime"
|
||||
:unlink-panels="true"
|
||||
type="datetimerange"
|
||||
@blur="datePickerVisibleChange"
|
||||
@change="timeArrChange"
|
||||
/>
|
||||
</el-config-provider>
|
||||
@@ -70,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>
|
||||
@@ -107,6 +109,10 @@ export default {
|
||||
},
|
||||
style: {
|
||||
type: String
|
||||
},
|
||||
showPosition: {
|
||||
type: String,
|
||||
default: 'right'
|
||||
}
|
||||
},
|
||||
emits: ['change'],
|
||||
@@ -135,6 +141,17 @@ export default {
|
||||
return str
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 时间选择器失去焦点之后就会隐藏,此时,Left展示类型的,就需要重新设置下拉框的位置
|
||||
*/
|
||||
datePickerVisibleChange () {
|
||||
if (this.showPosition === 'left') {
|
||||
this.leftStyle = this.leftStyleBefore
|
||||
// this.dropdownFlag = true
|
||||
}
|
||||
}
|
||||
},
|
||||
setup (props, ctx) {
|
||||
// data
|
||||
const store = useStore()
|
||||
@@ -160,6 +177,12 @@ export default {
|
||||
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 '
|
||||
@@ -197,6 +220,17 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 监测下拉框,一旦隐藏,则设置其位置靠最左边
|
||||
* */
|
||||
watch(() => dropdownFlag.value, (newVal) => {
|
||||
if (!newVal) {
|
||||
if (props.showPosition === 'left') {
|
||||
leftStyle.value = leftStyleBefore
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// methods
|
||||
/**
|
||||
* 打开/关闭时间面板
|
||||
@@ -215,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))
|
||||
@@ -306,6 +340,8 @@ export default {
|
||||
locale = cn
|
||||
}
|
||||
|
||||
const keyValue = window.$dayJs.tz().valueOf()
|
||||
|
||||
return {
|
||||
myStartTime,
|
||||
myEndTime,
|
||||
@@ -322,6 +358,12 @@ export default {
|
||||
rangeHistory,
|
||||
rangeHistoryArr,
|
||||
getMillisecond,
|
||||
datePickerLeftStyle,
|
||||
datePickerRightStyle,
|
||||
leftStyle,
|
||||
leftStyleBefore,
|
||||
rightStyle,
|
||||
keyValue,
|
||||
myDatePickerShow,
|
||||
showDropdown,
|
||||
changeDropdown,
|
||||
|
||||
@@ -132,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>
|
||||
@@ -591,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 {
|
||||
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 ')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,21 +183,21 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
checkParentNode(node) {
|
||||
if(node && this.$refs.menuTree.getNode(node)){
|
||||
let parent = this.$refs.menuTree.getNode(node).parent
|
||||
let parentNode = parent.data
|
||||
if(parentNode && parentNode.id && parentNode.id !== 0 ){
|
||||
this.$refs.menuTree.setChecked(parentNode,true,false)
|
||||
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) {//如果是选中节点,则同步选中所有的父辈节点(有全选和半选两种状态)
|
||||
if (isCheck) { // 如果是选中节点,则同步选中所有的父辈节点(有全选和半选两种状态)
|
||||
this.checkParentNode(data)
|
||||
} else {//如果是取消节点,则同步取消选中所有子节点
|
||||
if(data.children && data.children.length > 0) {
|
||||
} else { // 如果是取消节点,则同步取消选中所有子节点
|
||||
if (data.children && data.children.length > 0) {
|
||||
data.children.forEach(node => {
|
||||
this.$refs.menuTree.setChecked(node, false, true)
|
||||
})
|
||||
|
||||
@@ -132,9 +132,9 @@ export default {
|
||||
}
|
||||
}
|
||||
const validateConfirmPin = (rule, value, callback) => { // 确认密码的二次校验
|
||||
if (_.isEmpty(value) && !_.isEmpty(this.editObject.pin)) {//密码有内容,确认密码没内容
|
||||
if (_.isEmpty(value) && !_.isEmpty(this.editObject.pin)) { // 密码有内容,确认密码没内容
|
||||
callback(new Error(this.$t('config.user.confirmPin')))
|
||||
} else if (!_.isEmpty(value) && value !== this.editObject.pin) {//密码有内容,确认密码也有内容,内容不一致
|
||||
} else if (!_.isEmpty(value) && value !== this.editObject.pin) { // 密码有内容,确认密码也有内容,内容不一致
|
||||
callback(new Error(this.$t('config.user.confirmPinErr')))
|
||||
} else {
|
||||
callback()
|
||||
|
||||
@@ -197,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)
|
||||
}
|
||||
},
|
||||
/** 点击继续,进行第二步 */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,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: {
|
||||
@@ -207,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: {
|
||||
@@ -260,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 () {
|
||||
|
||||
@@ -1,41 +1,5 @@
|
||||
<template>
|
||||
<template v-if="!isNoData">
|
||||
<div class="card-type-title" v-if="aiTaggingList.length > 0">{{$t('knowledgeBase.intelligenceLearning')}}</div>
|
||||
<el-checkbox-group v-model="checkList" >
|
||||
<div class="card-box" v-for="data in aiTaggingList" :key="data.knowledgeId">
|
||||
<div @click="isSelectedStatus && data.isBuiltIn !== 1 && clickCard(data,$event)" @mouseenter="mouseenter(data)" @mouseleave="mouseleave(data)" class="card-item" :class="data.isSelected ? 'card-selected' : ''">
|
||||
<div class="card-content">
|
||||
<div class="card-operate">
|
||||
<el-switch v-model="data.status"
|
||||
v-if="hasPermission('editBuiltInKnowledgeBase')"
|
||||
class="card-enable"
|
||||
active-color="#38ACD2"
|
||||
inactive-color="#C0CEDB"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
:before-change="(knowledgeId) => confirmSwitchLearning(data.knowledgeId)"
|
||||
>
|
||||
</el-switch>
|
||||
</div>
|
||||
<div class="card-icon">
|
||||
<img :src="data.iconUrl"/>
|
||||
</div>
|
||||
<div class="card-title">
|
||||
<div class="card-title-name" :title="$t(data.label)">{{$t(data.label)}}</div>
|
||||
</div>
|
||||
<div class="card-desc" :title="$t(data.desc)">{{$t(data.desc) || '—'}}</div>
|
||||
</div>
|
||||
<div class="card-operate__footer">
|
||||
<button v-if="data.showUpdate && hasPermission('editBuiltInKnowledgeBase')"
|
||||
class="top-tool-btn--update"
|
||||
@click="jumpToUpdatePage(data,true)">
|
||||
<i class="cn-icon-update-knowledge-base cn-icon"></i>
|
||||
<span>{{$t('overall.update')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
<div class="card-type-title" style="margin-top:4px;" v-if="websketchList.length > 0">{{$t('knowledgeBase.websketchIntegration')}}</div>
|
||||
<el-checkbox-group v-model="checkList" >
|
||||
<div class="card-box" v-for="data in websketchList" :key="data.knowledgeId">
|
||||
@@ -275,38 +239,24 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="showConfirmSwitch"
|
||||
:width="330"
|
||||
custom-class="confirm-knowledge-switch"
|
||||
:title="$t('overall.hint')"
|
||||
>
|
||||
<div class="dialog-message">{{ confirmSwitchLearningTip }}</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="cancleSwitch">{{ $t('overall.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="switchLearning">{{$t('tip.confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import table from '@/mixins/table'
|
||||
import Loading from '@/components/common/Loading'
|
||||
import { getSecond, getMillisecond, xAxisTimeFormatter, xAxisTimeRich } from '@/utils/date-util'
|
||||
import { knowledgeCategoryValue, unitTypes, storageKey, builtInKnowledgeBaseBasicInfo, knowledgeCardUpdateRecordType } from '@/utils/constants'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { api } from '@/utils/api'
|
||||
import { detectionTooltipFormatter } from '@/views/charts/charts/tools'
|
||||
import ChartNoData from '@/views/charts/charts/ChartNoData'
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import * as echarts from 'echarts'
|
||||
import unitConvert from '@/utils/unit-convert'
|
||||
import table from '@/mixins/table'
|
||||
import Loading from '@/components/common/Loading'
|
||||
import { getSecond, getMillisecond, xAxisTimeFormatter, xAxisTimeRich } from '@/utils/date-util'
|
||||
import { knowledgeCategoryValue, unitTypes, storageKey, builtInKnowledgeBaseBasicInfo, knowledgeCardUpdateRecordType } from '@/utils/constants'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { api } from '@/utils/api'
|
||||
import { detectionTooltipFormatter } from '@/views/charts/charts/tools'
|
||||
import ChartNoData from '@/views/charts/charts/ChartNoData'
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import * as echarts from 'echarts'
|
||||
import unitConvert from '@/utils/unit-convert'
|
||||
|
||||
export default {
|
||||
export default {
|
||||
name: 'knowledgeBaseTableForCard',
|
||||
mixins: [table],
|
||||
props: {
|
||||
@@ -326,7 +276,6 @@ export default {
|
||||
return {
|
||||
tableTitle: [],
|
||||
checkList: [],
|
||||
aiTaggingList: [],
|
||||
websketchList: [],
|
||||
showUpdateDialog: false,
|
||||
showConfirmDialog: false,
|
||||
@@ -464,7 +413,7 @@ export default {
|
||||
this.myChart.setOption(this.chartOption)
|
||||
})
|
||||
},
|
||||
init (val, show, active, n) {
|
||||
init () {
|
||||
this.psiphon3Loading = true
|
||||
const endTime = window.$dayJs.tz().valueOf()
|
||||
const params = {
|
||||
@@ -790,7 +739,6 @@ export default {
|
||||
tableData: {
|
||||
handler (n) {
|
||||
if (this.tableData && this.tableData.length > 0) {
|
||||
this.aiTaggingList = []
|
||||
this.websketchList = []
|
||||
this.tableData.forEach(item => {
|
||||
item.showUpdate = false
|
||||
@@ -800,9 +748,7 @@ export default {
|
||||
...item,
|
||||
...basicInfo
|
||||
}
|
||||
if (item.category === knowledgeCategoryValue.aiTagging) {
|
||||
this.aiTaggingList.push(item)
|
||||
} else if (item.category === knowledgeCategoryValue.webSketch) {
|
||||
if (item.category === knowledgeCategoryValue.webSketch) {
|
||||
this.websketchList.push(item)
|
||||
}
|
||||
}
|
||||
@@ -837,19 +783,16 @@ export default {
|
||||
this.chartOption = null
|
||||
window.addEventListener('resize', this.resize)
|
||||
|
||||
this.aiTaggingList = []
|
||||
this.websketchList = []
|
||||
this.tableData.forEach(item => {
|
||||
item.showUpdate = false
|
||||
if (item.category === knowledgeCategoryValue.aiTagging) {
|
||||
this.aiTaggingList.push(item)
|
||||
} else if (item.category === knowledgeCategoryValue.webSketch) {
|
||||
if (item.category === knowledgeCategoryValue.webSketch) {
|
||||
this.websketchList.push(item)
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeUnmount () {
|
||||
//clearTimeout(this.timer)
|
||||
// clearTimeout(this.timer)
|
||||
window.removeEventListener('resize', this.resize)
|
||||
const dom = document.getElementById('psiphonBarChart')
|
||||
if (dom) {
|
||||
@@ -871,21 +814,7 @@ export default {
|
||||
action: 'overwrite',
|
||||
description: this.updateObject.description
|
||||
}
|
||||
},
|
||||
confirmSwitchLearningTip () {
|
||||
let tip = ''
|
||||
if (this.switchKnowledgeId) {
|
||||
const find = this.aiTaggingList.find(item => item.knowledgeId === this.switchKnowledgeId)
|
||||
if (find) {
|
||||
if (find.status === 0) {
|
||||
tip = this.$t('tip.confirmEnablePsiphon3') + '?'
|
||||
} else if (find.status === 1) {
|
||||
tip = this.$t('tip.confirmDisablePsiphon3') + '?'
|
||||
}
|
||||
}
|
||||
}
|
||||
return tip
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
133
src/components/table/system/PluginTable.vue
Normal 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>
|
||||
@@ -11,6 +11,7 @@ export async function loadI18n () {
|
||||
const items = await getI18n()
|
||||
if (items) {
|
||||
store.commit('loadI18n')
|
||||
store.state.i18nObj = items
|
||||
Object.keys(items).forEach(lang => {
|
||||
i18n.global.mergeLocaleMessage(lang, items[lang])
|
||||
})
|
||||
|
||||
@@ -21,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()
|
||||
@@ -48,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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -179,8 +179,14 @@ export function handleComponent (code) {
|
||||
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
|
||||
}
|
||||
@@ -192,7 +198,7 @@ export function handleRoutes (menus, routes) {
|
||||
return false
|
||||
}
|
||||
// administration的路由使用了嵌套,其他的是平铺
|
||||
if (menu.pid === 0 && menu.code === '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 = {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -119,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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -57,6 +57,7 @@ export const fromRoute = {
|
||||
dnsServiceInsights: 'dnsServiceInsights',
|
||||
linkMonitor: 'linkMonitor',
|
||||
user: 'user',
|
||||
plugin: 'plugin',
|
||||
galaxyProxy: 'galaxyProxy',
|
||||
chart: 'chart',
|
||||
cryptocurrency: 'cryptocurrency',
|
||||
@@ -95,9 +96,10 @@ export const position = {
|
||||
}
|
||||
|
||||
export const entityType = {
|
||||
app: 'APP',
|
||||
domain: 'Domain',
|
||||
ip: 'IP'
|
||||
app: 'app',
|
||||
domain: 'domain',
|
||||
ip: 'ip',
|
||||
subscriber: 'subscriber'
|
||||
}
|
||||
|
||||
export const knowledgeCardUpdateRecordType = {
|
||||
@@ -109,12 +111,60 @@ export const entityDetailTabsName = {
|
||||
informationAggregation: 'informationAggregation',
|
||||
relatedEntity: 'relatedEntity',
|
||||
openPort: 'openPort',
|
||||
deviceInformation: 'deviceInformation',
|
||||
accountInformation: 'accountInformation',
|
||||
digitalCertificate: 'digitalCertificate',
|
||||
securityEvent: 'securityEvent',
|
||||
performanceEvent: 'performanceEvent',
|
||||
behaviorPattern: 'behaviorPattern'
|
||||
}
|
||||
|
||||
export const entityDetailTabConfig = [
|
||||
{
|
||||
name: 'app',
|
||||
config: [
|
||||
{ name: entityDetailTabsName.relatedEntity, label: 'entities.relatedEntity', icon: 'cn-icon cn-icon-domain-name-resolution', tag: 0 },
|
||||
{ name: entityDetailTabsName.openPort, label: 'entities.openPort', icon: 'cn-icon cn-icon-open-port', tag: 0 },
|
||||
// { name: entityDetailTabsName.digitalCertificate, label: 'entities.digitalCertificate', icon: 'cn-icon cn-icon-digital-certificate', tag: 0 },
|
||||
{ name: entityDetailTabsName.securityEvent, label: 'overall.securityEvent', icon: 'cn-icon cn-icon-security-event', tag: 0 }
|
||||
// { name: entityDetailTabsName.performanceEvent, label: 'overall.performanceEvent', icon: 'cn-icon cn-icon-a-PerformanceEvent', tag: 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'domain',
|
||||
config: [
|
||||
{ name: entityDetailTabsName.informationAggregation, label: 'entities.informationAggregation', icon: 'cn-icon cn-icon-information-aggregation', tag: 0 },
|
||||
{ name: entityDetailTabsName.relatedEntity, label: 'entities.relatedEntity', icon: 'cn-icon cn-icon-domain-name-resolution', tag: 0 },
|
||||
{ name: entityDetailTabsName.openPort, label: 'entities.openPort', icon: 'cn-icon cn-icon-open-port', tag: 0 },
|
||||
// { name: entityDetailTabsName.digitalCertificate, label: 'entities.digitalCertificate', icon: 'cn-icon cn-icon-digital-certificate', tag: 0 },
|
||||
{ name: entityDetailTabsName.securityEvent, label: 'overall.securityEvent', icon: 'cn-icon cn-icon-security-event', tag: 0 }
|
||||
// { name: entityDetailTabsName.performanceEvent, label: 'overall.performanceEvent', icon: 'cn-icon cn-icon-a-PerformanceEvent', tag: 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'ip',
|
||||
config: [
|
||||
{ name: entityDetailTabsName.informationAggregation, label: 'entities.informationAggregation', icon: 'cn-icon cn-icon-information-aggregation', tag: 0 },
|
||||
{ name: entityDetailTabsName.relatedEntity, label: 'entities.relatedEntity', icon: 'cn-icon cn-icon-domain-name-resolution', tag: 0 },
|
||||
{ name: entityDetailTabsName.openPort, label: 'entities.openPort', icon: 'cn-icon cn-icon-open-port', tag: 0 },
|
||||
// { name: entityDetailTabsName.digitalCertificate, label: 'entities.digitalCertificate', icon: 'cn-icon cn-icon-digital-certificate', tag: 0 },
|
||||
{ name: entityDetailTabsName.securityEvent, label: 'overall.securityEvent', icon: 'cn-icon cn-icon-security-event', tag: 0 },
|
||||
// { name: entityDetailTabsName.performanceEvent, label: 'overall.performanceEvent', icon: 'cn-icon cn-icon-a-PerformanceEvent', tag: 0 },
|
||||
{ name: entityDetailTabsName.behaviorPattern, label: 'entities.behaviorPattern', icon: 'cn-icon cn-icon-behavior', tag: 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'subscriber',
|
||||
config: [
|
||||
{ name: entityDetailTabsName.deviceInformation, label: 'entities.deviceInformation', icon: 'cn-icon cn-icon-device-info', tag: 0 },
|
||||
{ name: entityDetailTabsName.accountInformation, label: 'entities.accountInformation', icon: 'cn-icon cn-icon-account-info', tag: 0 },
|
||||
{ name: entityDetailTabsName.relatedEntity, label: 'entities.relatedEntity', icon: 'cn-icon cn-icon-domain-name-resolution', tag: 0 },
|
||||
{ name: entityDetailTabsName.securityEvent, label: 'overall.securityEvent', icon: 'cn-icon cn-icon-security-event', tag: 0 }
|
||||
// { name: entityDetailTabsName.performanceEvent, label: 'overall.performanceEvent', icon: 'cn-icon cn-icon-a-PerformanceEvent', tag: 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export const entityDetailRelatedEntitiesShowSize = 100
|
||||
|
||||
export const echartsFontSize = {
|
||||
@@ -247,18 +297,25 @@ export const networkOverviewTabs = [
|
||||
export const metricOptions = [
|
||||
{
|
||||
value: 'Bits/s',
|
||||
label: 'Bits/s'
|
||||
label: 'metric.bps'
|
||||
},
|
||||
{
|
||||
value: 'Packets/s',
|
||||
label: 'Packets/s'
|
||||
label: 'metric.packets'
|
||||
},
|
||||
{
|
||||
value: 'Sessions/s',
|
||||
label: 'Sessions/s'
|
||||
label: 'metric.sessions'
|
||||
}
|
||||
]
|
||||
|
||||
export const metricType = {
|
||||
Bits: 'Bits/s',
|
||||
Packets: 'Packets/s',
|
||||
Sessions: 'Sessions/s'
|
||||
|
||||
}
|
||||
|
||||
export const operationType = {
|
||||
mainMenu: 0, // 菜单
|
||||
secondMenu: 2, // 二级菜单
|
||||
@@ -374,7 +431,23 @@ export const knowledgeBaseCategory = [
|
||||
value: 'user_defined'
|
||||
}
|
||||
]
|
||||
|
||||
export const knowledgeBaseColor = [
|
||||
{
|
||||
label: 'knowledge.info',
|
||||
value: 'rgb(119,131,145)',
|
||||
name: 'info'
|
||||
},
|
||||
{
|
||||
label: 'knowledge.benign',
|
||||
value: 'rgb(116,159,77)',
|
||||
name: 'benign'
|
||||
},
|
||||
{
|
||||
label: 'knowledge.malicious',
|
||||
value: 'rgb(226,97,84)',
|
||||
name: 'malicious'
|
||||
}
|
||||
]
|
||||
export const knowledgeCategoryValue = {
|
||||
webSketch: 'websketch',
|
||||
aiTagging: 'ai_tagging',
|
||||
@@ -442,15 +515,15 @@ export const knowledgeBaseSource = [
|
||||
value: 'cn_psiphon3_ip'
|
||||
},
|
||||
{
|
||||
name: 'IP Tag',
|
||||
name: 'IP',
|
||||
value: 'cn_ip_tag_user_defined'
|
||||
},
|
||||
{
|
||||
name: 'Domain Tag',
|
||||
name: 'Domain',
|
||||
value: 'cn_domain_tag_user_defined'
|
||||
},
|
||||
{
|
||||
name: 'APP Tag',
|
||||
name: 'APP',
|
||||
value: 'cn_app_tag_user_defined'
|
||||
}
|
||||
]
|
||||
@@ -712,7 +785,7 @@ export const networkOverviewTabList = [
|
||||
label: 'network.ips',
|
||||
prop: 'ip',
|
||||
queryCycleTotalProp: 'ips',
|
||||
dillDownProp: ['common_client_ip', 'common_server_ip'], // 下钻表格:查询条件q(dillDownProp里条件之间都是OR的关系),或者queryCondition属性,queryCondition: ['ip = \'$param\' AND side = \'server\''],
|
||||
dillDownProp: ['client_ip', 'server_ip'], // 下钻表格:查询条件q(dillDownProp里条件之间都是OR的关系),或者queryCondition属性,queryCondition: ['ip = \'$param\' AND side = \'server\''],
|
||||
thirdDimensionQueryCondition: [], // 下钻表格:第三级维度查询条件q
|
||||
lineQueryCondition: ['ip = \'$param\''], // 曲线图:查询条件q
|
||||
lineThirdDimensionQueryCondition: ['side = \'server\''], // 曲线图:第三级维度查询条件q
|
||||
@@ -745,10 +818,10 @@ export const networkOverviewTabList = [
|
||||
label: 'network.applications',
|
||||
prop: 'appLabel',
|
||||
queryCycleTotalProp: 'applications',
|
||||
dillDownProp: ['common_app_label'],
|
||||
dillDownProp: ['app'],
|
||||
thirdDimensionQueryCondition: [], // 下钻表格:第三级维度查询条件q
|
||||
lineQueryCondition: ['common_app_label = \'$param\''], // 曲线图:查询条件q
|
||||
lineThirdDimensionQueryCondition: ['notEmpty(common_app_label)'], // 曲线图:第三级维度查询条件q
|
||||
lineQueryCondition: ['app = \'$param\''], // 曲线图:查询条件q
|
||||
lineThirdDimensionQueryCondition: ['notEmpty(app)'], // 曲线图:第三级维度查询条件q
|
||||
checked: true,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.networkOverview
|
||||
@@ -789,10 +862,10 @@ export const networkOverviewTabList = [
|
||||
label: 'network.protocols',
|
||||
prop: 'l7Protocol',
|
||||
queryCycleTotalProp: 'protocols',
|
||||
dillDownProp: ['common_l7_protocol'],
|
||||
dillDownProp: ['l7_protocol'],
|
||||
thirdDimensionQueryCondition: [], // 下钻表格:第三级维度查询条件q
|
||||
lineQueryCondition: ['common_l7_protocol = \'$param\''], // 曲线图:查询条件q
|
||||
lineThirdDimensionQueryCondition: ['notEmpty(common_l7_protocol)'], // 曲线图:第三级维度查询条件q
|
||||
lineQueryCondition: ['l7_protocol = \'$param\''], // 曲线图:查询条件q
|
||||
lineThirdDimensionQueryCondition: ['notEmpty(l7_protocol)'], // 曲线图:第三级维度查询条件q
|
||||
checked: true,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.networkOverview
|
||||
@@ -866,10 +939,10 @@ export const networkOverviewTabList = [
|
||||
label: 'network.protocolPorts',
|
||||
prop: 'protocolPort',
|
||||
queryCycleTotalProp: 'protocolports',
|
||||
dillDownProp: ['common_l7_protocol', 'common_server_port'],
|
||||
dillDownProp: ['l7_protocol', 'server_port'],
|
||||
thirdDimensionQueryCondition: [], // 下钻表格:第三级维度查询条件q
|
||||
lineQueryCondition: ['common_l7_protocol = \'$protocol_param\' AND common_server_port = $port_param'], // 曲线图:查询条件q
|
||||
lineThirdDimensionQueryCondition: ['notEmpty(common_l7_protocol)'], // 曲线图:第三级维度查询条件q
|
||||
lineQueryCondition: ['l7_protocol = \'$protocol_param\' AND server_port = $port_param'], // 曲线图:查询条件q
|
||||
lineThirdDimensionQueryCondition: ['notEmpty(l7_protocol)'], // 曲线图:第三级维度查询条件q
|
||||
checked: false,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.networkOverview
|
||||
@@ -877,7 +950,7 @@ export const networkOverviewTabList = [
|
||||
label: 'network.clientIps',
|
||||
prop: 'clientIp',
|
||||
queryCycleTotalProp: 'clientIps',
|
||||
dillDownProp: ['common_client_ip'],
|
||||
dillDownProp: ['client_ip'],
|
||||
thirdDimensionQueryCondition: [], // 下钻表格:第三级维度查询条件q
|
||||
lineQueryCondition: ['ip = \'$param\' and side = \'client\''], // 曲线图:查询条件q
|
||||
lineThirdDimensionQueryCondition: ['notEmpty(ip) and side = \'client\''], // 曲线图:第三级维度查询条件q
|
||||
@@ -888,7 +961,7 @@ export const networkOverviewTabList = [
|
||||
label: 'network.serverIps',
|
||||
prop: 'serverIp',
|
||||
queryCycleTotalProp: 'serverIps',
|
||||
dillDownProp: ['common_server_ip'],
|
||||
dillDownProp: ['server_ip'],
|
||||
thirdDimensionQueryCondition: [], // 下钻表格:第三级维度查询条件q
|
||||
lineQueryCondition: ['ip = \'$param\' and side = \'server\''], // 曲线图:查询条件q
|
||||
lineThirdDimensionQueryCondition: ['notEmpty(ip) and side = \'server\''], // 曲线图:第三级维度查询条件q
|
||||
@@ -1040,8 +1113,8 @@ export const networkAppPerformanceTabList = [
|
||||
label: 'network.applications',
|
||||
prop: 'appLabel',
|
||||
queryCycleTotalProp: 'applications',
|
||||
dillDownProp: ['common_app_label'],
|
||||
lineQueryCondition: ['common_app_label = \'$param\''],
|
||||
dillDownProp: ['app'],
|
||||
lineQueryCondition: ['app = \'$param\''],
|
||||
checked: true,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.npmOverviewApp
|
||||
@@ -1076,7 +1149,7 @@ export const networkAppPerformanceTabList = [
|
||||
label: 'network.protocols',
|
||||
prop: 'l7Protocol',
|
||||
queryCycleTotalProp: 'protocols',
|
||||
dillDownProp: ['common_l7_protocol'],
|
||||
dillDownProp: ['l7_protocol'],
|
||||
lineQueryCondition: ['l7_protocol = \'$param\''],
|
||||
checked: true,
|
||||
disabled: false,
|
||||
@@ -1139,8 +1212,8 @@ export const networkAppPerformanceTabList = [
|
||||
label: 'network.protocolPorts',
|
||||
prop: 'protocolPort',
|
||||
queryCycleTotalProp: 'protocolports',
|
||||
dillDownProp: ['common_l7_protocol', 'common_server_port'],
|
||||
lineQueryCondition: ['common_l7_protocol = \'$protocol_param\' AND common_server_port = $port_param'],
|
||||
dillDownProp: ['l7_protocol', 'server_port'],
|
||||
lineQueryCondition: ['l7_protocol = \'$protocol_param\' AND server_port = $port_param'],
|
||||
checked: false,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.npmOverviewCommon
|
||||
@@ -1148,7 +1221,7 @@ export const networkAppPerformanceTabList = [
|
||||
label: 'network.clientIps',
|
||||
prop: 'clientIp',
|
||||
queryCycleTotalProp: 'clientIps',
|
||||
dillDownProp: ['common_client_ip'],
|
||||
dillDownProp: ['client_ip'],
|
||||
lineQueryCondition: ['ip = \'$param\' AND side = \'client\''],
|
||||
checked: false,
|
||||
disabled: false,
|
||||
@@ -1157,7 +1230,7 @@ export const networkAppPerformanceTabList = [
|
||||
label: 'network.serverIps',
|
||||
prop: 'serverIp',
|
||||
queryCycleTotalProp: 'serverIps',
|
||||
dillDownProp: ['common_server_ip'],
|
||||
dillDownProp: ['server_ip'],
|
||||
lineQueryCondition: ['ip = \'$param\' AND side = \'server\''],
|
||||
checked: false,
|
||||
disabled: false,
|
||||
@@ -1260,7 +1333,7 @@ export const linkMonitorTabList = [
|
||||
label: 'network.ips', // tab名称对应的il8n
|
||||
prop: 'ip', // 接口返回数据中,tab第一列对应的属性名
|
||||
queryCycleTotalProp: 'ips', // SQL中查询不同纬度的列名称
|
||||
dillDownProp: ['common_client_ip', 'common_server_ip'], // 下钻时,传递的查询条件,即接口的q参数
|
||||
dillDownProp: ['client_ip', 'server_ip'], // 下钻时,传递的查询条件,即接口的q参数
|
||||
checked: true, // 自定义设置中,是否默认选中
|
||||
disabled: false, // 自定义设置中,是否可操作(选中或取消选中)
|
||||
panelId: drillDownPanelTypeMapping.linkMonitor// 下钻后展示的panelId
|
||||
@@ -1284,7 +1357,7 @@ export const linkMonitorTabList = [
|
||||
label: 'network.applications',
|
||||
prop: 'appLabel',
|
||||
queryCycleTotalProp: 'applications',
|
||||
dillDownProp: ['common_app_label'],
|
||||
dillDownProp: ['app'],
|
||||
checked: true,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.linkMonitor
|
||||
@@ -1316,7 +1389,7 @@ export const linkMonitorTabList = [
|
||||
label: 'network.protocols',
|
||||
prop: 'l7Protocol',
|
||||
queryCycleTotalProp: 'protocols',
|
||||
dillDownProp: ['common_l7_protocol'],
|
||||
dillDownProp: ['l7_protocol'],
|
||||
checked: true,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.linkMonitor
|
||||
@@ -1372,7 +1445,7 @@ export const linkMonitorTabList = [
|
||||
label: 'network.protocolPorts',
|
||||
prop: 'protocolPort',
|
||||
queryCycleTotalProp: 'protocolports',
|
||||
dillDownProp: ['common_l7_protocol', 'common_server_port'],
|
||||
dillDownProp: ['l7_protocol', 'server_port'],
|
||||
checked: false,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.linkMonitor
|
||||
@@ -1380,7 +1453,7 @@ export const linkMonitorTabList = [
|
||||
label: 'network.clientIps',
|
||||
prop: 'clientIp',
|
||||
queryCycleTotalProp: 'clientIps',
|
||||
dillDownProp: ['common_client_ip'],
|
||||
dillDownProp: ['client_ip'],
|
||||
checked: false,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.linkMonitor
|
||||
@@ -1388,7 +1461,7 @@ export const linkMonitorTabList = [
|
||||
label: 'network.serverIps',
|
||||
prop: 'serverIp',
|
||||
queryCycleTotalProp: 'serverIps',
|
||||
dillDownProp: ['common_server_ip'],
|
||||
dillDownProp: ['server_ip'],
|
||||
checked: false,
|
||||
disabled: false,
|
||||
panelId: drillDownPanelTypeMapping.linkMonitor
|
||||
@@ -1480,7 +1553,7 @@ export const dnsServiceInsightsTabList = [
|
||||
label: 'dns.dnsServer', // tab名称对应的il8n
|
||||
prop: 'dnsServer', // 接口返回数据中,tab第一列对应的属性名
|
||||
queryCycleTotalProp: 'dnsServer', // SQL中查询不同纬度的列名称
|
||||
dillDownProp: ['common_server_ip'], // 下钻时,传递的查询条件,即接口的q参数
|
||||
dillDownProp: ['server_ip'], // 下钻时,传递的查询条件,即接口的q参数
|
||||
lineQueryCondition: ['server_ip = \'$param\''], // 曲线图:查询条件q
|
||||
checked: true, // 自定义设置中,是否默认选中
|
||||
disabled: false, // 自定义设置中,是否可操作(选中或取消选中)
|
||||
@@ -1808,6 +1881,89 @@ export const performanceMetricMapping = {
|
||||
'high dns response time': 'DNS Response Latency'
|
||||
}
|
||||
|
||||
export const pluginBasicInfo = [
|
||||
{
|
||||
id: 109,
|
||||
name: 'Psiphon3 VPN',
|
||||
type: ['IP'],
|
||||
schedule: 'plugin.hourly',
|
||||
desc: 'knowledgeBase.desc.psiphon3',
|
||||
iconUrl: 'images/knowledge-base-logo/psiphon3-vpn.png'
|
||||
},
|
||||
{
|
||||
id: 111,
|
||||
name: 'HotSpot VPN',
|
||||
type: ['IP'],
|
||||
schedule: 'plugin.hourly',
|
||||
desc: 'knowledgeBase.desc.hotSpot',
|
||||
iconUrl: 'images/knowledge-base-logo/hotspot-vpn.png'
|
||||
},
|
||||
{
|
||||
id: 112,
|
||||
name: 'IpVanish VPN',
|
||||
type: ['IP', 'Domain'],
|
||||
schedule: 'plugin.hourly',
|
||||
desc: 'knowledgeBase.desc.ipVanishDomain',
|
||||
iconUrl: 'images/knowledge-base-logo/ip-vanish.png'
|
||||
},
|
||||
{
|
||||
id: 113,
|
||||
name: 'Ivacy VPN',
|
||||
type: ['IP', 'Domain'],
|
||||
schedule: 'plugin.daily',
|
||||
desc: 'knowledgeBase.desc.ivacyDomain',
|
||||
iconUrl: 'images/knowledge-base-logo/ivacy.png'
|
||||
},
|
||||
{
|
||||
id: 114,
|
||||
name: 'Proton VPN',
|
||||
type: ['IP'],
|
||||
schedule: 'plugin.hourly',
|
||||
desc: 'knowledgeBase.desc.protonvpn',
|
||||
iconUrl: 'images/knowledge-base-logo/protonvpn.png'
|
||||
},
|
||||
{
|
||||
id: 115,
|
||||
name: 'CyberGhost VPN',
|
||||
type: ['IP', 'Domain'],
|
||||
schedule: 'plugin.daily',
|
||||
desc: 'knowledgeBase.desc.cyberGhostDomain',
|
||||
iconUrl: 'images/knowledge-base-logo/cyber-ghost.png'
|
||||
},
|
||||
{
|
||||
id: 116,
|
||||
name: 'Windscribe VPN',
|
||||
type: ['IP', 'Domain'],
|
||||
schedule: 'plugin.hourly',
|
||||
desc: 'knowledgeBase.desc.windscribeDomain',
|
||||
iconUrl: 'images/knowledge-base-logo/windscribe.png'
|
||||
},
|
||||
{
|
||||
id: 117,
|
||||
name: 'Turbo VPN',
|
||||
type: ['IP'],
|
||||
schedule: 'plugin.hourly',
|
||||
desc: 'knowledgeBase.desc.turboIp',
|
||||
iconUrl: 'images/knowledge-base-logo/turbo.png'
|
||||
},
|
||||
{
|
||||
id: 118,
|
||||
name: 'Gecko VPN',
|
||||
type: ['IP'],
|
||||
schedule: 'plugin.hourly',
|
||||
desc: 'knowledgeBase.desc.geckoIp',
|
||||
iconUrl: 'images/knowledge-base-logo/gecko.png'
|
||||
},
|
||||
{
|
||||
id: 119,
|
||||
name: 'Vpnunlimited',
|
||||
type: ['IP'],
|
||||
schedule: 'plugin.hourly',
|
||||
desc: 'knowledgeBase.desc.vpnunlimited',
|
||||
iconUrl: 'images/knowledge-base-logo/vpnunlimited.png'
|
||||
}
|
||||
]
|
||||
|
||||
export const builtInKnowledgeBaseBasicInfo = [
|
||||
{
|
||||
knowledgeId: 10,
|
||||
@@ -1887,6 +2043,8 @@ export const chartColor5 = ['#E26154', '#E7B34E', '#88AF65']
|
||||
|
||||
export const chartColor6 = ['#E99F67', '#D9C74B']
|
||||
export const chartColorForBehaviorPattern = ['#7acac7', '#b4d38e', '#fee9b9', '#fec396', '#fb9b79', '#e3799c', '#edd5f5', '#868cac', '#a4adde', '#64b4e6']
|
||||
export const chartColorForSubscriberTopApp = ['#A7C186', '#AFCC8A', '#BEDCAC', '#80BEA5', '#7BBBBC', '#8CB9C8', '#E6BF88', '#E6D99B', '#E0D1B0', '#ECAE95']
|
||||
|
||||
export const iso36112 = {
|
||||
[storageKey.iso36112Capital]: 'data/countriesWithCapital',
|
||||
[storageKey.iso36112WorldLow]: 'worldChinaLow',
|
||||
@@ -2374,20 +2532,20 @@ export const reg = {
|
||||
|
||||
export const report = {
|
||||
timeRuleList: [
|
||||
{ name: 'today', value: 'today' },
|
||||
{ name: 'yesterday', value: 'yesterday' },
|
||||
{ name: 'this', value: 'this' },
|
||||
{ name: 'last', value: 'last' },
|
||||
{ name: 'previous', value: 'previous' },
|
||||
{ name: 'customize', value: 'customize' }
|
||||
{ name: 'overall.today', value: 'today' },
|
||||
{ name: 'overall.yesterday', value: 'yesterday' },
|
||||
{ name: 'overall.this', value: 'this' },
|
||||
{ name: 'overall.last', value: 'last' },
|
||||
{ name: 'overall.previous', value: 'previous' },
|
||||
{ name: 'overall.customize', value: 'customize' }
|
||||
],
|
||||
timeUnitList: [
|
||||
{ name: 'minute', value: 'minute' },
|
||||
{ name: 'hour', value: 'hour' },
|
||||
{ name: 'day', value: 'day' },
|
||||
{ name: 'week', value: 'week' },
|
||||
{ name: 'month', value: 'month' },
|
||||
{ name: 'year', value: 'year' }
|
||||
{ name: 'overall.minute', value: 'minute' },
|
||||
{ name: 'overall.hour', value: 'hour' },
|
||||
{ name: 'overall.day', value: 'day' },
|
||||
{ name: 'overall.week', value: 'week' },
|
||||
{ name: 'overall.month', value: 'month' },
|
||||
{ name: 'overall.year', value: 'year' }
|
||||
],
|
||||
scheduleTypeList: [
|
||||
{ name: 'report.daily', value: 'day' },
|
||||
@@ -2440,6 +2598,10 @@ export const entityDetailTags = {
|
||||
{
|
||||
name: 'nodeType',
|
||||
type: 'negative'
|
||||
},
|
||||
{
|
||||
name: 'vpnServiceName',
|
||||
type: 'negative'
|
||||
}
|
||||
],
|
||||
ip: [
|
||||
@@ -2462,6 +2624,10 @@ export const entityDetailTags = {
|
||||
{
|
||||
name: 'dnsServerRole',
|
||||
type: 'normal'
|
||||
},
|
||||
{
|
||||
name: 'vpnServiceName',
|
||||
type: 'negative'
|
||||
}
|
||||
],
|
||||
app: [
|
||||
|
||||
@@ -13,11 +13,17 @@ export function getMillisecond (time) {
|
||||
ms = window.$dayJs.tz(new Date(time)).valueOf()
|
||||
} else if (_.isNumber(time)) {
|
||||
const timeStr = _.toString(time)
|
||||
const difference = timeStr.length - 13
|
||||
/* const difference = timeStr.length - 13
|
||||
if (difference >= 0) {
|
||||
ms = window.$dayJs.tz(new Date(Number(timeStr.slice(0, 13)))).valueOf()
|
||||
} else {
|
||||
ms = window.$dayJs.tz(new Date(Math.floor(time * (10 ** (0 - difference))))).valueOf()
|
||||
} */
|
||||
// 判断9位和10位数为秒,12位和13位为毫秒。其他位数不做处理
|
||||
if (timeStr.length === 9 || timeStr.length === 10) {
|
||||
ms = window.$dayJs.tz(new Date(Number(time * 1000))).valueOf()
|
||||
} else {
|
||||
ms = window.$dayJs.tz(new Date(Number(time))).valueOf()
|
||||
}
|
||||
} else if (_.isString(time)) {
|
||||
try {
|
||||
|
||||
@@ -31,7 +31,7 @@ axios.interceptors.request.use(config => {
|
||||
err => Promise.reject(err)
|
||||
)
|
||||
const accountErrorCode = [518003, 518004, 518005, 518006, 518007, 518008] // 账号锁定等
|
||||
const licenceErrorCode = [711001]
|
||||
const licenceErrorCode = [715001]
|
||||
|
||||
// 若get请求的url中带问号,则将url上的参数截取,改为对象形式传参
|
||||
axios.interceptors.request.use(
|
||||
|
||||
@@ -222,26 +222,31 @@ export const dataForNpmEventsHeader = {
|
||||
chartData: [
|
||||
{
|
||||
eventSeverity: 'overall.critical',
|
||||
class: 'critical',
|
||||
count: '-',
|
||||
index: 0
|
||||
},
|
||||
{
|
||||
eventSeverity: 'overall.high',
|
||||
class: 'high',
|
||||
count: '-',
|
||||
index: 1
|
||||
},
|
||||
{
|
||||
eventSeverity: 'overall.medium',
|
||||
class: 'medium',
|
||||
count: '-',
|
||||
index: 2
|
||||
},
|
||||
{
|
||||
eventSeverity: 'overall.low',
|
||||
class: 'low',
|
||||
count: '-',
|
||||
index: 3
|
||||
},
|
||||
{
|
||||
eventSeverity: 'overall.info',
|
||||
class: 'info',
|
||||
count: '-',
|
||||
index: 4
|
||||
}
|
||||
@@ -338,7 +343,7 @@ const securityEvent = [
|
||||
{
|
||||
name: 'event_type',
|
||||
type: 'string',
|
||||
label: 'event_type',
|
||||
label: 'eventType',
|
||||
doc: {
|
||||
constraints: {
|
||||
operator_functions: '=,in,like'
|
||||
@@ -348,7 +353,7 @@ const securityEvent = [
|
||||
{
|
||||
name: 'event_name',
|
||||
type: 'string',
|
||||
label: 'event_name',
|
||||
label: 'eventName',
|
||||
doc: {
|
||||
constraints: {
|
||||
operator_functions: '=,in,like'
|
||||
@@ -368,7 +373,7 @@ const securityEvent = [
|
||||
{
|
||||
name: 'offender_ip',
|
||||
type: 'string',
|
||||
label: 'offender Ip',
|
||||
label: 'offenderIp',
|
||||
doc: {
|
||||
constraints: {
|
||||
operator_functions: '=,in,like'
|
||||
@@ -378,7 +383,7 @@ const securityEvent = [
|
||||
{
|
||||
name: 'victim_ip',
|
||||
type: 'string',
|
||||
label: 'victim Ip',
|
||||
label: 'victimIp',
|
||||
doc: {
|
||||
constraints: {
|
||||
operator_functions: '=,in,like'
|
||||
@@ -404,6 +409,49 @@ const securityEvent = [
|
||||
operator_functions: '=,in,like'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'status',
|
||||
type: 'string',
|
||||
doc: {
|
||||
constraints: {
|
||||
operator_functions: '=,in'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export const enumerateData = [
|
||||
{
|
||||
name: 'status',
|
||||
data: [
|
||||
{ code: _this.$t('detections.ended'), code1: 'detections.ended', value: 1 },
|
||||
{ code: _this.$t('detections.active'), code1: 'detections.active', value: 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'eventType',
|
||||
data: [
|
||||
{ code: 'Initial Access', value: 'Initial Access' },
|
||||
{ code: 'Command and Control', value: 'Command and Control' },
|
||||
{ code: 'Credential Access', value: 'Credential Access' },
|
||||
{ code: 'Lateral Movement', value: 'Lateral Movement' },
|
||||
{ code: 'Collection', value: 'Collection' },
|
||||
{ code: 'Impact', value: 'Impact' },
|
||||
{ code: 'Anonymity', value: 'Anonymity' },
|
||||
{ code: 'Regulatory Risk', value: 'Regulatory Risk' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'severity',
|
||||
data: [
|
||||
{ code: _this.$t('overall.critical'), code1: 'overall.critical', value: 'critical' },
|
||||
{ code: _this.$t('overall.high'), code1: 'overall.high', value: 'high' },
|
||||
{ code: _this.$t('overall.medium'), code1: 'overall.medium', value: 'medium' },
|
||||
{ code: _this.$t('overall.low'), code1: 'overall.low', value: 'low' },
|
||||
{ code: _this.$t('overall.info'), code1: 'overall.info', value: 'info' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -417,6 +465,12 @@ if (schema) {
|
||||
securityEventMetadata = JSON.parse(schema).securityEventMetadata.searchColumns
|
||||
}
|
||||
}
|
||||
securityEventMetadata.forEach(item => {
|
||||
const obj = enumerateData.find(d => d.name === item.label)
|
||||
if (obj) {
|
||||
item.doc.data = obj.data
|
||||
}
|
||||
})
|
||||
export const schemaDetectionSecurity = securityEventMetadata
|
||||
|
||||
export const operatorList = ['=', '!=', /* '>', '<', '>=', '<=', */'IN', 'NOT IN', 'LIKE', 'NOT LIKE']
|
||||
|
||||
151
src/utils/timeQueryApi.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import vue from '@/main.js'
|
||||
// import Moment from "moment/moment";
|
||||
// import {transformToUTCTime} from './TimeZone.js'
|
||||
|
||||
//默认Schema 延时15s ,时间粒度 1s
|
||||
const defaultSchema = {
|
||||
"doc": {
|
||||
"measurements": {
|
||||
"granularity": 1,
|
||||
"ingestion_delay": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let schemaDict=null;
|
||||
function getSchemaFromLocal(schemaType) {
|
||||
let schemaData=null
|
||||
// if(schemaDict?.[schemaType]){
|
||||
// schemaData=schemaDict
|
||||
// }else{
|
||||
schemaData = localStorage.getItem('TSGSchema')
|
||||
// }
|
||||
if (!schemaData) {
|
||||
return null;
|
||||
}
|
||||
const storeData = JSON.parse(schemaData)[schemaType];
|
||||
// 如果根据key没有找到数据,直接返回空
|
||||
if (!storeData) {
|
||||
return null;
|
||||
}
|
||||
const parsedData = storeData;
|
||||
const currentTimestamp = new Date().getTime();
|
||||
|
||||
// 将当前的时间戳和保存在storage中的timestamp进行比较
|
||||
// 如果时间差小于等于过期时间说明没有过期,直接返回数据
|
||||
// 否则,说明数据已经过期,将storage中的key清除
|
||||
if (currentTimestamp - parsedData.timestamp <= parsedData.expire) {
|
||||
return parsedData.value ? JSON.parse(parsedData.value) : parsedData.value
|
||||
} else {
|
||||
setDashboardSchema(schemaType,'')
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向localStorage中添加字段
|
||||
* @param {*} schemaType 保存数据的key
|
||||
* @param {*} value 保存的数据
|
||||
* @param {*} expire 过期时间,默认为1分钟
|
||||
*/
|
||||
function setDashboardSchema(schemaType, value, expire = 60000) {
|
||||
let schemaData = localStorage.getItem('TSGSchema');
|
||||
if (!schemaData) {
|
||||
localStorage.setItem('TSGSchema', '{}')
|
||||
}
|
||||
let TSGSchema = JSON.parse(localStorage.getItem('TSGSchema'))
|
||||
TSGSchema[schemaType] = {
|
||||
value: value,
|
||||
expire: expire,
|
||||
timestamp: new Date().getTime()
|
||||
}
|
||||
const stringfiedData = JSON.stringify(TSGSchema);
|
||||
localStorage.setItem('TSGSchema', stringfiedData);
|
||||
schemaDict=TSGSchema;
|
||||
}
|
||||
|
||||
// 获取过期时间毫秒数
|
||||
function getExpireTime(data){
|
||||
let expireDate=data.expireDate
|
||||
if(!expireDate){
|
||||
return 24 * 60 * 60 * 1000
|
||||
}
|
||||
let expireTime=new Date().getTime()-expireDate
|
||||
return expireTime
|
||||
}
|
||||
|
||||
|
||||
function getSchemaFromRemote(schemaType) {
|
||||
return vue.$get(`/interface/gateway/api/galaxy/v1/metadata/schema/${schemaType}`).then(res => {
|
||||
if (res.status === 200) {
|
||||
let expireTime=getExpireTime(res.data)
|
||||
setDashboardSchema(schemaType, JSON.stringify(res.data), expireTime)
|
||||
return res.data
|
||||
}
|
||||
return defaultSchema
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
return defaultSchema
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// async 延时时间
|
||||
// 转换时间 start end schmaType 相对时间 绝对时间
|
||||
// 时间粒度
|
||||
|
||||
//获取Schema数据,存在于local 直接取,不存在 直接请求,存本地
|
||||
async function getSchemaInfo(schemaType = '') {
|
||||
var schemaInfo = getSchemaFromLocal(schemaType)
|
||||
if (!schemaInfo) {
|
||||
schemaInfo = await getSchemaFromRemote(schemaType)
|
||||
}
|
||||
return schemaInfo
|
||||
}
|
||||
|
||||
/*
|
||||
* 这里对 时间的延时
|
||||
* 时间范围做处理
|
||||
* 默认 不做 UTC 转换处理
|
||||
*
|
||||
* granularity 单位必须是S 秒
|
||||
* */
|
||||
async function getTimeQueryParams({start, end, schemaType = '', granularity = 1, toUtc = false, delay = false}) {
|
||||
let schemaData = await getSchemaInfo(schemaType);
|
||||
|
||||
const schema_ingestion_delay = schemaData?.doc?.measurements?.ingestion_delay || 0;
|
||||
const schema_granularity = schemaData?.doc?.measurements?.granularity || 1;
|
||||
|
||||
//这里需要考虑 传入的是UTC 时间,Moment 可以正常处理吗
|
||||
var delayTime = delay ? schema_ingestion_delay : 0;
|
||||
// let startDate = Moment(start).subtract(delayTime, 'seconds')
|
||||
// let endDate = Moment(end).subtract(delayTime, 'seconds')
|
||||
let startDate = '1'
|
||||
let endDate = '2'
|
||||
|
||||
var startStr = startDate.format('YYYY-MM-DD HH:mm:ss')
|
||||
var endStr = endDate.format('YYYY-MM-DD HH:mm:ss')
|
||||
|
||||
//前端计算时间粒度,不能比Schema 支持的最小时间粒度 小
|
||||
var alignmentPeriod = schema_granularity > granularity ? schema_granularity : granularity
|
||||
|
||||
//UTC 转换
|
||||
if (toUtc) {
|
||||
// startStr = transformToUTCTime(startStr)
|
||||
// endStr = transformToUTCTime(endStr)
|
||||
startStr = 'transformToUTCTime(startStr)'
|
||||
endStr = 'transformToUTCTime(endStr)'
|
||||
}
|
||||
return {
|
||||
start: startStr,
|
||||
end: endStr,
|
||||
granularity: 'PT'+schema_granularity+'S',
|
||||
alignmentPeriod: 'PT'+alignmentPeriod+'S',
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getTimeQueryParams,
|
||||
getSchemaInfo
|
||||
}
|
||||
export {getSchemaFromLocal, getSchemaInfo, setDashboardSchema, getSchemaFromRemote, getTimeQueryParams};
|
||||
@@ -1,13 +1,26 @@
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import i18n from '@/i18n'
|
||||
import _ from 'lodash'
|
||||
import { storageKey, iso36112, topDomain, echartsFontSize, dbGeoDataTableName, networkTable, dbDrilldownTableConfig, ZH, EN } from '@/utils/constants'
|
||||
import {
|
||||
storageKey,
|
||||
iso36112,
|
||||
topDomain,
|
||||
echartsFontSize,
|
||||
dbGeoDataTableName,
|
||||
networkTable,
|
||||
dbDrilldownTableConfig,
|
||||
ZH,
|
||||
EN,
|
||||
securityLevel,
|
||||
entityDetailTags, entityDefaultColor, tagValueLabelMapping
|
||||
} from '@/utils/constants'
|
||||
import { getIso36112JsonData, getDictList } from '@/utils/api'
|
||||
import { format } from 'echarts'
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import indexedDBUtils from '@/indexedDB'
|
||||
import { columnType } from '@/components/advancedSearch/meta/meta'
|
||||
import { dateFormatByAppearance, getMillisecond } from '@/utils/date-util'
|
||||
|
||||
export const tableSort = {
|
||||
// 是否需要排序
|
||||
@@ -1399,7 +1412,7 @@ export const myHighLight = {
|
||||
// 此处不用el.className.indexOf('high-light-block')判断,是因为block可能会有多个,有一个满足所有的都会渲染
|
||||
if (newText.indexOf('highlight__block') > -1) {
|
||||
el.style.cssText = el.style.cssText + 'background: #FEECC2;'
|
||||
// 此处是相关app、相关ip、相关domain弹窗获取不到dom的草错
|
||||
// 此处是相关app、相关ip、相关domain弹窗获取不到dom的操作
|
||||
let dom
|
||||
if (document.getElementById('showRelatedApp')) {
|
||||
dom = document.getElementById('showRelatedApp')
|
||||
@@ -1447,3 +1460,108 @@ export const changeTimeByDate = (date) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换严重程度severity的国际化值
|
||||
*/
|
||||
export const changeI18nOfSeverity = (severity) => {
|
||||
if (severity) {
|
||||
const obj = securityLevel.find(d => d.value === severity)
|
||||
if (obj) {
|
||||
return i18n.global.t(obj.label)
|
||||
} else {
|
||||
return severity
|
||||
}
|
||||
} else {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将时间戳转为YYYY-MM-DD hh:mm:ss格式
|
||||
* @param timestamp
|
||||
* @returns {string|*}
|
||||
*/
|
||||
export const changeTimestampToTime = (timestamp) => {
|
||||
if (timestamp) {
|
||||
if (_.isNumber(Number(timestamp))) {
|
||||
return dateFormatByAppearance(getMillisecond(Number(timestamp)))
|
||||
} else {
|
||||
return '-'
|
||||
}
|
||||
} else {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范实体的tag展示
|
||||
*/
|
||||
export const formatTags = (data, type, list) => {
|
||||
Object.keys(data).forEach(k => {
|
||||
if (k !== 'userDefinedTags' && data[k]) {
|
||||
if (_.isArray(data[k])) {
|
||||
data[k].forEach(k3 => {
|
||||
const find = entityDetailTags[type].find(t => t.name === 'vpnServiceName')
|
||||
if (find) {
|
||||
list.push({ key: 'vpnServiceName', value: tagValueHandler(k3.vpnServiceName), type: find.type })
|
||||
} else {
|
||||
list.push({ key: 'vpnServiceName', value: tagValueHandler(k3.vpnServiceName), color: k3.knowledgeBase ? (k3.knowledgeBase.color || entityDefaultColor) : entityDefaultColor })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Object.keys(data[k]).forEach(k2 => {
|
||||
const find = entityDetailTags[type].find(t => t.name === k2)
|
||||
if (find) {
|
||||
list.push({ key: k2, value: tagValueHandler(data[k][k2]), type: find.type })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范实体详情的tag展示
|
||||
*/
|
||||
export const formatTagsOfDetails = (data, type, list) => {
|
||||
data.forEach(r => {
|
||||
Object.keys(r).forEach(k => {
|
||||
const aggregation = {
|
||||
createTime: r[k].createTime,
|
||||
updateTime: r[k].updateTime,
|
||||
status: r[k].isValid,
|
||||
intelligenceContent: []
|
||||
}
|
||||
if (k === 'userDefinedTag') {
|
||||
aggregation.intelligenceContent.push({ key: k, value: r[k].tagValue, type: 'normal' })
|
||||
} else {
|
||||
if (_.isArray(r[k])) {
|
||||
r[k].forEach(k3 => {
|
||||
const find = entityDetailTags[type].find(t => t.name === k3.vpnServiceName)
|
||||
if (find) {
|
||||
aggregation.intelligenceContent.push({ key: 'vpnServiceName', value: tagValueHandler(k3.vpnServiceName), type: find.type })
|
||||
} else {
|
||||
aggregation.intelligenceContent.push({ key: 'vpnServiceName', value: tagValueHandler(k3.vpnServiceName), color: k3.knowledgeBase ? (k3.knowledgeBase.color || entityDefaultColor) : entityDefaultColor })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Object.keys(r[k]).forEach(k2 => {
|
||||
const find = entityDetailTags[type].find(t => t.name === k2)
|
||||
if (find) {
|
||||
aggregation.intelligenceContent.push({ key: k2, value: tagValueHandler(r[k][k2]), type: find.type })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (aggregation.intelligenceContent.length > 0) {
|
||||
list.push(aggregation)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const tagValueHandler = (value) => {
|
||||
const find = tagValueLabelMapping.find(t => t.value === value)
|
||||
return find ? find.name : value
|
||||
}
|
||||
|
||||
@@ -140,8 +140,8 @@ export function getUnitType (column) {
|
||||
}
|
||||
|
||||
/* 单位转换,返回转换后的[value, unit],type=time时若value<1ms,返回<1ms,type=percent时若value<0.01%,返回<0.01% */
|
||||
export function valueToRangeValue (value, unitType) {
|
||||
const values = unitConvert(value, unitType)
|
||||
export function valueToRangeValue (value, unitType,sourceUnit, targetUnit, dot) {
|
||||
const values = unitConvert(value, unitType,sourceUnit, targetUnit, dot)
|
||||
if (values[0] === '-') {
|
||||
return values
|
||||
}
|
||||
|
||||
159
src/views/administration/License.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="license" :class="from">
|
||||
<div class="license-form">
|
||||
<el-form ref="licenseForm" :model="licenseObject" label-position="left" label-width="134px" size="small">
|
||||
<el-form-item :label="`${$t('license.type')}:`" prop="type">
|
||||
{{licenseObject.type}}
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('license.organization')}:`" prop="organization">
|
||||
<div class="">{{licenseObject.organization}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('license.supportId')}:`" prop="supportID">
|
||||
<div class="">{{licenseObject.supportID}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('license.dateIssued')}:`" prop="dateIssued">
|
||||
<div class="">{{licenseObject.dateIssued}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('license.dateExpires')}:`" prop="dateExpires">
|
||||
<div class="">{{licenseObject.dateExpires}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="`${$t('license.licenseFile')}:`" >
|
||||
<div class="license-file">
|
||||
<button style="position: relative;" class="license__btn margin-r-20" @click.prevent="downloadFile">
|
||||
<i class="cn-icon-download1 cn-icon margin-r-6"></i><span>{{$t('license.download')}}</span>
|
||||
</button>
|
||||
<el-upload :action="`${baseUrl}sys/license/upload`"
|
||||
ref="licenseUpload"
|
||||
id="licenseUpload"
|
||||
:headers="uploadHeaders"
|
||||
: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>{{$t('license.upload')}}</span>
|
||||
</button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { api } from '@/utils/api'
|
||||
import { storageKey } from '@/utils/constants'
|
||||
import axios from 'axios'
|
||||
import { ref } from 'vue'
|
||||
import { dateFormat } from '@/utils/date-util'
|
||||
|
||||
export default {
|
||||
name: 'License',
|
||||
data () {
|
||||
return {
|
||||
url: api.license,
|
||||
downloadC2vUrl: api.downloadLicenseC2v,
|
||||
licenseObject: { // 对象
|
||||
type: '',
|
||||
organization: '',
|
||||
supportID: '',
|
||||
dateIssued: '',
|
||||
dateExpires: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.initData()
|
||||
},
|
||||
methods: {
|
||||
initData () {
|
||||
axios.get(this.url, { pageSize: -1 }).then(response => {
|
||||
if (response.status === 200) {
|
||||
this.licenseObject = response.data.data.license
|
||||
this.licenseObject.dateExpires = dateFormat(new Date(this.licenseObject.hasp.feature.license.exp_date * 1000))
|
||||
this.licenseObject.dateIssued = dateFormat(new Date(this.licenseObject.hasp.production_date * 1000))
|
||||
}
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.$message.error(this.errorMsgHandler(e))
|
||||
})
|
||||
},
|
||||
|
||||
fileChange (file, fileList) {
|
||||
if (file.status !== 'ready') return
|
||||
if (!_.endsWith(file.name, '.xml')) {
|
||||
this.fileList = []
|
||||
this.$message.error(this.$t('validate.fileTypeLimit', { types: this.fileTypeLimit }))
|
||||
} else {
|
||||
this.fileList = fileList.slice(-1)
|
||||
this.$refs.licenseUpload.submit()
|
||||
}
|
||||
},
|
||||
uploadSuccess (response) {
|
||||
this.initData()
|
||||
this.$message.success(this.$t('tip.success'))
|
||||
},
|
||||
uploadError (error) {
|
||||
let errorMsg
|
||||
if (error.message) {
|
||||
errorMsg = JSON.parse(error.message).message
|
||||
} else {
|
||||
errorMsg = 'error'
|
||||
}
|
||||
this.$message.error(this.$t('tip.uploadFailed', { msg: errorMsg }))
|
||||
},
|
||||
downloadFile () {
|
||||
axios.get(this.downloadC2vUrl, { responseType: 'blob' }).then(res => {
|
||||
let fileName = ''
|
||||
if(res.headers['content-disposition']) {
|
||||
fileName = res.headers['content-disposition'].split(';')[1].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)
|
||||
})
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
return {
|
||||
baseUrl: BASE_CONFIG.baseUrl,
|
||||
apiVersion: BASE_CONFIG.apiVersion,
|
||||
uploadHeaders: {
|
||||
'Cn-Authorization': localStorage.getItem(storageKey.token)
|
||||
},
|
||||
fileTypeLimit: '.xml',
|
||||
fileList: ref([])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -247,9 +247,8 @@ import {
|
||||
chartActiveIpTableOrderOptions,
|
||||
chartPieTableTopOptions,
|
||||
eventSeverity,
|
||||
chartTableColumnMapping, panelTypeAndRouteMapping
|
||||
chartTableColumnMapping
|
||||
} from '@/utils/constants'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
export default {
|
||||
name: 'ChartHeader',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tooLongFormatter } from '../tools'
|
||||
import { chartColor, storageKey } from '@/utils/constants'
|
||||
|
||||
const legendFontSize = localStorage.getItem(storageKey.echartLegendFontSize)
|
||||
const labelFontSize = localStorage.getItem(storageKey.echartLabelFontSize)
|
||||
// const labelFontSize = localStorage.getItem(storageKey.echartLabelFontSize)
|
||||
export const pieWithTable = {
|
||||
tooltip: {
|
||||
appendToBody: true
|
||||
|
||||
@@ -496,6 +496,40 @@ export function stackedLineTooltipFormatter (params) {
|
||||
str += '</div>'
|
||||
return str
|
||||
}
|
||||
// subscriber详情页的app页签
|
||||
export function stackedLineWithLegendTooltipFormatter (params) {
|
||||
// 堆叠图tooltip倒叙操作
|
||||
params = params.reverse()
|
||||
|
||||
let str = '<div>'
|
||||
params.forEach((item, i) => {
|
||||
const tData = item.data[0]
|
||||
if (i === 0) {
|
||||
str += '<div style="margin-bottom: 5px">'
|
||||
str += dateFormatByAppearance(tData)
|
||||
str += '</div>'
|
||||
}
|
||||
})
|
||||
str += '<div class="cn-chart-body">'
|
||||
str += '<div class="cn-chart-tooltip">'
|
||||
params.forEach((item, i) => {
|
||||
str += '<span class="cn-chart-tooltip-box">'
|
||||
str += `<span style="display:inline-block;margin-right:4px;border-radius:2px;width:10px;height:10px;background-color:${item.borderColor};"></span>`
|
||||
str += `<span class="cn-chart-tooltip-content">${item.seriesName.split('(')[0]}</span>`
|
||||
str += '</span>'
|
||||
})
|
||||
str += '</div>'
|
||||
str += '<div class="cn-chart-tooltip">'
|
||||
params.forEach((item, i) => {
|
||||
str += `<span class="cn-chart-tooltip-value cn-chart-tooltip__color">
|
||||
${valueToRangeValue(item.data[1], item.value[2]).join(' ')}
|
||||
</span>`
|
||||
})
|
||||
str += '</div>'
|
||||
str += '</div>'
|
||||
str += '</div>'
|
||||
return str
|
||||
}
|
||||
export function appStackedLineTooltipFormatter (params) {
|
||||
let str = '<div>'
|
||||
params.forEach((item, i) => {
|
||||
|
||||
@@ -166,18 +166,42 @@
|
||||
:entity="entity"
|
||||
@toggleLoading="toggleLoading"
|
||||
></entity-detail-basic-info>
|
||||
<entity-detail-subscriber-kpi
|
||||
v-else-if="chart.type === typeMapping.entityDetail.subscriberKpi"
|
||||
:chart="chart"
|
||||
:entity="entity"
|
||||
@toggleLoading="toggleLoading"
|
||||
></entity-detail-subscriber-kpi>
|
||||
<entity-detail-subscriber-top-app
|
||||
v-else-if="chart.type === typeMapping.entityDetail.subscriberTopApp"
|
||||
:chart="chart"
|
||||
:entity="entity"
|
||||
@toggleLoading="toggleLoading"
|
||||
></entity-detail-subscriber-top-app>
|
||||
<entity-detail-line
|
||||
v-else-if="chart.type === typeMapping.entityDetail.line"
|
||||
:chart="chart"
|
||||
:entity="entity"
|
||||
@toggleLoading="toggleLoading"
|
||||
></entity-detail-line>
|
||||
<entity-detail-subscriber-line
|
||||
v-else-if="chart.type === typeMapping.entityDetail.subscriberLine"
|
||||
:chart="chart"
|
||||
:entity="entity"
|
||||
@toggleLoading="toggleLoading"
|
||||
></entity-detail-subscriber-line>
|
||||
<entity-detail-tabs-chart
|
||||
v-else-if="chart.type === typeMapping.entityDetail.tabsChart"
|
||||
:chart="chart"
|
||||
:entity="entity"
|
||||
@toggleLoading="toggleLoading"
|
||||
></entity-detail-tabs-chart>
|
||||
<entity-detail-map
|
||||
v-else-if="chart.type === typeMapping.entityDetail.map"
|
||||
:chart="chart"
|
||||
:entity="entity"
|
||||
@toggleLoading="toggleLoading"
|
||||
></entity-detail-map>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -210,7 +234,11 @@ import DnsRecentEvents from '@/views/charts2/charts/dnsInsight/DnsRecentEvents'
|
||||
import DnsTrafficLine from '@/views/charts2/charts/dnsInsight/DnsTrafficLine'
|
||||
import EntityDetailBasicInfo from '@/views/charts2/charts/entityDetail/EntityDetailBasicInfo'
|
||||
import EntityDetailLine from '@/views/charts2/charts/entityDetail/EntityDetailLine'
|
||||
import EntityDetailSubscriberLine from '@/views/charts2/charts/entityDetail/EntityDetailSubscriberLine'
|
||||
import EntityDetailSubscriberKpi from '@/views/charts2/charts/entityDetail/EntityDetailSubscriberKpi'
|
||||
import EntityDetailSubscriberTopApp from '@/views/charts2/charts/entityDetail/EntityDetailSubscriberTopApp'
|
||||
import EntityDetailTabsChart from '@/views/charts2/charts/entityDetail/EntityDetailTabs'
|
||||
import EntityDetailMap from '@/views/charts2/charts/entityDetail/EntityDetailMap'
|
||||
|
||||
import { getNowTime } from '@/utils/date-util'
|
||||
import { ref } from 'vue'
|
||||
@@ -245,8 +273,12 @@ export default {
|
||||
DnsRecentEvents,
|
||||
DnsTrafficLine,
|
||||
EntityDetailBasicInfo,
|
||||
EntityDetailSubscriberKpi,
|
||||
EntityDetailSubscriberTopApp,
|
||||
EntityDetailLine,
|
||||
EntityDetailTabsChart
|
||||
EntityDetailSubscriberLine,
|
||||
EntityDetailTabsChart,
|
||||
EntityDetailMap
|
||||
},
|
||||
props: {
|
||||
chart: Object,
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<template #prefix>
|
||||
<span class="select-prefix">{{$t('detections.metric')}}:</span>
|
||||
</template>
|
||||
<el-option v-for="item in metricOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
<el-option v-for="item in metricOptions" :key="item.value" :label="$t(item.label)" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
<div class="panel__time">
|
||||
<date-time-range
|
||||
@@ -124,7 +124,7 @@ export default {
|
||||
watch: {
|
||||
timeFilter: {
|
||||
handler () {
|
||||
if (this.$route.path === '/panel/networkAppPerformance') {
|
||||
if (this.$route.path === '/panel/networkAppPerformance' || this.$route.path === '/panel/linkMonitor') {
|
||||
this.$store.commit('resetScoreBase')
|
||||
this.queryScoreBase()
|
||||
if (this.lineQueryCondition || this.networkOverviewBeforeTab) {
|
||||
@@ -408,9 +408,18 @@ export default {
|
||||
},
|
||||
metricChange (value) {
|
||||
const { query } = this.$route
|
||||
const newUrl = urlParamsHandler(window.location.href, query, {
|
||||
metric: value
|
||||
})
|
||||
const rangeParam = query.range
|
||||
let params = { metric: value }
|
||||
// 优先级:url > config.js > 默认值。
|
||||
const dateRangeValue = rangeParam ? parseInt(rangeParam) : (DEFAULT_TIME_FILTER_RANGE.dashboard || 60)
|
||||
if (dateRangeValue !== -1) {
|
||||
const { startTime, endTime } = getNowTime(dateRangeValue)
|
||||
// this.timeFilter = { startTime: getSecond(startTime), endTime: getSecond(endTime), dateRangeValue: dateRangeValue }
|
||||
this.$store.commit('setTimeRangeArray', [this.timeFilter.startTime, this.timeFilter.endTime])
|
||||
this.$store.commit('setTimeRangeFlag', dateRangeValue.value)
|
||||
params = { metric: value, startTime: getSecond(startTime), endTime: getSecond(endTime), range: dateRangeValue }
|
||||
}
|
||||
const newUrl = urlParamsHandler(window.location.href, query, params)
|
||||
overwriteUrl(newUrl)
|
||||
},
|
||||
// 动态查询评分基准
|
||||
|
||||
@@ -43,10 +43,17 @@ export default {
|
||||
break
|
||||
case entityDetailTabsName.securityEvent:
|
||||
dataRangeValue = DEFAULT_TIME_FILTER_RANGE.entity.securityEvent
|
||||
params.pageSize = -1
|
||||
break
|
||||
case entityDetailTabsName.openPort:
|
||||
dataRangeValue = DEFAULT_TIME_FILTER_RANGE.entity.openPort
|
||||
break
|
||||
case entityDetailTabsName.deviceInformation:
|
||||
dataRangeValue = DEFAULT_TIME_FILTER_RANGE.entity.deviceInformation
|
||||
break
|
||||
case entityDetailTabsName.accountInformation:
|
||||
dataRangeValue = DEFAULT_TIME_FILTER_RANGE.entity.accountInformation
|
||||
break
|
||||
case entityDetailTabsName.informationAggregation:
|
||||
dataRangeValue = DEFAULT_TIME_FILTER_RANGE.entity.informationAggregation
|
||||
break
|
||||
|
||||
@@ -34,7 +34,11 @@ export const typeMapping = {
|
||||
},
|
||||
entityDetail: {
|
||||
basicInfo: 712,
|
||||
subscriberKpi: 714,
|
||||
subscriberTopApp: 715,
|
||||
line: 107,
|
||||
tabsChart: 713
|
||||
subscriberLine: 108,
|
||||
tabsChart: 713,
|
||||
map: 3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:key="index"
|
||||
@mouseenter="mouseenter(item)"
|
||||
@mouseleave="mouseleave(item)"
|
||||
@click="activeChange(item, index)">
|
||||
@click="activeChange(item, index,true)">
|
||||
<div class="line-value-tabs-name">
|
||||
<div :class="item.class"></div>
|
||||
<div class="tabs-name">{{$t(item.name)}}</div>
|
||||
@@ -176,6 +176,7 @@ export default {
|
||||
|
||||
axios.get(url, { params }).then(res => {
|
||||
if (res.status === 200) {
|
||||
this.chartDateObject = res.data.data.result
|
||||
this.showError = false
|
||||
this.isNoData = res.data.data.result.length === 0
|
||||
if (!active) {
|
||||
@@ -201,6 +202,23 @@ export default {
|
||||
this.toggleLoading(false)
|
||||
})
|
||||
},
|
||||
initTabData (val, show, active) {
|
||||
if (!val) {
|
||||
val = this.lineMetric
|
||||
}
|
||||
this.toggleLoading(true)
|
||||
try {
|
||||
if (this.chartDateObject.length > 0) {
|
||||
this.initData(this.chartDateObject, val, active, show)
|
||||
} else {
|
||||
this.init(val, show, active)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.toggleLoading(false)
|
||||
}
|
||||
},
|
||||
echartsInit (echartsData, show) {
|
||||
this.$nextTick(() => {
|
||||
if (this.lineTab) {
|
||||
@@ -325,12 +343,16 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
activeChange (item, index) {
|
||||
activeChange (item, index, isClick) {
|
||||
if (this.isNoData) return
|
||||
this.lineTab = item.class
|
||||
this.legendSelectChange(item, index, 'active')
|
||||
this.showMarkLine = !item.invertTab
|
||||
if (isClick) {
|
||||
this.initTabData(this.lineMetric, this.showMarkLine, 'active')
|
||||
} else {
|
||||
this.init(this.lineMetric, this.showMarkLine, 'active')
|
||||
}
|
||||
},
|
||||
mouseenter (item) {
|
||||
if (this.isNoData) return
|
||||
@@ -573,6 +595,7 @@ export default {
|
||||
}
|
||||
this.chartOption = null
|
||||
this.unitConvert = null
|
||||
this.chartDateObject = []
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@show="analysisPopSwitch(true)"
|
||||
@hide="analysisPopSwitch(false)"
|
||||
>
|
||||
<template #reference>
|
||||
<template #reference v-if="entity.entityType !== entityType.subscriber">
|
||||
<div class="analysis-btn" :class="{'analysis-btn--active': analysisPopShow}"><i class="cn-icon cn-icon-analysis"></i>{{$t('overall.analysis')}}</div>
|
||||
</template>
|
||||
<div class="analysis-entry">
|
||||
@@ -47,7 +47,13 @@
|
||||
<i :class="card.icon"></i>
|
||||
<div class="detail-card__text">
|
||||
<div class="detail-card__label">{{card.label}}:</div>
|
||||
<div class="detail-card__value" :title="card.value">{{card.value || '-'}}</div>
|
||||
<div v-if="card.name==='reputationLevel'"
|
||||
class="detail-card__value row__tag row__tag__level"
|
||||
:title="card.value"
|
||||
:style="`background-color:${riskLevelColor1[card.level]};text-align: center;color: ${card.value !== '-' ? '#fff' : ''}`">
|
||||
{{card.value || '-'}}
|
||||
</div>
|
||||
<div v-else class="detail-card__value" :title="card.value">{{card.value || '-'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,12 +65,11 @@ import ChartError from '@/components/common/Error'
|
||||
import {
|
||||
drillDownPanelTypeMapping,
|
||||
entityType,
|
||||
entityDetailTags,
|
||||
tagValueLabelMapping,
|
||||
riskLevelMapping,
|
||||
entityDefaultColor
|
||||
entityDefaultColor,
|
||||
riskLevelColor1
|
||||
} from '@/utils/constants'
|
||||
import { selectElementText, copySelectionText, getTagColor } from '@/utils/tools'
|
||||
import { selectElementText, copySelectionText, getTagColor, formatTags } from '@/utils/tools'
|
||||
import { ref } from 'vue'
|
||||
import i18n from '@/i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -87,7 +92,8 @@ export default {
|
||||
// type: positive 正面的,绿色;normal 灰色;negative 负面的,红色
|
||||
levelTwoTags: [{ value: '安全', type: 'positive' }, { value: '工具', type: 'normal' }, { value: '恶意IP', type: 'negative' }],
|
||||
analysisPopShow: false,
|
||||
hideTagArea: false
|
||||
hideTagArea: false,
|
||||
riskLevelColor1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -107,6 +113,10 @@ export default {
|
||||
entityTypeName = 'APP'
|
||||
break
|
||||
}
|
||||
case ('subscriber'): {
|
||||
entityTypeName = this.$t('entity.subscriberId')
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -115,17 +125,19 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getTagColor,
|
||||
tagValueHandler (value) {
|
||||
const find = tagValueLabelMapping.find(t => t.value === value)
|
||||
return find ? find.name : value
|
||||
},
|
||||
getData () {
|
||||
this.toggleLoading(true)
|
||||
this.showError = false
|
||||
this.levelTwoTags = []
|
||||
const tagRequest = axios.get(`${api.entity.tags}/${this.entity.entityType}?resource=${this.entity.entityName}`)
|
||||
const basicInfoRequest = axios.get(`${api.entity.basicInfo}/${this.entity.entityType}?resource=${this.entity.entityName}`)
|
||||
Promise.all([tagRequest, basicInfoRequest]).then(response => {
|
||||
let requestArray = []
|
||||
if (this.entity.entityType === entityType.subscriber) {
|
||||
requestArray = [null, basicInfoRequest]
|
||||
} else {
|
||||
const tagRequest = axios.get(`${api.entity.tags}/${this.entity.entityType}?resource=${this.entity.entityName}`)
|
||||
requestArray = [tagRequest, basicInfoRequest]
|
||||
}
|
||||
Promise.all(requestArray).then(response => {
|
||||
const tagData = response[0]
|
||||
let tagError = ''
|
||||
const basicInfoData = response[1]
|
||||
@@ -133,27 +145,18 @@ export default {
|
||||
if (tagData) {
|
||||
const res = tagData.data
|
||||
if (tagData.status === 200) {
|
||||
Object.keys(res.data).forEach(k => {
|
||||
if (k !== 'userDefinedTags' && res.data[k]) {
|
||||
Object.keys(res.data[k]).forEach(k2 => {
|
||||
const find = entityDetailTags[this.entity.entityType].find(t => t.name === k2)
|
||||
if (find) {
|
||||
this.levelTwoTags.push({ key: k2, value: this.tagValueHandler(res.data[k][k2]), type: find.type })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
formatTags(res.data, this.entity.entityType, this.levelTwoTags)
|
||||
if (_.isArray(res.data.userDefinedTags)) {
|
||||
this.levelTwoTags = _.concat(this.levelTwoTags, res.data.userDefinedTags.map(tag => ({ value: tag.tagValue, color: tag.knowledgeBase ? tag.knowledgeBase.color : entityDefaultColor })))
|
||||
}
|
||||
this.hideTagArea = _.isEmpty(this.levelTwoTags)
|
||||
this.$nextTick(() => {
|
||||
this.emitter.emit('entity-detail-hide-tag-area')
|
||||
})
|
||||
} else {
|
||||
tagError = this.errorMsgHandler(res)
|
||||
}
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.emitter.emit('entity-detail-hide-tag-area')
|
||||
})
|
||||
if (basicInfoData) {
|
||||
const res = basicInfoData.data
|
||||
if (basicInfoData.status === 200) {
|
||||
@@ -175,7 +178,8 @@ export default {
|
||||
if (res.data.category) {
|
||||
this.detailCards.find(c => c.name === 'categoryName').value = res.data.category.categoryName
|
||||
this.detailCards.find(c => c.name === 'categoryGroup').value = res.data.category.categoryGroup
|
||||
this.detailCards.find(c => c.name === 'reputationLevel').value = res.data.category.reputationLevel
|
||||
this.detailCards.find(c => c.name === 'reputationLevel').value = this.reputationLevel(res.data.category.reputationLevel)
|
||||
this.detailCards.find(c => c.name === 'reputationLevel').level = res.data.category.reputationLevel
|
||||
}
|
||||
if (res.data.whois) {
|
||||
this.detailCards.find(c => c.name === 'expireDate').value = dateFormatByAppearance(res.data.whois.expireDate)
|
||||
@@ -199,6 +203,13 @@ export default {
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'subscriber': {
|
||||
this.detailCards.find(c => c.name === 'iphoneNumber').value = res.data.phone_number
|
||||
this.detailCards.find(c => c.name === 'imei').value = res.data.imei
|
||||
this.detailCards.find(c => c.name === 'imsi').value = res.data.imsi
|
||||
this.detailCards.find(c => c.name === 'apn').value = res.data.apn
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
basicInfoError = this.errorMsgHandler(res)
|
||||
@@ -252,6 +263,14 @@ export default {
|
||||
},
|
||||
jump (url) {
|
||||
window.open(url, '_blank')
|
||||
},
|
||||
reputationLevel (level) {
|
||||
if (level) {
|
||||
const m = riskLevelMapping.find(mapping => mapping.name === level)
|
||||
return (m && this.$t(m.label)) || level
|
||||
} else {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
@@ -295,7 +314,7 @@ export default {
|
||||
networkOverviewAnalysisItemCopy.url = resolvePath({
|
||||
path: networkOverviewAnalysisItemCopy.path,
|
||||
query: {
|
||||
queryCondition: `common_client_ip='${props.entity.entityName}' OR common_server_ip='${props.entity.entityName}'`,
|
||||
queryCondition: `client_ip='${props.entity.entityName}' OR server_ip='${props.entity.entityName}'`,
|
||||
lineQueryCondition: `ip='${props.entity.entityName}'`,
|
||||
panelName: props.entity.entityName,
|
||||
thirdMenu: 'network.ips',
|
||||
@@ -323,7 +342,7 @@ export default {
|
||||
dnsAnalysisItemCopy.url = resolvePath({
|
||||
path: dnsAnalysisItem.path,
|
||||
query: {
|
||||
queryCondition: `common_server_ip='${props.entity.entityName}'`,
|
||||
queryCondition: `server_ip='${props.entity.entityName}'`,
|
||||
lineQueryCondition: `server_ip='${props.entity.entityName}'`,
|
||||
panelName: props.entity.entityName,
|
||||
thirdMenu: 'dns.dnsServer',
|
||||
@@ -387,7 +406,7 @@ export default {
|
||||
break
|
||||
}
|
||||
case 'app': {
|
||||
const queryCondition = `common_app_label='${props.entity.entityName}'`
|
||||
const queryCondition = `app='${props.entity.entityName}'`
|
||||
const networkOverviewAnalysisItemCopy = _.cloneDeep(networkOverviewAnalysisItem)
|
||||
networkOverviewAnalysisItemCopy.url = resolvePath({
|
||||
path: networkOverviewAnalysisItemCopy.path,
|
||||
@@ -439,7 +458,7 @@ export default {
|
||||
case 'domain': {
|
||||
detailCards.value = _.concat(detailCards.value,
|
||||
{ icon: 'cn-icon cn-icon-category2', name: 'categoryName', label: i18n.global.t('entities.category'), value: '' },
|
||||
{ icon: 'cn-icon cn-icon-sub-type', name: 'categoryGroup', label: i18n.global.t('entities.group'), value: '' },
|
||||
{ icon: 'cn-icon cn-icon-sub-type', name: 'categoryGroup', label: i18n.global.t('entities.domainDetail.categoryGroup'), value: '' },
|
||||
{ icon: 'cn-icon cn-icon-credit-rating', name: 'reputationLevel', label: i18n.global.t('entities.creditLevel2'), value: '' },
|
||||
{ icon: 'cn-icon cn-icon-expire-date', name: 'expireDate', label: i18n.global.t('entities.expirationDate2'), value: '' },
|
||||
{ icon: 'cn-icon cn-icon-registrar', name: 'registrarName', label: i18n.global.t('entities.registrar'), value: '' },
|
||||
@@ -460,6 +479,15 @@ export default {
|
||||
{ icon: 'cn-icon cn-icon-app-full-name', name: 'appLongname', label: i18n.global.t('overall.appFullName'), value: '' },
|
||||
{ icon: 'cn-icon cn-icon-description', name: 'appDescription', label: i18n.global.t('config.dataSource.description'), value: '' }
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'subscriber': {
|
||||
detailCards.value = _.concat(detailCards.value,
|
||||
{ icon: 'cn-icon cn-icon-shoujihaoma', name: 'iphoneNumber', label: i18n.global.t('entities.iphoneNumber'), value: '' },
|
||||
{ icon: 'cn-icon cn-icon-IMEI', name: 'imei', label: i18n.global.t('entities.imei'), value: '' },
|
||||
{ icon: 'cn-icon cn-icon-IMSI', name: 'imsi', label: i18n.global.t('entities.imsi'), value: '' },
|
||||
{ icon: 'cn-icon cn-icon-APN', name: 'apn', label: i18n.global.t('entities.apn'), value: '' }
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="line-select-metric">
|
||||
<span class="select-prefix line-margin-right">Metric:</span>
|
||||
<span class="select-prefix line-margin-right">{{$t('network.metric')}}:</span>
|
||||
<el-select
|
||||
size="mini"
|
||||
v-model="metric"
|
||||
@@ -55,7 +55,7 @@
|
||||
:popper-append-to-body="false"
|
||||
@change="metricChange"
|
||||
>
|
||||
<el-option v-for="item in metricOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
<el-option v-for="item in metricOptions" :key="item.value" :label="$t(item.label)" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,7 +71,9 @@
|
||||
:popper-append-to-body="false"
|
||||
@change="referenceSelectChange"
|
||||
>
|
||||
<el-option v-for="item in options2" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
<el-option :key="options2[0].value" :label="$t(options2[0].label)" :value="options2[0].value"></el-option>
|
||||
<el-option :key="options2[1].value" :label="$t(options2[1].label[0], options2[1].label[1])" :value="options2[1].value"></el-option>
|
||||
<el-option :key="options2[2].value" :label="$t(options2[2].label)" :value="options2[2].value"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,7 +92,7 @@ import chartMixin from '@/views/charts2/chart-mixin'
|
||||
import * as echarts from 'echarts'
|
||||
import { stackedLineChartOption } from '@/views/charts2/charts/options/echartOption'
|
||||
import unitConvert, { valueToRangeValue } from '@/utils/unit-convert'
|
||||
import { unitTypes, chartColor3, chartColor4 } from '@/utils/constants.js'
|
||||
import { unitTypes, chartColor3, chartColor4, metricType, metricOptions } from '@/utils/constants.js'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { stackedLineTooltipFormatter } from '@/views/charts/charts/tools'
|
||||
import _ from 'lodash'
|
||||
@@ -102,7 +104,6 @@ import { useRoute } from 'vue-router'
|
||||
import { getLineType, getMarkLineByLineRefer, overwriteUrl, urlParamsHandler } from '@/utils/tools'
|
||||
import ChartError from '@/components/common/Error'
|
||||
import { dataForNetworkOverviewLine } from '@/utils/static-data'
|
||||
import { metricOptions } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'EntityDetailLine',
|
||||
@@ -167,7 +168,8 @@ export default {
|
||||
brushHistory: [],
|
||||
showError: false,
|
||||
errorMsg: '',
|
||||
metricOptions
|
||||
metricOptions,
|
||||
isMetricChange: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -182,12 +184,15 @@ export default {
|
||||
},
|
||||
timeFilter: {
|
||||
handler () {
|
||||
if (!this.isMetricChange) {
|
||||
if (this.lineTab) {
|
||||
this.init(this.metric, this.showMarkLine, 'active')
|
||||
} else {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
this.isMetricChange = false
|
||||
}
|
||||
},
|
||||
metric (n) {
|
||||
this.handleActiveBar()
|
||||
@@ -223,8 +228,8 @@ export default {
|
||||
this.toggleLoading(true)
|
||||
axios.get(`${api.entity.throughput}/${this.entity.entityType}`, { params: params }).then(response => {
|
||||
const res = response.data
|
||||
|
||||
if (response.status === 200) {
|
||||
this.chartDateObject = res.data.result
|
||||
this.isNoData = res.data.result.length === 0
|
||||
this.showError = false
|
||||
if (!active) {
|
||||
@@ -246,6 +251,21 @@ export default {
|
||||
this.toggleLoading(false)
|
||||
})
|
||||
},
|
||||
initTabData (val, show, active, n) {
|
||||
const newVal = val ? _.clone(val) : this.metric
|
||||
this.toggleLoading(true)
|
||||
try {
|
||||
if (this.chartDateObject.length > 0) {
|
||||
this.initData(this.chartDateObject, newVal, active, show, n)
|
||||
} else {
|
||||
this.init(val, show, active, n)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.toggleLoading(false)
|
||||
}
|
||||
},
|
||||
echartsInit (echartsData, show) {
|
||||
// echarts内容在单元测试时不执行
|
||||
if (!this.isUnitTesting) {
|
||||
@@ -306,6 +326,14 @@ export default {
|
||||
label: {
|
||||
formatter (params) {
|
||||
const arr = unitConvert(params.value, unitTypes.number).join('')
|
||||
const referIndex = _this.options2.findIndex(o => o.value === _this.lineRefer)
|
||||
if (referIndex > -1) {
|
||||
if (referIndex === 1) {
|
||||
return _this.$t(_this.options2[1].label[0], _this.options2[1].label[1]) + '(' + arr + echartsData[0].unitType + ')'
|
||||
} else {
|
||||
return _this.$t(_this.options2[referIndex].label) + '(' + arr + echartsData[0].unitType + ')'
|
||||
}
|
||||
}
|
||||
return _this.lineRefer + '(' + arr + echartsData[0].unitType + ')'
|
||||
},
|
||||
position: 'insideStartTop',
|
||||
@@ -382,7 +410,12 @@ export default {
|
||||
this.legendSelectChange(item, index, 'active')
|
||||
this.showMarkLine = !item.invertTab
|
||||
}
|
||||
|
||||
if (isClick) {
|
||||
this.initTabData(this.metric, this.showMarkLine, 'active')
|
||||
} else {
|
||||
this.init(this.metric, this.showMarkLine, 'active')
|
||||
}
|
||||
},
|
||||
mouseenter (item) {
|
||||
if (this.isNoData) return
|
||||
@@ -532,7 +565,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (val === 'Sessions/s') {
|
||||
if (val === metricType.Sessions) {
|
||||
const tabs = _.cloneDeep(this.tabsTemplate)
|
||||
lineData.forEach((d, i) => {
|
||||
tabs[i].data = d.values
|
||||
@@ -542,7 +575,8 @@ export default {
|
||||
if (i !== 0) {
|
||||
e.show = false
|
||||
}
|
||||
e.unitType = 'sessions/s'
|
||||
const metric = metricOptions.find(item => item.value === metricType.Sessions)
|
||||
e.unitType = metric ? this.$t(metric.label) : ''
|
||||
e.invertTab = false
|
||||
this.lineTab = 'total'
|
||||
this.legendSelectChange(e, 0)
|
||||
@@ -553,7 +587,11 @@ export default {
|
||||
this.echartsInit(this.tabs, true)
|
||||
})
|
||||
} else {
|
||||
const unit = val === 'Bits/s' ? 'bps' : 'packets/s'
|
||||
let metric = metricOptions.find(item => item.value === val)
|
||||
if (!metric) {
|
||||
metric = metricOptions.find(item => item.value === metricType.Packets)
|
||||
}
|
||||
const unit = metric ? this.$t(metric.label) : ''
|
||||
this.legendInit(lineData, active, show, unit, n)
|
||||
}
|
||||
},
|
||||
@@ -640,10 +678,20 @@ export default {
|
||||
this.errorMsg = this.errorMsgHandler(e)
|
||||
},
|
||||
metricChange (value) {
|
||||
this.isMetricChange = true
|
||||
const { query } = this.$route
|
||||
const newUrl = urlParamsHandler(window.location.href, query, {
|
||||
metric: value
|
||||
})
|
||||
const rangeParam = query.range
|
||||
let params = { metric: value }
|
||||
// 优先级:url > config.js > 默认值。
|
||||
const dateRangeValue = rangeParam ? parseInt(rangeParam) : (DEFAULT_TIME_FILTER_RANGE.entity.trafficLine || 60)
|
||||
if (dateRangeValue !== -1) {
|
||||
const { startTime, endTime } = getNowTime(dateRangeValue)
|
||||
this.timeFilter = { startTime: getSecond(startTime), endTime: getSecond(endTime), dateRangeValue: dateRangeValue }
|
||||
this.$store.commit('setTimeRangeArray', [this.timeFilter.startTime, this.timeFilter.endTime])
|
||||
this.$store.commit('setTimeRangeFlag', dateRangeValue.value)
|
||||
params = { metric: value, startTime: this.timeFilter.startTime, endTime: this.timeFilter.endTime, range: dateRangeValue }
|
||||
}
|
||||
const newUrl = urlParamsHandler(window.location.href, query, params)
|
||||
overwriteUrl(newUrl)
|
||||
},
|
||||
reload (startTime, endTime, dateRangeValue) {
|
||||
@@ -679,7 +727,7 @@ export default {
|
||||
this.chartOption = null
|
||||
const self = this
|
||||
self.timer = setTimeout(() => {
|
||||
if (self.lineTab && self.metric !== 'Sessions/s') {
|
||||
if (self.lineTab && self.metric !== metricType.Sessions) {
|
||||
const data = self.tabsTemplate.find(t => t.class === self.lineTab)
|
||||
self.activeChange(data, data.positioning)
|
||||
} else {
|
||||
@@ -702,6 +750,7 @@ export default {
|
||||
// 检测时发现该方法占用较大内存,且未被释放
|
||||
this.unitConvert = null
|
||||
this.valueToRangeValue = null
|
||||
this.chartDateObject = []
|
||||
myChart = null
|
||||
}
|
||||
}
|
||||
|
||||
344
src/views/charts2/charts/entityDetail/EntityDetailMap.vue
Normal file
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<div class="subscriber-map">
|
||||
<div class="subscriber-map-header">
|
||||
<div class="subscriber-map-title">{{$t('subscriber.locationTrack')}}</div>
|
||||
<date-time-range
|
||||
class="entity-detail-date-time-range"
|
||||
:start-time="timeFilter.startTime"
|
||||
:end-time="timeFilter.endTime"
|
||||
:date-range="timeFilter.dateRangeValue"
|
||||
ref="dateTimeRange"
|
||||
@change="reload"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="subscriber-map-body">
|
||||
<chart-no-data v-if="isNoData" test-id="noData"></chart-no-data>
|
||||
<chart-error v-if="showError" :content="errorMsg" />
|
||||
<div id="subscriberMap" class="subscriber-map"></div>
|
||||
</div>
|
||||
|
||||
<!-- 地图point悬浮框 -->
|
||||
<div class="subscriber-map-point-tooltip" v-if="tooltip.showTooltip" :style="{'left': `${tooltip.x}px`, 'top': `${tooltip.y}px`}">
|
||||
<div class="subscriber-map-point-tooltip__time">{{dateFormatByAppearance(currentPoint.stat_time)}}</div>
|
||||
<div class="subscriber-map-point-tooltip__coordinates">
|
||||
<div class="subscriber-map-point-tooltip__coordinate">
|
||||
<div class="coordinate__label">{{$t('overall.longitude')}}</div>
|
||||
<div class="coordinate__value">{{currentPoint.longitude}}</div>
|
||||
</div>
|
||||
<div class="subscriber-map-point-tooltip__coordinate">
|
||||
<div class="coordinate__label">{{$t('overall.latitude')}}</div>
|
||||
<div class="coordinate__value">{{currentPoint.latitude}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import chartMixin from '@/views/charts2/chart-mixin'
|
||||
import ChartError from '@/components/common/Error'
|
||||
import ChartNoData from '@/views/charts/charts/ChartNoData'
|
||||
import maplibregl from 'maplibre-gl'
|
||||
import mapStyle from './mapStyle'
|
||||
import axios from 'axios'
|
||||
import { api } from '@/utils/api'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import _ from 'lodash'
|
||||
import { ref } from 'vue'
|
||||
import { getNowTime, getSecond } from '@/utils/date-util'
|
||||
|
||||
export default {
|
||||
name: 'EntityDetailMap',
|
||||
mixins: [chartMixin],
|
||||
components: {
|
||||
ChartError,
|
||||
ChartNoData
|
||||
},
|
||||
mounted () {
|
||||
this.initMap()
|
||||
},
|
||||
unmounted () {
|
||||
this.myChart && this.myChart.remove()
|
||||
this.myChart = null
|
||||
},
|
||||
methods: {
|
||||
async initMap () {
|
||||
const _this = this
|
||||
const map = new maplibregl.Map({
|
||||
container: 'subscriberMap',
|
||||
style: mapStyle,
|
||||
center: this.center,
|
||||
maxZoom: this.maxZoom,
|
||||
minZoom: this.minZoom,
|
||||
zoom: 7,
|
||||
transformRequest: function (url, resourceType) {
|
||||
if (resourceType === 'Tile' && url.indexOf('https://api.maptiler.com/tiles/v3') > -1) {
|
||||
const urlParams = url.split('.pbf')[0].split('/')
|
||||
const z = urlParams[urlParams.length - 3]
|
||||
const x = urlParams[urlParams.length - 2]
|
||||
const y = urlParams[urlParams.length - 1]
|
||||
const newUrl1 = `${window.location.protocol}//${window.location.host}/tiles/${z}/${x}/${y}.pbf`
|
||||
return {
|
||||
url: newUrl1,
|
||||
credentials: 'include',
|
||||
method: 'GET'
|
||||
}
|
||||
}
|
||||
if (resourceType === 'SpriteJSON') {
|
||||
return {
|
||||
url: `${window.location.protocol}//${window.location.host}/tiles/sprite.json`,
|
||||
credentials: 'include',
|
||||
method: 'GET'
|
||||
}
|
||||
}
|
||||
if (resourceType === 'SpriteImage') {
|
||||
return {
|
||||
url: `${window.location.protocol}//${window.location.host}/tiles/sprite.png`,
|
||||
credentials: 'include',
|
||||
method: 'GET'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
this.queryData().then(res => {
|
||||
map.on('load', () => {
|
||||
this.showError = false
|
||||
const result = _.get(res, 'data.data.result', []).sort((a, b) => a.stat_time - b.stat_time)
|
||||
const route = this.generateRouteGEOJSON(result)
|
||||
const points = this.generatePointsGEOJSON(result)
|
||||
if (result.length > 0) {
|
||||
map.jumpTo({ center: this.computeMapCenter(result) })
|
||||
map.zoomTo(this.computeMapZoom(result))
|
||||
}
|
||||
map.loadImage(
|
||||
`${window.location.protocol}//${window.location.host}/images/entity-detail/track-point.png`,
|
||||
(error, image) => {
|
||||
if (error) throw error
|
||||
map.addImage('trace-point', image)
|
||||
map.addSource('points', {
|
||||
type: 'geojson',
|
||||
data: points
|
||||
})
|
||||
map.addLayer(this.generatePointsLayer())
|
||||
}
|
||||
)
|
||||
map.addSource('route', {
|
||||
type: 'geojson',
|
||||
lineMetrics: true,
|
||||
data: route
|
||||
})
|
||||
map.addLayer(this.generateRouteLayer())
|
||||
|
||||
this.myChart = map
|
||||
|
||||
// point的鼠标事件
|
||||
map.on('mouseenter', 'points', () => {
|
||||
_this.tooltip.showTooltip = true
|
||||
})
|
||||
map.on('mouseleave', 'points', () => {
|
||||
_this.tooltip.showTooltip = false
|
||||
})
|
||||
map.on('mousemove', 'points', ({ point, originalEvent, features }) => {
|
||||
_this.tooltip.x = originalEvent.clientX + 10
|
||||
_this.tooltip.y = originalEvent.clientY - 95
|
||||
_this.currentPoint = { ...features[0].properties }
|
||||
})
|
||||
})
|
||||
}).catch(e => {
|
||||
this.showError = true
|
||||
console.error(e)
|
||||
this.errorMsg = this.errorMsgHandler(e)
|
||||
}).finally(() => {
|
||||
this.toggleLoading(false)
|
||||
})
|
||||
},
|
||||
queryData () {
|
||||
/*return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
status: 200,
|
||||
data: {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
stat_time: 1701259999,
|
||||
subscriber_id: '883849',
|
||||
subscriber_longitude: 116.29,
|
||||
subscriber_latitude: 39.8
|
||||
},
|
||||
{
|
||||
stat_time: 1701269999,
|
||||
subscriber_id: '883849',
|
||||
subscriber_longitude: 116.34,
|
||||
subscriber_latitude: 39.85
|
||||
},
|
||||
{
|
||||
stat_time: 1701279999,
|
||||
subscriber_id: '883849',
|
||||
subscriber_longitude: 116.29,
|
||||
subscriber_latitude: 39.9
|
||||
},
|
||||
{
|
||||
stat_time: 1701299999,
|
||||
subscriber_id: '883849',
|
||||
subscriber_longitude: 116.39,
|
||||
subscriber_latitude: 39.9
|
||||
},
|
||||
{
|
||||
stat_time: 1701289999,
|
||||
subscriber_id: '883849',
|
||||
subscriber_longitude: 116.3,
|
||||
subscriber_latitude: 39.97
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
})*/
|
||||
const params = {
|
||||
resource: this.entity.entityName,
|
||||
startTime: getSecond(this.timeFilter.startTime),
|
||||
endTime: getSecond(this.timeFilter.endTime)
|
||||
}
|
||||
return axios.get(`${api.entity.locationTrack}`, { params })
|
||||
},
|
||||
generateRouteGEOJSON (result) {
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: result.map(r => [r.subscriber_longitude, r.subscriber_latitude])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
generatePointsGEOJSON (result) {
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: result.map((r, index) => {
|
||||
return {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [r.subscriber_longitude, r.subscriber_latitude]
|
||||
},
|
||||
properties: {
|
||||
index: index + 1,
|
||||
stat_time: r.stat_time,
|
||||
longitude: r.subscriber_longitude,
|
||||
latitude: r.subscriber_latitude
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
generateRouteLayer () {
|
||||
return {
|
||||
id: 'route',
|
||||
source: 'route',
|
||||
type: 'line',
|
||||
paint: {
|
||||
'line-color': '#E26154',
|
||||
'line-width': 4,
|
||||
'line-gradient': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['line-progress'],
|
||||
0,
|
||||
'#F4B32F',
|
||||
1,
|
||||
'#E26154'
|
||||
]
|
||||
},
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round'
|
||||
}
|
||||
}
|
||||
},
|
||||
generatePointsLayer () {
|
||||
return {
|
||||
id: 'points',
|
||||
type: 'symbol',
|
||||
source: 'points',
|
||||
layout: {
|
||||
'icon-image': 'trace-point',
|
||||
'icon-size': 0.7,
|
||||
'icon-offset': [0, -20],
|
||||
'text-field': ['get', 'index'],
|
||||
'text-font': ['Noto Sans Bold'],
|
||||
'text-offset': [0, -1.1],
|
||||
'text-size': 13
|
||||
},
|
||||
paint: {
|
||||
'text-color': '#FFF'
|
||||
}
|
||||
}
|
||||
},
|
||||
computeMapCenter (result) {
|
||||
const longitude = result.map(r => r.subscriber_longitude)
|
||||
const latitude = result.map(r => r.subscriber_latitude)
|
||||
return [
|
||||
_.floor((_.max(longitude) + _.min(longitude)) / 2, 6),
|
||||
_.floor((_.max(latitude) + _.min(latitude)) / 2, 6)
|
||||
]
|
||||
},
|
||||
computeMapZoom (result) {
|
||||
const longitude = result.map(r => r.subscriber_longitude)
|
||||
const latitude = result.map(r => r.subscriber_latitude)
|
||||
const longitudeRange = _.max(longitude) - _.min(longitude)
|
||||
const latitudeRange = _.max(latitude) - _.min(latitude)
|
||||
const max = _.max([this.minZoom, 7 - Math.log2(_.max([longitudeRange, latitudeRange]))])
|
||||
return _.min([max, this.maxZoom])
|
||||
},
|
||||
reload (startTime, endTime, dateRangeValue) {
|
||||
this.toggleLoading(true)
|
||||
this.timeFilter = { startTime: getSecond(startTime), endTime: getSecond(endTime), dateRangeValue: dateRangeValue }
|
||||
this.queryData().then(res => {
|
||||
this.showError = false
|
||||
const result = _.get(res, 'data.data.result', []).sort((a, b) => a.stat_time - b.stat_time)
|
||||
console.info(result)
|
||||
const route = this.generateRouteGEOJSON(result)
|
||||
const points = this.generatePointsGEOJSON(result)
|
||||
this.myChart.getSource('route').setData(route)
|
||||
this.myChart.getSource('points').setData(points)
|
||||
if (result.length > 0) {
|
||||
this.myChart.jumpTo({ center: this.computeMapCenter(result) })
|
||||
this.myChart.zoomTo(this.computeMapZoom(result))
|
||||
}
|
||||
}).catch(e => {
|
||||
this.showError = true
|
||||
console.error(e)
|
||||
this.errorMsg = this.errorMsgHandler(e)
|
||||
}).finally(() => {
|
||||
this.toggleLoading(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
const dateRangeValue = DEFAULT_TIME_FILTER_RANGE.entity.subscriberMap || 60
|
||||
const timeFilter = ref({ dateRangeValue })
|
||||
const { startTime, endTime } = getNowTime(dateRangeValue)
|
||||
timeFilter.value.startTime = startTime
|
||||
timeFilter.value.endTime = endTime
|
||||
|
||||
const currentPoint = ref({})
|
||||
const tooltip = ref({
|
||||
showTooltip: false
|
||||
})
|
||||
|
||||
return {
|
||||
timeFilter,
|
||||
currentPoint,
|
||||
tooltip,
|
||||
maxZoom: 13,
|
||||
minZoom: 1,
|
||||
center: [116.38, 39.9]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<div class="subscriber-kpi">
|
||||
<div class="subscriber-kpi-header">
|
||||
<div class="subscriber-kpi-title">{{$t('subscriber.kpi')}}</div>
|
||||
<date-time-range
|
||||
class="entity-detail-date-time-range"
|
||||
:start-time="timeFilter.startTime"
|
||||
:end-time="timeFilter.endTime"
|
||||
:date-range="timeFilter.dateRangeValue"
|
||||
ref="dateTimeRange"
|
||||
showPosition="left"
|
||||
@change="reload"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="subscriber-kpi-body">
|
||||
<chart-no-data v-if="isNoData" test-id="noData"></chart-no-data>
|
||||
<chart-error v-if="showError" :content="errorMsg" style="top:34px;" />
|
||||
|
||||
<div class="subscriber-kpi-content" v-if="!isNoData && !showError">
|
||||
<div class="kpi-type">
|
||||
<div class="kpi-type-value">
|
||||
<div class="kpi-type-value-name">{{$t('subscriber.volume')}}</div>
|
||||
<div class="kpi-type-data" >
|
||||
<div class="kpi-type-value-number" >{{$_.get(kpiData, 'volume') ? unitConvert($_.get(kpiData, 'volume')[0], unitTypes.byte).join(' ') : '-'}}</div>
|
||||
<div class="data-trend" v-if="$_.get(kpiData, 'volume')">
|
||||
<div v-if="$_.get(kpiData, 'volume')[1] === 'up'" class="data-total-trend data-total-trend-red" >
|
||||
<i class="cn-icon-rise1 cn-icon"></i><span>{{$_.get(kpiData, 'volume')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'volume')[1] === 'down'" class="data-total-trend data-total-trend-green" >
|
||||
<i class="cn-icon-decline cn-icon"></i><span>{{$_.get(kpiData, 'volume')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'volume')[1] === 'noChange'" class="data-total-trend data-total-trend-black" >
|
||||
<i class="cn-icon-constant cn-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-type-value">
|
||||
<div class="kpi-type-value-name">{{$t('subscriber.throughput')}}</div>
|
||||
<div class="kpi-type-data" >
|
||||
<div class="kpi-type-value-number" >{{$_.get(kpiData, 'throughput') ? valueToRangeValue($_.get(kpiData, 'throughput')[0], unitTypes.bps).join(' ') : '-'}}</div>
|
||||
<div class="data-trend" v-if="$_.get(kpiData, 'throughput')">
|
||||
<div v-if="$_.get(kpiData, 'throughput')[1] === 'up'" class="data-total-trend data-total-trend-red" >
|
||||
<i class="cn-icon-rise1 cn-icon"></i><span>{{$_.get(kpiData, 'throughput')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'throughput')[1] === 'down'" class="data-total-trend data-total-trend-green" >
|
||||
<i class="cn-icon-decline cn-icon"></i><span>{{$_.get(kpiData, 'throughput')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'throughput')[1] === 'noChange'" class="data-total-trend data-total-trend-black" >
|
||||
<i class="cn-icon-constant cn-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-type-value">
|
||||
<div class="kpi-type-value-name">{{$t('subscriber.httpLatency')}}</div>
|
||||
<div class="kpi-type-data" >
|
||||
<div class="kpi-type-value-number" >{{$_.get(kpiData, 'httpLatency') ? valueToRangeValue($_.get(kpiData, 'httpLatency')[0], unitTypes.time).join(' ') : '-'}}</div>
|
||||
<div class="data-trend" v-if="$_.get(kpiData, 'httpLatency')">
|
||||
<div v-if="$_.get(kpiData, 'httpLatency')[1] === 'up'" class="data-total-trend data-total-trend-red" >
|
||||
<i class="cn-icon-rise1 cn-icon"></i><span>{{$_.get(kpiData, 'httpLatency')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'httpLatency')[1] === 'down'" class="data-total-trend data-total-trend-green" >
|
||||
<i class="cn-icon-decline cn-icon"></i><span>{{$_.get(kpiData, 'httpLatency')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'httpLatency')[1] === 'noChange'" class="data-total-trend data-total-trend-black" >
|
||||
<i class="cn-icon-constant cn-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-type-value">
|
||||
<div class="kpi-type-value-name">{{$t('subscriber.sslLatency')}}</div>
|
||||
<div class="kpi-type-data" >
|
||||
<div class="kpi-type-value-number" >{{$_.get(kpiData, 'sslLatency') ? valueToRangeValue($_.get(kpiData, 'sslLatency')[0], unitTypes.time).join(' ') : '-'}}</div>
|
||||
<div class="data-trend" v-if="$_.get(kpiData, 'sslLatency')">
|
||||
<div v-if="$_.get(kpiData, 'sslLatency')[1] === 'up'" class="data-total-trend data-total-trend-red" >
|
||||
<i class="cn-icon-rise1 cn-icon"></i><span>{{$_.get(kpiData, 'sslLatency')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'sslLatency')[1] === 'down'" class="data-total-trend data-total-trend-green" >
|
||||
<i class="cn-icon-decline cn-icon"></i><span>{{$_.get(kpiData, 'sslLatency')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'sslLatency')[1] === 'noChange'" class="data-total-trend data-total-trend-black" >
|
||||
<i class="cn-icon-constant cn-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-type-value">
|
||||
<div class="kpi-type-value-name">{{$t('subscriber.packetLoss')}}</div>
|
||||
<div class="kpi-type-data" >
|
||||
<div class="kpi-type-value-number" >{{$_.get(kpiData, 'packetLoss') ? valueToRangeValue($_.get(kpiData, 'packetLoss')[0], unitTypes.percent).join(' ') : '-'}}</div>
|
||||
<div class="data-trend" v-if="$_.get(kpiData, 'packetLoss')">
|
||||
<div v-if="$_.get(kpiData, 'packetLoss')[1] === 'up'" class="data-total-trend data-total-trend-red" >
|
||||
<i class="cn-icon-rise1 cn-icon"></i><span>{{$_.get(kpiData, 'packetLoss')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'packetLoss')[1] === 'down'" class="data-total-trend data-total-trend-green" >
|
||||
<i class="cn-icon-decline cn-icon"></i><span>{{$_.get(kpiData, 'packetLoss')[2]}}</span>
|
||||
</div>
|
||||
<div v-else-if=" $_.get(kpiData, 'packetLoss')[1] === 'noChange'" class="data-total-trend data-total-trend-black" >
|
||||
<i class="cn-icon-constant cn-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '@/utils/api'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import ChartNoData from '@/views/charts/charts/ChartNoData'
|
||||
import chartMixin from '@/views/charts2/chart-mixin'
|
||||
import ChartError from '@/components/common/Error'
|
||||
import unitConvert, { valueToRangeValue } from '@/utils/unit-convert'
|
||||
import { unitTypes } from '@/utils/constants'
|
||||
import { overwriteUrl, urlParamsHandler, getChainRatio } from '@/utils/tools'
|
||||
import axios from 'axios'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { getNowTime, getSecond } from '@/utils/date-util'
|
||||
|
||||
export default {
|
||||
name: 'EntityDetailSubscriberKpi',
|
||||
components: {
|
||||
ChartError,
|
||||
ChartNoData
|
||||
},
|
||||
mixins: [chartMixin],
|
||||
setup () {
|
||||
const { query } = useRoute()
|
||||
const queryCondition = ref(query.queryCondition || '')
|
||||
|
||||
// 获取url携带的range、startTime、endTime
|
||||
const rangeParam = query.kpiRange
|
||||
const startTimeParam = query.kpiStartTime
|
||||
const endTimeParam = query.kpiEndTime
|
||||
|
||||
// 优先级:url > config.js > 默认值。
|
||||
const dateRangeValue = rangeParam ? parseInt(rangeParam) : (DEFAULT_TIME_FILTER_RANGE.entity.subscriberKpi || 60)
|
||||
const timeFilter = ref({ dateRangeValue })
|
||||
if (!startTimeParam || !endTimeParam) {
|
||||
const { startTime, endTime } = getNowTime(dateRangeValue)
|
||||
timeFilter.value.startTime = startTime
|
||||
timeFilter.value.endTime = endTime
|
||||
} else {
|
||||
timeFilter.value.startTime = parseInt(startTimeParam)
|
||||
timeFilter.value.endTime = parseInt(endTimeParam)
|
||||
}
|
||||
|
||||
return {
|
||||
queryCondition,
|
||||
timeFilter
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
kpiData: {
|
||||
},
|
||||
unitConvert,
|
||||
valueToRangeValue,
|
||||
unitTypes,
|
||||
isNoData: false,
|
||||
showError: false,
|
||||
errorMsg: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
timeFilter: {
|
||||
handler () {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTrendData (curData, preData) {
|
||||
let trend = ''
|
||||
let trendPercent = ''
|
||||
curData = Number(curData)
|
||||
preData = Number(preData)
|
||||
if (curData === 0 && preData === 0) {
|
||||
trend = 'noChange'
|
||||
trendPercent = ''
|
||||
} else if (curData && preData) {
|
||||
const totalDiff = curData - preData
|
||||
const chainRatio = getChainRatio(curData, preData)
|
||||
if (chainRatio !== '-') {
|
||||
trendPercent = parseFloat(Math.abs(chainRatio) * 100).toFixed(2)
|
||||
if (totalDiff > 0) {
|
||||
trend = 'up'
|
||||
if (trendPercent <= 500) {
|
||||
trendPercent = trendPercent + '%'
|
||||
} else {
|
||||
trendPercent = '>500%'
|
||||
}
|
||||
} else if (totalDiff < 0) {
|
||||
trend = 'down'
|
||||
if (trendPercent <= 500) {
|
||||
trendPercent = trendPercent + '%'
|
||||
} else {
|
||||
trendPercent = '>500%'
|
||||
}
|
||||
} else if (totalDiff === 0) {
|
||||
trend = 'noChange'// 横向图标
|
||||
} else {
|
||||
trend = ''
|
||||
}
|
||||
if (trendPercent === '0%') {
|
||||
trend = 'noChange'
|
||||
trendPercent = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
return [curData, trend, trendPercent]
|
||||
},
|
||||
init () {
|
||||
const params = {
|
||||
resource: this.entity.entityName,
|
||||
startTime: getSecond(this.timeFilter.startTime),
|
||||
endTime: getSecond(this.timeFilter.endTime)
|
||||
}
|
||||
const preParams = {
|
||||
resource: this.entity.entityName,
|
||||
startTime: getSecond(this.timeFilter.startTime),
|
||||
endTime: getSecond(this.timeFilter.endTime),
|
||||
cycle: 1
|
||||
}
|
||||
|
||||
this.toggleLoading(true)
|
||||
|
||||
axios.get(api.entity.subscriberKpi, { params: params }).then(response => {
|
||||
const res = response.data
|
||||
if (response.status === 200) {
|
||||
const data = res.data
|
||||
this.isNoData = Object.keys(data).length === 0
|
||||
this.showError = false
|
||||
if (!this.isNoData) {
|
||||
this.kpiData = {
|
||||
volume: [data ? data.total_bytes : null],
|
||||
throughput: [data ? data.avg_bits_per_sec : null],
|
||||
httpLatency: [data ? data.avg_http_response_latency_ms : null],
|
||||
sslLatency: [data ? data.avg_ssl_handshake_latency_ms : null],
|
||||
packetLoss: [data ? data.tcp_lost_bytes_ratio : null]
|
||||
}
|
||||
|
||||
axios.get(api.entity.subscriberKpi, { params: preParams }).then(preCycleRes => {
|
||||
const preRes = preCycleRes.data
|
||||
if (preCycleRes.status === 200) {
|
||||
const data = preRes.data
|
||||
if (Object.keys(data).length > 0) {
|
||||
const preVolume = data ? data.avg_bits_per_sec : null
|
||||
const preThroughput = data ? data.total_bytes : null
|
||||
const preHttpLatency = data ? data.avg_http_response_latency_ms : null
|
||||
const preSslLatency = data ? data.avg_ssl_handshake_latency_ms : null
|
||||
const prePacketLoss = data ? data.tcp_lost_bytes_ratio : null
|
||||
|
||||
if (this.kpiData.volume[0] !== null && preVolume !== null) {
|
||||
this.kpiData.volume = this.handleTrendData(this.kpiData.volume[0], preVolume)
|
||||
}
|
||||
if (this.kpiData.throughput[0] !== null && preThroughput !== null) {
|
||||
this.kpiData.throughput = this.handleTrendData(this.kpiData.throughput[0], preThroughput)
|
||||
}
|
||||
if (this.kpiData.httpLatency[0] !== null && preHttpLatency !== null) {
|
||||
this.kpiData.httpLatency = this.handleTrendData(this.kpiData.httpLatency[0], preHttpLatency)
|
||||
}
|
||||
if (this.kpiData.sslLatency[0] !== null && preSslLatency !== null) {
|
||||
this.kpiData.sslLatency = this.handleTrendData(this.kpiData.sslLatency[0], preSslLatency)
|
||||
}
|
||||
if (this.kpiData.packetLoss[0] !== null && prePacketLoss !== null) {
|
||||
this.kpiData.packetLoss = this.handleTrendData(this.kpiData.packetLoss[0], prePacketLoss)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.httpError(res)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.httpError(res)
|
||||
}
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.httpError(e)
|
||||
}).finally(() => {
|
||||
this.toggleLoading(false)
|
||||
})
|
||||
},
|
||||
httpError (e) {
|
||||
this.isNoData = false
|
||||
this.showError = true
|
||||
this.errorMsg = this.errorMsgHandler(e)
|
||||
},
|
||||
reload (startTime, endTime, dateRangeValue) {
|
||||
this.timeFilter = { startTime: getSecond(startTime), endTime: getSecond(endTime) }
|
||||
const { query } = this.$route
|
||||
|
||||
const newUrl = urlParamsHandler(window.location.href, query, {
|
||||
kpiStartTime: this.timeFilter.startTime,
|
||||
kpiEndTime: this.timeFilter.endTime,
|
||||
kpiRange: dateRangeValue.value
|
||||
})
|
||||
overwriteUrl(newUrl)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
this.init()
|
||||
})
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.unitConvert = null
|
||||
this.valueToRangeValue = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="entity-detail-line entity-detail-line--subscriber">
|
||||
<el-tabs v-model="lineOuterTab" class="cn-chart__tabs" @tab-click="switchOuterTab">
|
||||
<el-tab-pane name="serviceName" class="tab-pane" :label="$t('subscriber.serviceName')">
|
||||
<service-name
|
||||
v-if="lineOuterTab === 'serviceName'"
|
||||
:chart="chart"
|
||||
:entity="entity"
|
||||
@toggleLoading="toggleLoading"
|
||||
></service-name>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="app" class="tab-pane" label="APP">
|
||||
<app
|
||||
v-if="lineOuterTab === 'app'"
|
||||
:chart="chart"
|
||||
:entity="entity"
|
||||
@toggleLoading="toggleLoading"
|
||||
></app>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ServiceName from '@/views/charts2/charts/entityDetail/lines/ServiceName'
|
||||
import App from '@/views/charts2/charts/entityDetail/lines/App'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref } from 'vue'
|
||||
import { overwriteUrl, urlParamsHandler } from '@/utils/tools'
|
||||
|
||||
export default {
|
||||
name: 'EntityDetailLine',
|
||||
components: {
|
||||
ServiceName,
|
||||
App
|
||||
},
|
||||
props: {
|
||||
chart: Object,
|
||||
entity: Object
|
||||
},
|
||||
methods: {
|
||||
switchOuterTab (tab) {
|
||||
const { query } = this.$route
|
||||
const newUrl = urlParamsHandler(window.location.href, query, {
|
||||
lineOuterTab: tab.paneName
|
||||
})
|
||||
overwriteUrl(newUrl)
|
||||
},
|
||||
toggleLoading (loading) {
|
||||
this.$emit('toggleLoading', loading)
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
const { query } = useRoute()
|
||||
const lineOuterTab = ref(query.lineOuterTab || 'serviceName')
|
||||
return {
|
||||
lineOuterTab
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||