Compare commits

..

34 Commits

Author SHA1 Message Date
Bai
65392f1d0c perf: ZTree Search Input support select node or asset 2025-12-29 18:09:04 +08:00
Bai
777b139fde perf: modify create asset field nodes to node; modify user-detail-tree and workbench-tree 2025-12-26 18:03:39 +08:00
Bai
cccd2cbb7a perf: ztree filterTree, add shouldCollapseNodes 2025-12-24 16:48:24 +08:00
Bai
f75a4007f6 perf: ztree filterTree, default expand matchNodes parents, not include childrens 2025-12-24 16:31:33 +08:00
Bai
a9c1e06706 refactor: while search tree, re-initial ZTree use API data. 2025-12-23 18:47:52 +08:00
Bai
b66b779e79 perf: modify AssetTreeTable to async:false, and fix sync filter tree nodes expand all children 2025-12-22 18:50:23 +08:00
feng
841ba521f6 perf: assistant content 2025-12-12 10:17:18 +08:00
feng
d2feaa021e perf: Chat input tool switch css 2025-12-11 15:24:13 +08:00
feng
c231f66e4a perf: remove typeOptions 2025-12-10 18:36:42 +08:00
feng
7fe6ab5f1b perf: mcp tools 2025-12-10 18:17:38 +08:00
feng
c037b52bb6 perf: tools 2025-12-10 17:11:07 +08:00
feng
37a45cbc6e perf: thinking 2025-12-10 17:11:07 +08:00
github-actions[bot]
3d90d56373 perf: Update Dockerfile with new base image tag 2025-12-10 17:11:07 +08:00
feng
587f49286f perf: To kael 2025-12-10 17:11:07 +08:00
feng
5e1b32aaf9 perf: Open web ui settigns 2025-12-10 17:11:07 +08:00
feng
e1bc8245c9 fix: Ticket comment not display 2025-12-10 17:11:07 +08:00
w940853815
6b6de2b2c5 perf: Update asset detection logic in AccountRiskList for improved menu display 2025-12-10 17:11:07 +08:00
feng
7913979b4b perf: Account bulk error prompt 2025-12-10 17:11:07 +08:00
w940853815
16d8c7c9ac perf: Enhance error message handling for nested plain objects in getErrorResponseMsg 2025-12-10 17:11:07 +08:00
zhaojisen
768210ef86 Fixed: Fix the issue where the global organization text is not displayed completely in the organization dropdown. 2025-12-10 17:11:07 +08:00
w940853815
ce9d598683 fix: Ensure progress display is shown correctly during job execution 2025-12-10 17:11:07 +08:00
zhaojisen
2d75514aa1 Fixed: Fix the issue where creating an SSH public key for a third-party user redirects to the login password tab. 2025-12-10 17:11:07 +08:00
w940853815
669831c533 fix: Click Review to enter comments, and the page will be dimmed 2025-12-10 17:11:07 +08:00
w940853815
eb3f3ba441 perf: Add support for error message extraction from plain objects in getErrorResponseMsg 2025-12-10 17:11:07 +08:00
feng
46cf67e10f fix: Account push show params 2025-12-10 17:11:07 +08:00
w940853815
4e0b20b8e8 perf: Add condition to check node type in AccountRiskList for asset detection 2025-12-10 17:11:07 +08:00
w940853815
a6faad4b93 perf: Refactor error handling and improve parameter assignment in job forms 2025-12-10 17:11:07 +08:00
w940853815
cb51ccae61 perf: Improve error message formatting for array of strings 2025-12-10 17:11:07 +08:00
w940853815
5a9d94aad0 perf: Enhance AccountFormatter with options to exclude accounts 2025-12-10 17:11:07 +08:00
w940853815
69bdc7c0a2 perf: Job execution detail content add text-overflow class 2025-12-10 17:11:07 +08:00
w940853815
2441c08da5 perf: Support batch import of leak passwords 2025-12-10 17:11:07 +08:00
ibuler
f9c244f006 perf: small size some png 2025-12-10 17:11:07 +08:00
fit2bot
9c5ff2b3a8 perf: upgrade vue version (#5260)
* perf: upgrade vue version

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: zhaojisen <1301338853@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-10 17:11:07 +08:00
w940853815
91bfb4d723 perf: Add confirmation dialog before syncing platform protocols 2025-12-10 17:11:07 +08:00
52 changed files with 2511 additions and 760 deletions

View File

@@ -1,4 +1,4 @@
FROM jumpserver/lina-base:20251105_092554 AS stage-build
FROM jumpserver/lina-base:20251204_081759 AS stage-build
ARG VERSION
ENV VERSION=$VERSION

View File

@@ -59,7 +59,9 @@
"npm": "^7.8.0",
"nprogress": "0.2.0",
"path-to-regexp": "3.3.0",
"socket.io-client": "^4.8.1",
"sortablejs": "^1.15.6",
"uuid": "8.3.2",
"v-sanitize": "^0.0.13",
"vue": "2.7.16",
"vue-codemirror": "4.0.6",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -62,7 +62,6 @@ import Dialog from '@/components/Dialog/index.vue'
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
import { SecretViewerFormatter } from '@/components/Table/TableFormatters'
import { encryptPassword } from '@/utils/secure'
import { mapGetters } from 'vuex'
export default {
name: 'ShowSecretInfo',
@@ -112,9 +111,6 @@ export default {
}
},
computed: {
...mapGetters({
publicSettings: 'publicSettings'
}),
secretTypeLabel() {
return this.account['secret_type'].label || 'Password'
},
@@ -150,11 +146,7 @@ export default {
})
},
showSecretDialog() {
if (!this.publicSettings.SECURITY_ACCOUNT_SECRET_READ) {
this.$message.warning(this.$tc('AccountSecretReadDisabled'))
return
}
return this.$axios.get(this.url).then((res) => {
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then((res) => {
this.secretInfo = res
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
this.showSecret = true
@@ -175,54 +167,54 @@ export default {
</script>
<style lang="scss" scoped>
.item-textarea ::v-deep .el-textarea__inner {
height: 110px;
}
.el-form-item {
border-bottom: 1px solid #EBEEF5;
padding: 5px 0;
margin-bottom: 0;
&:last-child {
border-bottom: none;
.item-textarea ::v-deep .el-textarea__inner {
height: 110px;
}
::v-deep .el-form-item__label {
display: flex;
align-items: center;
justify-content: flex-start;
padding-right: 20px;
line-height: 30px;
word-break: keep-all;
overflow-wrap: break-word;
white-space: normal;
}
.el-form-item {
border-bottom: 1px solid #EBEEF5;
padding: 5px 0;
margin-bottom: 0;
::v-deep .el-form-item__content {
line-height: 30px;
&:last-child {
border-bottom: none;
}
pre {
margin: 0;
::v-deep .el-form-item__label {
display: flex;
align-items: center;
justify-content: flex-start;
padding-right: 20px;
line-height: 30px;
word-break: keep-all;
overflow-wrap: break-word;
white-space: normal;
}
::v-deep .el-form-item__content {
line-height: 30px;
pre {
margin: 0;
}
}
}
}
ul {
margin: 0;
}
li {
display: block;
font-size: 13px;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.title {
color: #303133;
font-weight: 500;
ul {
margin: 0;
}
li {
display: block;
font-size: 13px;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.title {
color: #303133;
font-weight: 500;
}
}
}
</style>

View File

@@ -20,7 +20,7 @@
<script>
import TreeTable from '../../Table/TreeTable/index.vue'
import { getShowCurrentAssetValue, setRouterQuery, setUrlParam } from '@/utils/common/index'
import { setRouterQuery, setUrlParam } from '@/utils/common/index'
import $ from '@/utils/jquery-vendor'
export default {
@@ -160,7 +160,7 @@ export default {
return str
},
decorateRMenu() {
const show_current_asset = getShowCurrentAssetValue(this.$cookie)
const show_current_asset = this.$cookie.get('show_current_asset') || '0'
if (show_current_asset === '1') {
$('#m_show_asset_all_children_node').css('color', '#606266')
$('#m_show_asset_only_current_node').css('color', 'green')
@@ -172,7 +172,6 @@ export default {
getAssetsUrl(treeNode) {
let url = this.treeSetting?.url || this.url
const showCurrentAsset = getShowCurrentAssetValue(this.$cookie)
const setParam = (param, value, delay) => {
setTimeout(() => {
@@ -184,12 +183,10 @@ export default {
const nodeId = treeNode.meta.data.id
setParam('node_id', nodeId)
setParam('asset_id', '')
setParam('show_current_asset', showCurrentAsset)
} else if (treeNode.meta.type === 'asset') {
const assetId = treeNode.meta.data?.id || treeNode.id
setParam('node_id', '')
setParam('asset_id', assetId)
setParam('show_current_asset', showCurrentAsset)
} else if (treeNode.meta.type === 'category') {
setParam('category', treeNode.meta.category)
} else if (treeNode.meta.type === 'type') {

View File

@@ -1,12 +1,60 @@
<template>
<div class="container">
<div v-if="hasPrompt" class="chat-action">
<Select2
v-model="select.value"
:disabled="isLoading || isSelectDisabled"
v-bind="select"
@change="onSelectChange"
/>
<div class="chat-action">
<div class="model-select">
<Select2
v-model="select.value"
:disabled="isLoading || isSelectDisabled || loading || !options.length"
v-bind="select"
@change="onSelectChange"
/>
</div>
<el-dropdown
:hide-on-click="false"
trigger="click"
>
<span class="el-dropdown-link">
<i class="fa fa-plug" />
</span>
<el-dropdown-menu slot="dropdown">
<div class="menu-section">
<div v-if="toolsLoading">
<i class="el-icon-loading" /> {{ $t('Loading') }}
</div>
<div v-else class="menu-body">
<div>
<div
v-for="item in toolOptions"
:key="item.value"
>
<div style="padding: 0 10px">
<i class="fa fa-wrench item-icon" />
<span class="item-label">{{ item.label }}</span>
&nbsp;&nbsp;&nbsp;
<el-switch
:value="selectedToolsSet.has(item.value)"
@change="() => toggleTool(item.value)"
/>
</div>
</div>
<div
v-for="item in toolServerOptions"
:key="item.value"
>
<div>
<i class="fa fa-server item-icon" />
<span class="item-label">{{ item.label }}</span>
<el-switch
:value="selectedToolServersSet.has(item.value)"
@change="() => toggleToolServer(item.value)"
/>
</div>
</div>
</div>
</div>
</div>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="chat-input">
<el-input
@@ -37,9 +85,37 @@ export default {
type: Boolean,
default: false
},
hasPrompt: {
modelOptions: {
type: Array,
default: () => []
},
selectedModel: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: true
default: false
},
toolOptions: {
type: Array,
default: () => []
},
toolServerOptions: {
type: Array,
default: () => []
},
selectedTools: {
type: Array,
default: () => []
},
selectedToolServers: {
type: Array,
default: () => []
},
toolsLoading: {
type: Boolean,
default: false
}
},
data() {
@@ -47,15 +123,10 @@ export default {
isIM: false,
inputValue: '',
select: {
url: '/api/v1/settings/chatai-prompts/',
value: '',
multiple: false,
placeholder: this.$t('Role'),
ajax: {
transformOption: (item) => {
return { label: item.name, value: item.content }
}
}
placeholder: this.$t('Model'),
options: []
}
}
},
@@ -64,7 +135,32 @@ export default {
isLoading: state => state.chat.loading
}),
isSelectDisabled() {
return !!this.select.value
return false
},
options() {
return (this.modelOptions || []).map(item => {
return { label: item.name || item.id, value: item.id }
})
},
selectedToolsSet() {
return new Set(this.selectedTools || [])
},
selectedToolServersSet() {
return new Set(this.selectedToolServers || [])
}
},
watch: {
modelOptions: {
immediate: true,
handler(val) {
this.select.options = (val || []).map(item => ({ label: item.name || item.id, value: item.id }))
}
},
selectedModel: {
immediate: true,
handler(val) {
this.select.value = val || ''
}
}
},
methods: {
@@ -84,7 +180,25 @@ export default {
this.inputValue = ''
},
onSelectChange(value) {
this.$emit('select-prompt', value)
this.$emit('select-model', value)
},
toggleTool(id) {
const set = new Set(this.selectedTools || [])
if (set.has(id)) {
set.delete(id)
} else {
set.add(id)
}
this.$emit('select-tools', Array.from(set))
},
toggleToolServer(id) {
const set = new Set(this.selectedToolServers || [])
if (set.has(id)) {
set.delete(id)
} else {
set.add(id)
}
this.$emit('select-tool-servers', Array.from(set))
}
}
}
@@ -99,9 +213,18 @@ export default {
.chat-action {
width: 100%;
margin: 6px 0;
display: flex;
align-items: center;
gap: 8px;
.model-select {
flex: 0 0 48%;
max-width: 240px;
min-width: 160px;
}
&::v-deep .el-select {
width: 50%;
width: 100%;
.el-input__inner {
height: 28px;

View File

@@ -3,9 +3,13 @@
<div class="chart-item-container">
<div class="avatar">
<el-avatar
:src="isUserRole ? userUrl : chatUrl"
v-if="isUserRole"
:src="userUrl"
class="header-avatar"
/>
<el-avatar v-else class="header-avatar" :style="{ backgroundColor: 'transparent' }">
<ModelIcon :name="modelIconName" class-name="model-icon" />
</el-avatar>
</div>
<div class="content">
<div class="operational">
@@ -76,6 +80,7 @@
<script>
import MessageText from './MessageText.vue'
import ModelIcon from '../../models/ModelIcon.vue'
import { mapGetters, mapState } from 'vuex'
import { copy } from '@/utils/common/index'
import { useChat } from '../../useChat.js'
@@ -85,7 +90,8 @@ const { setLoading, removeLoadingMessageInChat } = useChat()
export default {
components: {
MessageText
MessageText,
ModelIcon
},
props: {
item: {
@@ -93,6 +99,10 @@ export default {
default: () => {
}
},
selectedModel: {
type: String,
default: ''
},
isTerminal: {
type: Boolean,
default: false
@@ -129,10 +139,8 @@ export default {
? this.$i18n.t('ServerBusyRetry')
: ''
},
chatUrl() {
return this.publicSettings.CHAT_AI_TYPE === 'gpt'
? require('@/assets/img/chat.png')
: require('@/assets/img/deepSeek.png')
modelIconName() {
return (this.item?.message?.model || this.selectedModel || this.publicSettings.CHAT_AI_TYPE || '').toString()
}
},
methods: {
@@ -171,11 +179,18 @@ export default {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: transparent;
&::v-deep img {
background-color: #fff;
}
}
.model-icon {
width: 100%;
height: 100%;
display: block;
}
}
.content {

View File

@@ -40,7 +40,7 @@ export default {
text() {
const value = this.message?.content || ''
if (value && this.markdown) {
return this.markdown?.render(value)
return this.renderContentWithDetails(value)
}
return this.$xss.process(value)
}
@@ -120,6 +120,77 @@ export default {
btn.addEventListener('click', callback)
})
},
renderContentWithDetails(value) {
// Kael responses may wrap reasoning/thinking in <details type="reasoning">; render them with a custom block.
const detailRegex = /<details[^>]*>[\s\S]*?<\/details>/gi
let result = ''
let lastIndex = 0
let match
let hasDetails = false
while ((match = detailRegex.exec(value))) {
hasDetails = true
const preceding = value.slice(lastIndex, match.index)
if (preceding.trim()) {
result += this.markdown.render(preceding)
}
result += this.renderDetailBlock(match[0])
lastIndex = match.index + match[0].length
}
if (!hasDetails) {
return this.markdown.render(value)
}
const remaining = value.slice(lastIndex)
if (remaining.trim()) {
result += this.markdown.render(remaining)
}
return result
},
renderDetailBlock(detailStr) {
const attributes = this.extractAttributes(detailStr)
const inner = detailStr.replace(/^<details[^>]*>/i, '').replace(/<\/details>$/i, '')
const summaryMatch = inner.match(/<summary>([\s\S]*?)<\/summary>/i)
const summary = summaryMatch ? this.decodeHtml(summaryMatch[1]) : ''
const body = summaryMatch ? inner.replace(summaryMatch[0], '') : inner
const bodyHtml = body.trim() ? this.markdown.render(this.decodeHtml(body.trim())) : ''
const baseClass = 'kael-detail'
if (attributes.type === 'reasoning') {
const statusClass = attributes.done === 'true' ? 'is-done' : 'is-pending'
const title = summary || this.$t('DeeplyThoughtAbout')
return `<div class="${baseClass} ${baseClass}--reasoning ${statusClass}">
<div class="${baseClass}__header">
<span class="${baseClass}__status-dot"></span>
<span class="${baseClass}__title">${title}</span>
</div>
${bodyHtml ? `<div class="${baseClass}__body">${bodyHtml}</div>` : ''}
</div>`
}
return `<div class="${baseClass}">
${summary ? `<div class="${baseClass}__header">${summary}</div>` : ''}
${bodyHtml ? `<div class="${baseClass}__body">${bodyHtml}</div>` : ''}
</div>`
},
extractAttributes(detailStr) {
const attrs = {}
const attrMatch = detailStr.match(/^<details([^>]*)>/i)
const attrStr = (attrMatch && attrMatch[1]) || ''
attrStr.replace(/(\w+)="(.*?)"/g, (all, key, val) => {
attrs[key] = val
return all
})
return attrs
},
decodeHtml(str) {
if (!str) return ''
const textArea = document.createElement('textarea')
textArea.innerHTML = str
return textArea.value
},
removeBtnClickEvent(selector) {
const buttons = this.$refs.textRef.querySelectorAll(selector)
buttons.forEach((btn) => {
@@ -258,4 +329,64 @@ export default {
.loading-box span:nth-child(3) {
animation-delay: 0.49s;
}
.kael-detail {
margin: 8px 0;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid #e5e5e5;
background: #f7f8fa;
&__header {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #6b7280;
}
&__title {
font-weight: 600;
}
&__status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #10b981;
}
&__body {
margin-top: 6px;
padding-left: 8px;
border-left: 2px solid #e5e5e5;
}
&--reasoning.is-pending {
border-color: #f59e0b40;
background: #fff8e6;
.kael-detail__status-dot {
background: #f59e0b;
animation: kael-pulse 1.2s ease-in-out infinite;
}
}
&--reasoning.is-done {
border-color: #dbeafe;
background: #f4f6ff;
}
}
@keyframes kael-pulse {
0% {
opacity: 0.45;
}
50% {
opacity: 1;
}
100% {
opacity: 0.45;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -46,7 +46,6 @@
import Sidebar from './components/Sidebar/index.vue'
import Chat from './components/ChitChat/index.vue'
import { getInputFocus } from './useChat.js'
import { ws } from '@/utils/request'
import DrawerPanel from '@/components/Apps/DrawerPanel/index.vue'
import { ObjectLocalStorage } from '@/utils/common'
import { mapGetters } from 'vuex'
@@ -82,7 +81,8 @@ export default {
height: '400px',
expanded: false,
clientOffset: {},
currentTerminalContent: {}
currentTerminalContent: {},
initialized: false
}
},
computed: {
@@ -123,11 +123,18 @@ export default {
document.body.appendChild(script)
}
},
initAssistant() {
if (this.initialized) return
this.initialized = true
this.$nextTick(() => {
this.$refs.component?.init()
})
},
handlePostMessage() {
window.addEventListener('message', (event) => {
if (event.data === 'show-chat-panel') {
this.$refs.drawer.show = true
this.initWebSocket()
this.initAssistant()
return
}
const msg = event.data
@@ -152,11 +159,6 @@ export default {
}
this.$refs.drawer.handleHeaderMoveUp(event)
},
initWebSocket() {
if (!ws) {
this.$refs.component?.init()
}
},
onClose() {
this.$refs.drawer.show = false
},
@@ -170,7 +172,6 @@ export default {
},
save_pannel_settings() {
aiPannelLocalStorage.set('expanded', this.expanded)
console.log('AI panel settings saved:', this.expanded)
},
updateExpandedState(expanded) {
this.expanded = expanded
@@ -184,8 +185,8 @@ export default {
})
},
onToggle(status) {
this.initWebSocket()
if (status) {
this.initAssistant()
getInputFocus()
}
}

View File

@@ -0,0 +1,33 @@
<template>
<svg
:stroke-width="strokeWidth"
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
:class="className"
>
<path
fill="#6b7280"
style="fill: #6b7280 !important"
d="M11.2475 18.25C10.6975 18.25 10.175 18.1455 9.67999 17.9365C9.18499 17.7275 8.74499 17.436 8.35999 17.062C7.94199 17.205 7.50749 17.2765 7.05649 17.2765C6.31949 17.2765 5.63749 17.095 5.01049 16.732C4.38349 16.369 3.87749 15.874 3.49249 15.247C3.11849 14.62 2.93149 13.9215 2.93149 13.1515C2.93149 12.8325 2.97549 12.486 3.06349 12.112C2.62349 11.705 2.28249 11.2375 2.04049 10.7095C1.79849 10.1705 1.67749 9.6095 1.67749 9.0265C1.67749 8.4325 1.80399 7.8605 2.05699 7.3105C2.30999 6.7605 2.66199 6.2875 3.11299 5.8915C3.57499 5.4845 4.10849 5.204 4.71349 5.05C4.83449 4.423 5.08749 3.862 5.47249 3.367C5.86849 2.861 6.35249 2.465 6.92449 2.179C7.49649 1.893 8.10699 1.75 8.75599 1.75C9.30599 1.75 9.82849 1.8545 10.3235 2.0635C10.8185 2.2725 11.2585 2.564 11.6435 2.938C12.0615 2.795 12.496 2.7235 12.947 2.7235C13.684 2.7235 14.366 2.905 14.993 3.268C15.62 3.631 16.1205 4.126 16.4945 4.753C16.8795 5.38 17.072 6.0785 17.072 6.8485C17.072 7.1675 17.028 7.514 16.94 7.888C17.38 8.295 17.721 8.768 17.963 9.307C18.205 9.835 18.326 10.3905 18.326 10.9735C18.326 11.5675 18.1995 12.1395 17.9465 12.6895C17.6935 13.2395 17.336 13.718 16.874 14.125C16.423 14.521 15.895 14.796 15.29 14.95C15.169 15.577 14.9105 16.138 14.5145 16.633C14.1295 17.139 13.651 17.535 13.079 17.821C12.507 18.107 11.8965 18.25 11.2475 18.25ZM7.17199 16.1875C7.72199 16.1875 8.20049 16.072 8.60749 15.841L11.7095 14.059C11.8195 13.982 11.8745 13.8775 11.8745 13.7455V12.3265L7.88149 14.62C7.63949 14.763 7.39749 14.763 7.15549 14.62L4.03699 12.8215C4.03699 12.8545 4.03149 12.893 4.02049 12.937C4.02049 12.981 4.02049 13.047 4.02049 13.135C4.02049 13.696 4.15249 14.213 4.41649 14.686C4.69149 15.148 5.07099 15.511 5.55499 15.775C6.03899 16.05 6.57799 16.1875 7.17199 16.1875ZM7.33699 13.498C7.40299 13.531 7.46349 13.5475 7.51849 13.5475C7.57349 13.5475 7.62849 13.531 7.68349 13.498L8.92099 12.7885L4.94449 10.4785C4.70249 10.3355 4.58149 10.121 4.58149 9.835V6.2545C4.03149 6.4965 3.59149 6.8705 3.26149 7.3765C2.93149 7.8715 2.76649 8.4215 2.76649 9.0265C2.76649 9.5655 2.90399 10.0825 3.17899 10.5775C3.45399 11.0725 3.81149 11.4465 4.25149 11.6995L7.33699 13.498ZM11.2475 17.161C11.8305 17.161 12.3585 17.029 12.8315 16.765C13.3045 16.501 13.6785 16.138 13.9535 15.676C14.2285 15.214 14.366 14.697 14.366 14.125V10.561C14.366 10.429 14.311 10.33 14.201 10.264L12.947 9.538V14.1415C12.947 14.4275 12.826 14.642 12.584 14.785L9.46549 16.5835C10.0045 16.9685 10.5985 17.161 11.2475 17.161ZM11.8745 11.122V8.878L10.01 7.822L8.12899 8.878V11.122L10.01 12.178L11.8745 11.122ZM7.05649 5.8585C7.05649 5.5725 7.17749 5.358 7.41949 5.215L10.538 3.4165C9.99899 3.0315 9.40499 2.839 8.75599 2.839C8.17299 2.839 7.64499 2.971 7.17199 3.235C6.69899 3.499 6.32499 3.862 6.04999 4.324C5.78599 4.786 5.65399 5.303 5.65399 5.875V9.4225C5.65399 9.5545 5.70899 9.659 5.81899 9.736L7.05649 10.462V5.8585ZM15.4385 13.7455C15.9885 13.5035 16.423 13.1295 16.742 12.6235C17.072 12.1175 17.237 11.5675 17.237 10.9735C17.237 10.4345 17.0995 9.9175 16.8245 9.4225C16.5495 8.9275 16.192 8.5535 15.752 8.3005L12.6665 6.5185C12.6005 6.4745 12.54 6.458 12.485 6.469C12.43 6.469 12.375 6.4855 12.32 6.5185L11.0825 7.2115L15.0755 9.538C15.1965 9.604 15.2845 9.692 15.3395 9.802C15.4055 9.901 15.4385 10.022 15.4385 10.165V13.7455ZM12.122 5.3635C12.364 5.2095 12.606 5.2095 12.848 5.3635L15.983 7.195C15.983 7.118 15.983 7.019 15.983 6.898C15.983 6.37 15.851 5.8695 15.587 5.3965C15.334 4.9125 14.9655 4.5275 14.4815 4.2415C14.0085 3.9555 13.4585 3.8125 12.8315 3.8125C12.2815 3.8125 11.803 3.928 11.396 4.159L8.29399 5.941C8.18399 6.018 8.12899 6.1225 8.12899 6.2545V7.6735L12.122 5.3635Z"
/>
</svg>
</template>
<script>
export default {
name: 'ChatGPTIcon',
props: {
className: {
type: String,
default: 'size-8'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<svg
:stroke-width="strokeWidth"
viewBox="0 0 24 16"
overflow="visible"
width="20"
height="20"
:class="className"
>
<g style="transform: translateX(13px) rotateZ(0deg); transform-origin: 4.775px 7.73501px;">
<path
shape-rendering="geometricPrecision"
fill-opacity="1"
fill="#8c653f"
style="fill: #8c653f !important"
d=" M0,0 C0,0 6.1677093505859375,15.470022201538086 6.1677093505859375,15.470022201538086 C6.1677093505859375,15.470022201538086 9.550004005432129,15.470022201538086 9.550004005432129,15.470022201538086 C9.550004005432129,15.470022201538086 3.382294178009033,0 3.382294178009033,0 C3.382294178009033,0 0,0 0,0 C0,0 0,0 0,0z"
/>
</g>
<g opacity="1" style="transform: none; transform-origin: 7.935px 7.73501px;">
<path
shape-rendering="geometricPrecision"
fill-opacity="1"
fill="#8c653f"
style="fill: #8c653f !important"
d=" M5.824605464935303,9.348296165466309 C5.824605464935303,9.348296165466309 7.93500280380249,3.911694288253784 7.93500280380249,3.911694288253784 C7.93500280380249,3.911694288253784 10.045400619506836,9.348296165466309 10.045400619506836,9.348296165466309 C10.045400619506836,9.348296165466309 5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309 C5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309z M6.166755199432373,0 C6.166755199432373,0 0,15.470022201538086 0,15.470022201538086 C0,15.470022201538086 3.4480772018432617,15.470022201538086 3.4480772018432617,15.470022201538086 C3.4480772018432617,15.470022201538086 4.709278583526611,12.22130012512207 4.709278583526611,12.22130012512207 C4.709278583526611,12.22130012512207 11.16093635559082,12.22130012512207 11.16093635559082,12.22130012512207 C11.16093635559082,12.22130012512207 12.421928405761719,15.470022201538086 12.421928405761719,15.470022201538086 C12.421928405761719,15.470022201538086 15.87000560760498,15.470022201538086 15.87000560760498,15.470022201538086 C15.87000560760498,15.470022201538086 9.703250885009766,0 9.703250885009766,0 C9.703250885009766,0 6.166755199432373,0 6.166755199432373,0 C6.166755199432373,0 6.166755199432373,0 6.166755199432373,0z"
/>
</g>
</svg>
</template>
<script>
export default {
name: 'ClaudeIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,34 @@
<template>
<svg
id="图层_1"
xmlns="http://www.w3.org/2000/svg"
data-name="图层 1"
viewBox="0 0 71.69 52.76"
:class="className"
:stroke-width="strokeWidth"
>
<path
id="path"
fill="#4d6bfe"
style="fill: #4d6bfe !important"
d="M523.77,276.34c-.76-.38-1.08.33-1.53.69a4,4,0,0,0-.41.41,5.07,5.07,0,0,1-4.1,1.87,8,8,0,0,0-6.46,2.53,5.82,5.82,0,0,0-3.72-4.62,6.39,6.39,0,0,1-2.85-1.94,7.76,7.76,0,0,1-.92-2.31c-.16-.48-.32-1-.87-1.05s-.83.41-1.07.82a11,11,0,0,0-1.26,5.5,11.9,11.9,0,0,0,5.49,10.14.75.75,0,0,1,.39,1c-.25.84-.54,1.65-.79,2.49-.17.53-.41.65-1,.42a16.63,16.63,0,0,1-5.18-3.52c-2.56-2.48-4.88-5.21-7.76-7.35-.68-.5-1.36-1-2.06-1.41-2.94-2.86.39-5.2,1.16-5.48s.28-1.29-2.33-1.28-5,.88-8,2a8.23,8.23,0,0,1-1.39.41,28.67,28.67,0,0,0-8.61-.3,18.57,18.57,0,0,0-13.44,7.83c-4,5.47-4.91,11.67-3.76,18.15a27.68,27.68,0,0,0,10,16.88,26.8,26.8,0,0,0,19.23,6.39c4.43-.25,9.36-.84,14.92-5.55a13.84,13.84,0,0,0,5.32,1.18,17.24,17.24,0,0,0,5.09-.38c2.2-.46,2.05-2.5,1.25-2.87-6.43-3-5-1.78-6.3-2.77,3.27-3.87,8.2-7.89,10.13-20.92a12.44,12.44,0,0,0,0-2.52c0-.51.1-.71.68-.76a12.55,12.55,0,0,0,4.62-1.42c4.17-2.28,5.85-6,6.25-10.51A1.57,1.57,0,0,0,523.77,276.34Zm-36.34,40.37c-6.24-4.9-9.27-6.52-10.52-6.45s-1,1.41-.7,2.28a8.49,8.49,0,0,0,1.11,2.21,1.14,1.14,0,0,1-.34,1.8c-2,1.24-5.5-.42-5.66-.5a26.08,26.08,0,0,1-9.87-9.88,30.15,30.15,0,0,1-3.87-13.39c-.06-1.15.28-1.56,1.42-1.77a14.31,14.31,0,0,1,4.57-.11,28.56,28.56,0,0,1,16.33,8.29,54.06,54.06,0,0,1,6.58,8.63,41.46,41.46,0,0,0,7.41,8.71,24.36,24.36,0,0,0,2.66,2C494.16,318.82,490.16,318.87,487.43,316.71Zm3-19.23a.92.92,0,0,1,.92-.92.83.83,0,0,1,.32.06.8.8,0,0,1,.34.22.9.9,0,0,1,.25.64.92.92,0,0,1-1.83,0Zm9.29,4.76a5.27,5.27,0,0,1-1.77.48,3.75,3.75,0,0,1-2.38-.76,3.57,3.57,0,0,1-1.65-2.26,5.16,5.16,0,0,1,0-1.76,2,2,0,0,0-.71-2.17,3.1,3.1,0,0,0-2.06-.59,1.63,1.63,0,0,1-.76-.24.75.75,0,0,1-.34-1.07,3.47,3.47,0,0,1,.57-.62,3.9,3.9,0,0,1,3.43,0,10,10,0,0,1,3,2.34,18.62,18.62,0,0,1,2,2.73,10.9,10.9,0,0,1,1.33,2.53C500.65,301.47,500.4,302,499.71,302.24Z"
transform="translate(-452.83 -271.91)"
/>
</svg>
</template>
<script>
export default {
name: 'DeepSeekIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
width="800px"
height="800px"
viewBox="0 0 512 512"
:class="className"
:stroke-width="strokeWidth"
>
<path
fill="#4285f4"
style="fill: #4285f4 !important"
d="M473.16,221.48l-2.26-9.59H262.46v88.22H387c-12.93,61.4-72.93,93.72-121.94,93.72-35.66,0-73.25-15-98.13-39.11a140.08,140.08,0,0,1-41.8-98.88c0-37.16,16.7-74.33,41-98.78s61-38.13,97.49-38.13c41.79,0,71.74,22.19,82.94,32.31l62.69-62.36C390.86,72.72,340.34,32,261.6,32h0c-60.75,0-119,23.27-161.58,65.71C58,139.5,36.25,199.93,36.25,256S56.83,369.48,97.55,411.6C141.06,456.52,202.68,480,266.13,480c57.73,0,112.45-22.62,151.45-63.66,38.34-40.4,58.17-96.3,58.17-154.9C475.75,236.77,473.27,222.12,473.16,221.48Z"
/>
</svg>
</template>
<script>
export default {
name: 'GeminiIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
fill="currentColor"
:class="className"
:stroke-width="strokeWidth"
>
<path
fill="#000000"
style="fill: #000000 !important"
d="m3.005 8.858 8.783 12.544h3.904L6.908 8.858zM6.905 15.825 3 21.402h3.907l1.951-2.788zM16.585 2l-6.75 9.64 1.953 2.79L20.492 2zM17.292 7.965v13.437h3.2V3.395z"
/>
</svg>
</template>
<script>
export default {
name: 'GrokIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,50 @@
<template>
<component
:is="resolvedIcon"
v-if="resolvedIcon"
:class-name="className"
:stroke-width="strokeWidth"
/>
</template>
<script>
import ChatGPTIcon from './ChatGPT.vue'
import DeepSeekIcon from './DeepSeek.vue'
import GrokIcon from './Grok.vue'
import ClaudeIcon from './Claude.vue'
import GeminiIcon from './Gemini.vue'
export default {
name: 'ModelIcon',
props: {
name: {
type: String,
default: ''
},
className: {
type: String,
default: 'size-5'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
},
computed: {
normalizedName() {
return (this.name || '').toLowerCase()
},
resolvedIcon() {
const name = this.normalizedName
if (!name) return null
if (name.includes('gpt')) return ChatGPTIcon
if (name.includes('deep-seek')) return DeepSeekIcon
if (name.includes('deepseek')) return DeepSeekIcon
if (name.includes('grok')) return GrokIcon
if (name.includes('claude')) return ClaudeIcon
if (name.includes('gemini')) return GeminiIcon
return null
}
}
}
</script>

View File

@@ -11,6 +11,7 @@
import AssetTreeTable from '@/components/Apps/AssetTreeTable'
import { AccountInfoFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
import { connectivityMeta } from '@/components/Apps/AccountListTable/const'
import { setUrlParam } from '@/utils/common/index'
export default {
name: 'GrantedAssets',
@@ -34,7 +35,7 @@ export default {
}
const initialUrl = vm.tableConfig.initialUrl
const nodeId = node.meta.data.id
const url = initialUrl.replace('/assets/', `/nodes/${nodeId}/assets/`)
const url = setUrlParam(initialUrl, 'node_id', nodeId)
vm.tableConfig.url = url
}
},
@@ -70,7 +71,7 @@ export default {
showMenu: false,
showRefresh: true,
showAssets: false,
showSearch: false,
showSearch: true,
url: this.tableUrl,
// ?assets=0不显示资产. =1显示资产
treeUrl: this.treeUrl,
@@ -78,6 +79,9 @@ export default {
callback: {
onSelected: (event, node) => vm.onSelected(node, vm),
refresh: vm.refreshObjectAssetPermission
},
async: {
enable: false
}
},
tableConfig: {

View File

@@ -149,7 +149,7 @@ export default {
},
labelWidth: {
type: String,
default: '25%'
default: '18.2%'
}
},
data() {

View File

@@ -4,11 +4,15 @@
<div class="panel-title">
<el-avatar :src="imageUrl" shape="square" />
<div class="title-display">
<p class="name" :title="object.name">{{ object.name }}</p>
<span class="name">{{ object.name }}</span>
<span class="comment">{{ object.provider.label }}</span>
</div>
</div>
<div v-if="iActions.length !== 0" class="panel-actions" @click="handleClick($event)">
<div
v-if="iActions.length !== 0"
class="panel-actions"
@click="handleClick($event)"
>
<el-dropdown>
<el-button size="mini">
<i class="el-icon-more el-icon--right" />
@@ -60,19 +64,21 @@ export default {
},
getImage: {
type: Function,
default: obj => ''
default: (obj) => ''
},
getInfos: {
type: Function,
default: obj => []
default: (obj) => []
},
handleUpdate: {
type: Function,
default: () => {}
default: () => {
}
},
onView: {
type: Function,
default: () => {}
default: () => {
}
}
},
data() {
@@ -146,13 +152,13 @@ export default {
</script>
<style lang="scss" scoped>
div.info-panel {
display: flex;
flex-direction: column;
padding: 10px;
gap: unset;
gap: 10px;
cursor: pointer;
height: initial !important;
.panel-header {
padding: 10px 20px;
@@ -169,23 +175,12 @@ div.info-panel {
.title-display {
display: flex;
flex-basis: 225px;
flex-direction: column;
justify-content: center;
align-items: start;
max-width: 225px;
min-width: 0;
overflow-x: hidden;
text-align: left;
.name {
font-size: 1.1em;
color: #555555;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
margin: unset;
text-align: start;
}
.comment {
@@ -198,7 +193,6 @@ div.info-panel {
::v-deep {
.el-avatar {
background: #fff;
flex-shrink: 0;
}
}
}

View File

@@ -111,7 +111,7 @@ export default {
{
name: 'actionFilter',
icon: 'filter',
tip: this.$t('QuickFilter'),
tip: this.$t('Filter'),
has: this.hasQuickFilter,
callback: this.handleFilterClick.bind(this)
},

View File

@@ -38,7 +38,6 @@
<script>
import { copy, downloadText } from '@/utils/common/index'
import BaseFormatter from '@/components/Table/TableFormatters/base.vue'
import { mapGetters } from 'vuex'
export default {
name: 'SecretViewerFormatter',
@@ -70,9 +69,6 @@ export default {
}
},
computed: {
...mapGetters({
publicSettings: 'publicSettings'
}),
hasShow: function() {
return this.formatterArgs.hasShow
},
@@ -144,10 +140,6 @@ export default {
},
methods: {
async getAccountSecret() {
if (!this.publicSettings.SECURITY_ACCOUNT_SECRET_READ) {
this.$message.warning(this.$tc('AccountSecretReadDisabled'))
return
}
if (this.formatterArgs.secretFrom === 'cellValue' || this.getIt) {
return
}
@@ -186,48 +178,48 @@ export default {
}
</script>
<style lang="scss" scoped>
.content {
display: inline-block;
width: 100%;
overflow: hidden;
//white-space: nowrap;
text-overflow: ellipsis;
font-size: 13px;
.text {
flex: 1;
display: inline;
margin: 0;
padding: 0;
.content {
display: inline-block;
width: 100%;
overflow: hidden;
white-space: nowrap;
//white-space: nowrap;
text-overflow: ellipsis;
}
.action {
font-size: 13px;
cursor: pointer;
margin-left: 1px;
display: inline;
&.right {
float: right;
.text {
flex: 1;
display: inline;
margin: 0;
padding: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.fa {
margin-right: 5px;
.action {
font-size: 13px;
cursor: pointer;
margin-left: 1px;
display: inline;
&:hover {
color: var(--color-primary);
&.right {
float: right;
}
.fa {
margin-right: 5px;
&:hover {
color: var(--color-primary);
}
}
}
}
}
.edit-input ::v-deep input {
border-left: none;
border-right: none;
border-top: none;
height: 30px;
}
.edit-input ::v-deep input {
border-left: none;
border-right: none;
border-top: none;
height: 30px;
}
</style>

View File

@@ -243,12 +243,10 @@ export default {
delete routeFilter.search
}
const asFilterTags = _.cloneDeep(this.filterTags)
setTimeout(() => {
this.filterTags = {
...asFilterTags,
...routeFilter
}
}, 100)
this.filterTags = {
...asFilterTags,
...routeFilter
}
},
getValueLabel(key, value) {
for (const field of this.options) {

View File

@@ -26,7 +26,6 @@
import DataZTree from '../DataZTree/index.vue'
import Icon from '@/components/Widgets/Icon'
import $ from '@/utils/jquery-vendor'
import { getShowCurrentAssetValue } from '@/utils/common/index'
import { mapGetters } from 'vuex'
export default {
@@ -200,7 +199,7 @@ export default {
},
// Request URL: http://localhost/api/v1/assets/assets/?node_id=ID&show_current_asset=null&draw=2&limit=15&offset=0&_=1587022917769
onSelected: function(event, treeNode) {
const show_current_asset = getShowCurrentAssetValue(this.$cookie)
const show_current_asset = this.$cookie.get('show_current_asset') || '0'
if (!this.setting.url) {
return
}

View File

@@ -1,16 +1,67 @@
<template>
<div>
<div class="treebox">
<div v-if="treeSetting.showSearch">
<div v-if="treeSetting.showSearch" @click="focusTreeSearchInput">
<el-input
v-show="showTreeSearch"
ref="treeSearchInput"
v-model="treeSearchValue"
:placeholder="$tc('Search')"
class="fixed-tree-search"
prefix-icon="fa fa-search"
:placeholder="treeSearchInputPlaceholder"
size="mini"
@input="treeSearchHandle"
>
<template #prepend>
<template v-if="!isSearchTypeDropdownEnabled">
<el-tooltip
effect="dark"
placement="top"
:content="currentTreeSearchTypeTooltip"
:open-delay="300"
>
<span style="cursor: pointer;" @click.stop="focusTreeSearchInput">
<i class="fa fa-search" />
<span class="search-label">{{ treeSearchTypeLabel }}</span>
</span>
</el-tooltip>
</template>
<template v-else>
<el-dropdown trigger="hover" @command="onSearchTypeChange">
<el-tooltip
effect="dark"
placement="top"
:content="currentTreeSearchTypeTooltip"
:open-delay="1000"
>
<span @click.stop="focusTreeSearchInput">
<i class="fa fa-search" />
<span class="search-label">{{ treeSearchTypeLabel }}</span>
<i class="el-icon-arrow-down" />
</span>
</el-tooltip>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-for="(item, type) in treeSearchTypeOptions"
:key="type"
:command="type"
:class="{ 'is-active': treeSearchType === type }"
>
<el-tooltip
effect="dark"
placement="right"
:content="item.tooltip"
:open-delay="300"
>
<span>{{ item.label }}</span>
</el-tooltip>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</template>
<span slot="suffix">
<i
class="el-icon-close"
@@ -47,6 +98,7 @@ import '@ztree/ztree_v3/js/jquery.ztree.exhide.min.js'
import '@/styles/ztree.css'
import '@/styles/ztree_icon.scss'
import axiosRetry from 'axios-retry'
import { setUrlParam } from '@/utils/common'
const defaultObject = {
type: Object,
@@ -66,18 +118,52 @@ export default {
rMenu: '',
init: false,
loading: false,
showTreeSearch: false,
treeSearchValue: ''
showTreeSearch: true,
treeSearchValue: '',
treeSearchType: 'asset',
treeSearchTypeOptions: {},
treeSearchTypeSupportOptions: {
node: {
label: this.$t('Node'),
placeholder: this.$t('Search node'),
tooltip: this.$t('Search by node name'),
search_key: 'search_node'
},
asset: {
label: this.$t('Asset'),
placeholder: this.$t('Search asset'),
tooltip: this.$t('Search by asset name or address'),
search_key: 'search_asset'
}
},
treeType: '' // asset | node
}
},
computed: {
treeSetting() {
return this.setting
},
isSearchTypeDropdownEnabled() {
return Object.keys(this.treeSearchTypeOptions).length > 1
},
currentTreeSearchType() {
return this.treeSearchTypeOptions[this.treeSearchType]
},
currentTreeSearchTypeTooltip() {
return this.currentTreeSearchType?.tooltip || ''
},
treeSearchTypeLabel() {
return this.currentTreeSearchType?.label || ''
},
treeSearchInputPlaceholder() {
return this.currentTreeSearchType?.placeholder || this.$t('Search')
}
},
mounted() {
window.refresh = this.refresh
window.onSearch = this.onSearch
this.initTreeType()
this.initTreeSearchTypeOptions()
this.initTree().then(() => {
this.$nextTick(() => {
this.updateTreeHeight()
@@ -90,6 +176,36 @@ export default {
window.removeEventListener('resize', this.updateTreeHeight)
},
methods: {
initTreeType() {
let treeType = this.treeSetting.treeType
if (!treeType) {
treeType = this.treeSetting.async?.enable ? 'asset' : 'node'
}
this.treeType = treeType
},
initTreeSearchTypeOptions() {
if (this.treeType === 'asset') {
// 资产树支持异步搜索节点和资产
this.treeSearchTypeOptions = this.treeSearchTypeSupportOptions
// 默认搜索资产
this.treeSearchType = 'asset'
} else {
// 节点树只支持搜索节点
this.treeSearchTypeOptions = Object.fromEntries(
Object.entries(this.treeSearchTypeSupportOptions)
.filter(([key]) => key === 'node')
)
// 默认搜索节点
this.treeSearchType = 'node'
}
},
onSearchTypeChange(type) {
this.treeSearchType = type
this.focusTreeSearchInput()
},
focusTreeSearchInput() {
this.$refs.treeSearchInput.focus()
},
onMenuClick(menu) {
if (menu.disabled) {
return
@@ -116,19 +232,14 @@ export default {
const zTreeRect = tree.getBoundingClientRect()
tree.style.height = `calc(100vh - ${zTreeRect.top}px - 30px - 25px)`
}, 100),
async initTree(refresh = false) {
async initTree(refresh = false, iTreeUrl = '') {
const vm = this
let treeUrl
this.loading = true
if (refresh && this.treeSetting.treeUrl.indexOf('/perms/') !== -1 &&
this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1
) {
treeUrl = (this.treeSetting.treeUrl.indexOf('?') === -1)
? `${this.treeSetting.treeUrl}?rebuild_tree=1`
: `${this.treeSetting.treeUrl}&rebuild_tree=1`
} else {
let treeUrl = iTreeUrl
if (!treeUrl) {
treeUrl = this.treeSetting.treeUrl
}
treeUrl = setUrlParam(treeUrl, 'tree_type', this.treeType)
if (refresh) {
$.fn.zTree.destroy(this.iZTreeID)
@@ -221,25 +332,31 @@ export default {
searchInput.oninput = e => this.treeSearchHandle((e.target.value || ''))
},
treeSearchHandle: _.debounce(function(value) {
if (this.treeSetting.async.enable) {
this.filterAssetsServer(value)
if (this.treeSetting.async?.enable) {
this.searchFromServer(value)
} else {
this.filterTree(value)
this.searchFromLocal(value)
}
}, 600),
getCheckedNodes: function() {
return this.zTree.getCheckedNodes(true)
},
recurseParent(node) {
const parentNode = node.getParentNode()
if (parentNode && parentNode.pId) {
return [parentNode, ...this.recurseParent(parentNode)]
} else if (parentNode) {
return [parentNode]
} else {
if (!parentNode) {
return []
}
const allParents = []
if (parentNode) {
allParents.push(parentNode)
if (parentNode.pId) {
allParents.push(...this.recurseParent(parentNode))
}
}
return allParents
},
recurseChildren(node) {
if (!node.isParent) {
return []
@@ -248,49 +365,27 @@ export default {
if (!children) {
return []
}
let allChildren = []
const allChildren = []
children.forEach((n) => {
allChildren = [...children, ...this.recurseChildren(n)]
allChildren.push(n)
allChildren.push(...this.recurseChildren(n))
})
return allChildren
},
groupBy(array, filter) {
const groups = {}
array.forEach(function(o) {
const group = JSON.stringify(filter(o))
groups[group] = groups[group] || []
groups[group].push(o)
})
return Object.keys(groups).map(function(group) {
return groups[group]
})
},
filterTree(keyword, tree = this.zTree) {
searchFromLocal(keyword, tree = this.zTree) {
if (!this.zTree) return
const searchNode = tree.getNodesByFilter((node) => node.id === 'search')
if (searchNode) tree.removeNode(searchNode[0])
const nodes = tree.transformToArray(tree.getNodes())
const allNodes = tree.transformToArray(tree.getNodes())
if (!keyword) {
tree.showNodes(nodes)
tree.showNodes(allNodes)
tree.expandAll(false)
return
}
if (!keyword) {
if (tree.hiddenNodes) {
tree.showNodes(tree.hiddenNodes)
tree.hiddenNodes = null
}
if (tree.expandNodes) {
tree.expandNodes.forEach((node) => {
if (node.id !== nodes[0].id) {
tree.expandNode(node, false)
}
})
tree.expandNodes = null
}
return null
}
let shouldShow = []
const matchedNodes = tree.getNodesByFilter((node) => {
return node.name.toLowerCase().indexOf(keyword.toLowerCase()) > -1
})
@@ -300,67 +395,54 @@ export default {
const assetsAmount = matchedNodes.length
name = `${name} (${assetsAmount})`
const newNode = { id: 'search', name: name, isParent: false, open: false }
tree.addNodes(null, newNode)
const addedNodes = tree.addNodes(null, newNode)
// 隐藏所有节点,只显示搜索节点
tree.hideNodes(allNodes)
tree.showNodes(addedNodes)
return
}
// 获取应该展示的节点,以及应该展开的节点
let shouldShow = []
let shouldExpandNodes = []
let shouldCollapseNodes = []
matchedNodes.forEach((node) => {
const parents = this.recurseParent(node)
const children = this.recurseChildren(node)
shouldShow = [...shouldShow, ...parents, ...children, node]
})
// 应该显示匹配节点本身、其祖先节点和子孙节点
shouldShow.push(node)
shouldShow.push(...parents)
shouldShow.push(...children)
tree.hiddenNodes = nodes
tree.expandNodes = shouldShow
tree.hideNodes(nodes)
// 应该展开匹配节点的父节点,不展开匹配节点的子孙节点
shouldExpandNodes.push(...parents)
// 应该折叠匹配节点的子孙节点
shouldCollapseNodes.push(node)
shouldCollapseNodes.push(...children)
})
shouldShow = Array.from(new Set(shouldShow))
shouldExpandNodes = Array.from(new Set(shouldExpandNodes))
shouldCollapseNodes = Array.from(new Set(shouldCollapseNodes))
// 隐藏所有节点,显示应该显示的节点
tree.hideNodes(allNodes)
tree.showNodes(shouldShow)
for (const node of shouldShow) {
if (node.isParent) {
tree.expandNode(node, true)
}
// 展开应该展开的节点
for (const node of shouldExpandNodes) {
tree.expandNode(node, true)
}
// 折叠应该折叠的节点
for (const node of shouldCollapseNodes) {
tree.expandNode(node, false)
}
},
filterAssetsServer(keyword) {
if (!this.zTree) return
let searchNode = this.zTree.getNodesByFilter((node) => node.id === 'search')
if (searchNode) {
this.zTree.removeChildNodes(searchNode[0])
this.zTree.removeNode(searchNode[0])
}
const treeNodes = this.zTree.getNodes()
if (!keyword) {
if (treeNodes.length !== 0) {
this.zTree.showNodes(treeNodes)
}
return
}
if (treeNodes.length !== 0) {
this.zTree.hideNodes(treeNodes)
}
let treeUrl = this.treeSetting.searchUrl ? this.treeSetting.searchUrl : this.treeSetting.treeUrl
const filterField = treeUrl.includes('?') ? `&search=${keyword}` : `?search=${keyword}`
if (treeUrl.indexOf('assets/nodes/children/tree') > -1) {
treeUrl = treeUrl + '&all=all'
}
const searchUrl = `${treeUrl}${filterField}`
this.$axios.get(searchUrl).then(nodes => {
let name = this.$t('Search')
const assetsAmount = nodes.length
name = `${name} (${assetsAmount})`
const newNode = { id: 'search', name: name, isParent: true, open: true, zAsync: true }
searchNode = this.zTree.addNodes(null, newNode)[0]
searchNode.zAsync = true
this.rootNodeAddDom(searchNode)
const nodesGroupByOrg = this.groupBy(nodes, (node) => {
return node.meta?.data?.org_name
})
for (const item of nodesGroupByOrg) {
this.zTree.addNodes(searchNode, item)
}
searchNode.open = true
})
searchFromServer(keyword) {
// 直接用搜索 API 返回的数据重新初始化树
const treeUrl = this.treeSetting.searchUrl ? this.treeSetting.searchUrl : this.treeSetting.treeUrl
const searchTypeKey = this.treeSearchTypeOptions[this.treeSearchType]?.search_key || 'search'
const searchUrl = setUrlParam(treeUrl, searchTypeKey, keyword)
this.initTree(true, searchUrl)
}
}
@@ -624,12 +706,19 @@ div.rMenu li {
.fixed-tree-search {
margin-bottom: 10px;
border: 1px solid;
border-radius: 3px;
&:hover,
&:focus-within {
border-color: var(--color-primary);
}
& ::v-deep .el-input__inner {
border-radius: 4px;
border: none;
background: #fafafa;
padding-right: 32px;
color: var(--color-text-primary)
color: var(--color-text-primary);
}
& ::v-deep .el-input__suffix {
@@ -653,6 +742,37 @@ div.rMenu li {
& ::v-deep .el-input__suffix-inner {
line-height: 30px;
}
& ::v-deep .el-input-group__prepend {
padding-left: 5px;
padding-right: 3px;
border: none;
color: #999;
* {
color: inherit;
}
align-items: center;
background: #fafafa;
.el-icon-arrow-down {
display: inline-block;
transition: transform 0.8s ease; /* 动画关键 */
}
:hover {
.el-icon-arrow-down {
transform: rotate(180deg); /* 顺时针 180° */
}
}
.search-label {
margin-left: 1px;
margin-right: 1px;
}
}
}
::v-deep .el-dropdown-menu__item.is-active {
color: var(--color-primary);
font-weight: 500;
}
.icon-refresh {

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free v5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M512 176.001C512 273.203 433.202 352 336 352c-11.22 0-22.19-1.062-32.827-3.069l-24.012 27.014A23.999 23.999 0 0 1 261.223 384H224v40c0 13.255-10.745 24-24 24h-40v40c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24v-78.059c0-6.365 2.529-12.47 7.029-16.971l161.802-161.802C163.108 213.814 160 195.271 160 176 160 78.798 238.797.001 335.999 0 433.488-.001 512 78.511 512 176.001zM336 128c0 26.51 21.49 48 48 48s48-21.49 48-48-21.49-48-48-48-48 21.49-48 48z"/></svg>

Before

Width:  |  Height:  |  Size: 691 B

View File

@@ -123,16 +123,6 @@ export default {
permissions: ['authentication.view_connectiontoken']
}
},
{
path: '/profile/access-token',
component: () => import('@/views/profile/AccessToken'),
name: 'AccessToken',
meta: {
title: i18n.t('AccessToken'),
icon: 'access-token',
permissions: ['oauth2_provider.view_accesstoken']
}
},
{
path: '/profile/preferences',
name: 'Preferences',

View File

@@ -150,11 +150,10 @@ export function getErrorResponseMsg(error) {
} else if (typeof data === 'string') {
return data
} else if (_.isPlainObject(data)) {
const msg = Object.values(data)
return Object.values(data)
.map(item => getErrorResponseMsg(item))
.filter(i => i)
// 错误信息不要重复提示
return [...new Set(msg)].join('; ')
.join('; ')
} else {
msg = error.toString()
}
@@ -424,28 +423,6 @@ export function getDrawerWidth() {
return '90%'
}
export function getShowCurrentAssetValue(cookie, defaultValue = '0') {
const stored = typeof window !== 'undefined'
? window.localStorage.getItem('show_current_asset')
: null
if (stored === '0' || stored === '1') {
return stored
}
if (cookie && typeof cookie.get === 'function') {
return cookie.get('show_current_asset') || defaultValue
}
return defaultValue
}
export function setShowCurrentAssetValue(cookie, value) {
if (typeof window !== 'undefined') {
window.localStorage.setItem('show_current_asset', String(value))
}
if (cookie && typeof cookie.set === 'function') {
cookie.set('show_current_asset', value, 1)
}
}
export class ObjectLocalStorage {
constructor(key, attr) {
this.key = key

View File

@@ -92,7 +92,7 @@ export function encryptPassword(password) {
rsaPublicKeyText = rsaPublicKeyText.replaceAll('"', '')
const rsaPublicKey = atob(rsaPublicKeyText)
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey)
const passwordCipher = aesEncrypt(String(password), aesKey)
const passwordCipher = aesEncrypt(password, aesKey)
return `${keyCipher}:${passwordCipher}`
}

View File

@@ -38,6 +38,11 @@ export default {
value: this.object.snapshot.node_amount
},
'trigger_display', 'date_start', 'date_finished',
{
key: this.$t('MailRecipient'),
value: this.object.recipients ? this.object.recipients.map(
i => `${i[0]}` + `${i[1] ? ': ' + this.$t('ContainAttachment') : ''}`).join(', ') : ''
},
{
key: this.$t('Comment'),
value: this.object.snapshot.common

View File

@@ -5,12 +5,7 @@
</el-alert>
<TwoCol>
<template>
<GenericListTable
ref="listTable"
:detail-drawer="detailDrawer"
:header-actions="headerActions"
:table-config="tableConfig"
/>
<GenericListTable ref="listTable" :header-actions="headerActions" :table-config="tableConfig" />
</template>
<template #right>
<QuickActions :actions="quickActions" type="primary" />
@@ -26,13 +21,12 @@
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import { QuickActions } from '@/components'
import { openTaskPage } from '@/utils/jms/index'
import { GenericListTable } from '@/layout/components'
import { ActionsFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
import ViewSecret from '@/components/Apps/AccountListTable/ViewSecret'
import { openTaskPage } from '@/utils/jms/index'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
export default {
name: 'AccountTemplateChangeSecret',
@@ -52,7 +46,6 @@ export default {
data() {
const vm = this
return {
detailDrawer: () => import('@/views/accounts/AccountDiscover/TaskDetail/index.vue'),
visible: false,
secretUrl: '',
showViewSecretDialog: false,
@@ -65,20 +58,20 @@ export default {
},
callbacks: Object.freeze({
click: () => {
this.$axios
.patch(
`/api/v1/accounts/account-templates/${this.object.id}/sync-related-accounts/`
)
.then(res => {
openTaskPage(res['task'])
})
this.$axios.patch(
`/api/v1/accounts/account-templates/${this.object.id}/sync-related-accounts/`
).then(res => {
openTaskPage(res['task'])
})
}
})
}
],
tableConfig: {
url: `/api/v1/accounts/accounts/?source_id=${this.object.id}`,
columns: ['name', 'asset', 'secret_type', 'is_active', 'date_created'],
columns: [
'name', 'asset', 'secret_type', 'is_active', 'date_created'
],
columnsMeta: {
name: {
formatter: DetailFormatter,
@@ -86,9 +79,8 @@ export default {
drawer: true,
can: vm.$hasPerm('accounts.view_account'),
getRoute: ({ row }) => {
this.detailDrawer = () => import('@/views/accounts/Account/AccountDetail/index.vue')
return {
name: 'AccountDetail',
name: 'AssetAccountDetail',
params: { id: row.id }
}
}
@@ -102,7 +94,6 @@ export default {
can: vm.$hasPerm('assets.view_asset'),
getTitle: ({ row }) => row.asset.name,
getRoute: ({ row }) => {
this.detailDrawer = () => import('@/views/assets/Asset/AssetDetail')
return {
name: 'AssetDetail',
params: { id: row.asset.id }

View File

@@ -137,8 +137,8 @@ export default {
getUrlQuery: true,
options: [
{
label: this.$t('ID'),
value: 'id'
label: this.$t('TaskID'),
value: 'automation_id'
},
{
label: this.$t('DisplayName'),

View File

@@ -61,7 +61,7 @@ export default {
updateSuccessNextRoute: this.updateSuccessNextRoute,
hasDetailInMsg: false,
fields: [
[this.$t('Basic'), ['name', 'address', 'platform', 'nodes']],
[this.$t('Basic'), ['name', 'address', 'platform', 'node']],
[this.$t('Protocol'), ['protocols']],
[this.$t('Account'), ['accounts']],
[this.$t('Other'), ['directory_services', 'zone', 'labels', 'is_active', 'comment']]
@@ -73,8 +73,8 @@ export default {
const values = _.cloneDeep(validValues)
const submitMethod = id ? 'put' : 'post'
if (values.nodes && values.nodes.length === 0) {
delete values['nodes']
if (!values.node) {
delete values['node']
}
if (submitMethod === 'put') {
@@ -143,15 +143,14 @@ export default {
},
async setInitial() {
const { defaultConfig } = this
const { node } = this.$route.query
const nodesInitial = node ? [node] : []
const { node_id } = this.$route.query
const platformId = this.platformID || 'Linux'
const url = `/api/v1/assets/platforms/${platformId}/`
this.platform = await this.$axios.get(url)
const initial = {
labels: [],
is_active: true,
nodes: nodesInitial,
node: node_id,
platform: parseInt(this.platform.id),
protocols: []
}

View File

@@ -61,11 +61,8 @@ export default {
},
callbacks: {
change: function(val) {
const category = this.object.category.value
const normalizedCategory =
category === 'ds' ? 'directorie' : category
this.$axios.patch(
`/api/v1/assets/${normalizedCategory}s/${this.object.id}/`,
`/api/v1/assets/assets/${this.object.id}/`,
{ is_active: val }
).then(res => {
this.$message.success(this.$tc('UpdateSuccessMsg'))

View File

@@ -28,12 +28,7 @@ import { mapGetters } from 'vuex'
import TreeMenu from './components/TreeMenu'
import BaseList from './components/BaseList'
import $ from '@/utils/jquery-vendor'
import {
getShowCurrentAssetValue,
setShowCurrentAssetValue,
setRouterQuery,
setUrlParam
} from '@/utils/common/index'
import { setRouterQuery, setUrlParam } from '@/utils/common/index'
export default {
components: {
@@ -51,6 +46,9 @@ export default {
url: '/api/v1/assets/assets/',
showMenu: !this.$store.getters.currentOrgIsRoot,
showDefaultMenu: true,
async: {
enable: false
},
menu: [
]
},
@@ -78,7 +76,7 @@ export default {
},
methods: {
decorateRMenu() {
const show_current_asset = getShowCurrentAssetValue(this.$cookie)
const show_current_asset = this.$cookie.get('show_current_asset') || '0'
if (show_current_asset === '1') {
$('#m_show_asset_all_children_node').css('color', '#606266')
$('#m_show_asset_only_current_node').css('color', 'green')
@@ -88,7 +86,7 @@ export default {
}
},
showAll({ node, showCurrentAsset }) {
setShowCurrentAssetValue(this.$cookie, showCurrentAsset)
this.$cookie.set('show_current_asset', showCurrentAsset, 1)
this.decorateRMenu()
const url = `${this.treeSetting.url}?node_id=${node.meta.data.id}&show_current_asset=${showCurrentAsset}`
this.$refs.AssetTreeTable.$refs.TreeList.handleUrlChange(url)

View File

@@ -77,7 +77,7 @@ export default {
[this.$tc('Platform'), 'platform'],
[this.$tc('Node'), 'node'],
[this.$tc('Protocol'), 'protocols'],
[this.$tc('Region'), 'region_name']
[this.$tc('Region'), 'region_id']
],
data: []
},
@@ -124,7 +124,7 @@ export default {
this.ws.onmessage = e => {
const data = JSON.parse(e.data)
if (data.action === 'sync_region') {
this.addRegion(data.id, data.name)
this.addRegion(data.region_id)
} else if (data.action === 'import') {
data['@status'] = 'pending'
this.$refs.importTable.addTableItem(data)
@@ -140,10 +140,10 @@ export default {
}
}
},
addRegion(regionId, regionName) {
if (!this.alreadySync.includes(regionId)) {
this.alreadySync.push(regionId)
this.tip = `${this.$t('SyncRegion')}: ${regionName}`
addRegion(region) {
if (!this.alreadySync.includes(region)) {
this.alreadySync.push(region)
this.tip = `${this.$t('SyncRegion')}: ${this.alreadySync.at(-1)}`
}
},
showResult() {

View File

@@ -17,8 +17,6 @@ export const ucloud = 'ucloud'
export const volcengine = 'volcengine'
export const ctyun = 'ctyun'
export const qingcloud_private = 'qingcloud_private'
export const huaweicloud_private = 'huaweicloud_private'
export const ctyun_private = 'ctyun_private'
@@ -48,8 +46,7 @@ export const publicHostProviders = [
gcp,
ucloud,
volcengine,
smartx,
ctyun
smartx
]
export const publicDBProviders = [aliyun]
@@ -144,12 +141,6 @@ export const ACCOUNT_PROVIDER_ATTRS_MAP = {
attrs: ['access_key_id', 'access_key_secret'],
image: require('@/assets/img/cloud/volcengine.svg')
},
[ctyun]: {
name: ctyun,
title: i18n.t('CTYun'),
attrs: ['access_key_id', 'access_key_secret', 'project_id'],
image: require('@/assets/img/cloud/state.svg')
},
[vmware]: {
name: vmware,
title: 'VMware',

View File

@@ -148,9 +148,10 @@ export const assetFieldsMeta = (vm, category, type) => {
return vm.platform.ds_enabled === false
}
},
nodes: {
node: {
rules: [rules.RequiredChange],
el: {
multiple: false,
ajax: {
url: '/api/v1/assets/nodes/',
transformOption: item => {

View File

@@ -8,6 +8,7 @@ export const UserAssetPermissionListPageSearchConfigOptions = [
{ label: i18n.t('UserGroups'), value: 'user_group' },
{ label: i18n.t('AssetName'), value: 'asset_name' },
{ label: i18n.t('AssetAddress'), value: 'address' },
{ label: i18n.t('Account'), value: 'accounts' },
{
label: i18n.t('Valid'),
value: 'is_valid',

View File

@@ -1,77 +0,0 @@
<template>
<GenericListPage
ref="GenericListTable"
:header-actions="headerActions"
:help-tip="helpMessage"
:table-config="tableConfig"
/>
</template>
<script>
import { GenericListPage } from '@/layout/components'
export default {
components: {
GenericListPage
},
data() {
const ajaxUrl = '/api/v1/authentication/access-tokens/'
return {
helpMessage: this.$t('AccessTokenTip'),
tableConfig: {
hasSelection: false,
url: ajaxUrl,
columns: [
'token_preview', 'scope', 'is_valid', 'expires', 'updated', 'created', 'actions'
],
columnsMeta: {
actions: {
prop: '',
formatterArgs: {
hasUpdate: false,
hasClone: false,
hasDelete: false,
extraActions: [
{
name: 'Revoke',
title: this.$t('Revoke'),
can: ({ row }) => this.$hasPerm('oauth2_provider.delete_accesstoken'),
type: 'info',
callback: function({ row }) {
this.$axios.delete(`${ajaxUrl}${row.id}/revoke/`,
).then(res => {
this.reloadTable()
this.$message.success(this.$tc('UpdateSuccessMsg'))
}).catch(error => {
this.$message.error(this.$tc('UpdateErrorMsg' + ' ' + error))
})
}.bind(this)
}
]
}
}
}
},
headerActions: {
hasLeftActions: false,
hasSearch: false,
hasRightActions: true,
hasRefresh: true,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasCreate: false,
extraActions: []
}
}
},
methods: {
reloadTable() {
this.$refs.GenericListTable.reloadTable()
}
}
}
</script>
<style scoped>
</style>

View File

@@ -96,6 +96,7 @@ export default {
{
title: this.$t('FacialFeatures'),
has: this.$store.getters.publicSettings.FACE_RECOGNITION_ENABLED &&
this.$store.getters.publicSettings.XPACK_LICENSE_EDITION_ULTIMATE &&
!store.getters.publicSettings['PRIVACY_MODE'],
attrs: {
type: 'primary',

View File

@@ -1,41 +1,36 @@
<template>
<TwoCol>
<template>
<Account :url="url" :columns-meta="columnsMeta" :object.sync="object" />
</template>
<template #right>
<QuickActions :actions="quickActions" type="primary" />
</template>
</TwoCol>
<Account
:url="url"
:columns-meta="columnsMeta"
:extra-quick-actions="quickActions"
:object.sync="object"
/>
</template>
<script>
import { QuickActions } from '@/components'
import Account from '@/views/assets/Asset/AssetDetail/Account'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
export default {
name: 'Accounts',
components: {
TwoCol,
Account,
QuickActions
Account
},
props: {
object: {
type: Object,
default: () => {}
default: () => {
}
}
},
data() {
return {
columnsMeta: {
name: {
formatter: row => <span>{row.name}</span>
formatter: (row) => <span>{row.name}</span>
},
asset: {
label: this.$t('Asset'),
formatter: row => <span>{row.asset.name}</span>
formatter: (row) => <span>{row.asset.name}</span>
}
},
quickActions: [

View File

@@ -29,7 +29,7 @@ export default {
'CAS_RENAME_ATTRIBUTES'
]],
[this.$t('Other'), [
'CAS_ORG_IDS', 'CAS_LOGOUT_COMPLETELY'
'CAS_ORG_IDS', 'CAS_CREATE_USER', 'CAS_LOGOUT_COMPLETELY'
]]
],
fieldsMeta: {

View File

@@ -10,6 +10,7 @@
import { GenericCreateUpdateForm } from '@/layout/components'
import IBox from '@/components/Common/IBox/index.vue'
import { mapGetters } from 'vuex'
import ChatProvidersField from './components/ChatProvidersField.vue'
export default {
components: {
@@ -17,118 +18,54 @@ export default {
GenericCreateUpdateForm
},
data() {
const vm = this
const hasProviders = (formValue) => {
const providers = formValue?.CHAT_AI_PROVIDERS?.providers || []
return Array.isArray(providers) && providers.length > 0
}
return {
url: '/api/v1/settings/setting/?category=chat',
hasReset: false,
moreButtons: [
{
title: this.$t('Test'),
loading: false,
callback: function(value, form, btn) {
btn.loading = true
vm.$axios.post(
'/api/v1/settings/chatai/testing/',
value
).then(res => {
vm.$message.success(res['msg'])
}).catch(() => {
vm.$log.error('err occur')
}).finally(() => {
btn.loading = false
})
}
}
],
encryptedFields: ['VAULT_HCP_TOKEN'],
fields: [
'CHAT_AI_ENABLED',
'CHAT_AI_METHOD',
'CHAT_AI_EMBED_URL',
'CHAT_AI_TYPE',
'DEEPSEEK_BASE_URL',
'DEEPSEEK_API_KEY',
'DEEPSEEK_PROXY',
'DEEPSEEK_MODEL',
'GPT_BASE_URL',
'GPT_API_KEY',
'GPT_PROXY',
'GPT_MODEL',
'CUSTOM_GPT_MODEL',
'CUSTOM_DEEPSEEK_MODEL'
'CHAT_AI_PROVIDERS',
'CHAT_AI_EMBED_URL'
],
fieldsMeta: {
CHAT_AI_TYPE: {
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api'
return formValue.CHAT_AI_METHOD !== 'api' || hasProviders(formValue)
}
},
GPT_BASE_URL: {
el: {
autocomplete: 'new-password'
},
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'gpt'
}
},
GPT_API_KEY: {
el: {
autocomplete: 'new-password'
},
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'gpt'
}
},
GPT_PROXY: {
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'gpt'
}
},
GPT_MODEL: {
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'gpt'
}
},
DEEPSEEK_BASE_URL: {
el: {
autocomplete: 'new-password'
},
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'deep-seek'
}
},
DEEPSEEK_API_KEY: {
el: {
autocomplete: 'new-password'
},
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'deep-seek'
}
},
DEEPSEEK_PROXY: {
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'deep-seek'
}
},
DEEPSEEK_MODEL: {
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'deep-seek'
}
CHAT_AI_PROVIDERS: {
component: ChatProvidersField,
hidden: (formValue) => formValue.CHAT_AI_METHOD !== 'api'
},
CHAT_AI_EMBED_URL: {
hidden: (formValue) => formValue.CHAT_AI_METHOD !== 'embed'
},
CUSTOM_GPT_MODEL: {
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'gpt' || formValue.GPT_MODEL !== 'custom'
}
},
CUSTOM_DEEPSEEK_MODEL: {
hidden: (formValue) => {
return formValue.CHAT_AI_METHOD !== 'api' || formValue.CHAT_AI_TYPE !== 'deep-seek' || formValue.DEEPSEEK_MODEL !== 'custom'
}
},
afterGetFormValue(formValue) {
const providers = Array.isArray(formValue.CHAT_AI_PROVIDERS) ? formValue.CHAT_AI_PROVIDERS : []
return {
...formValue,
CHAT_AI_PROVIDERS: {
providers: providers
}
}
},
cleanFormValue(values) {
const config = values.CHAT_AI_PROVIDERS || {}
const providers = Array.isArray(config.providers) ? config.providers : []
return {
...values,
CHAT_AI_PROVIDERS: providers,
CHAT_AI_TYPE: values.CHAT_AI_TYPE || providers[0]?.type || values.CHAT_AI_TYPE
}
},
submitMethod() {
return 'patch'
}

View File

@@ -0,0 +1,186 @@
<template>
<div class="providers-card">
<div class="providers-header">
<el-button size="mini" type="primary" icon="el-icon-plus" @click="addProvider">
{{ $t('Add') }}
</el-button>
</div>
<el-table :data="localProviders" size="mini" border>
<el-table-column :label="$t('Type')" width="140">
<template #default="{ row }">
<el-select v-model="row.type" size="mini" @change="emitChange">
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column :label="$t('Base URL')" min-width="200">
<template #default="{ row }">
<el-input
v-model="row.base_url"
size="mini"
:placeholder="openAIBaseUrl"
@input="emitChange"
/>
</template>
</el-table-column>
<el-table-column :label="$t('API Key')" min-width="180">
<template #default="{ row }">
<el-input
v-model="row.api_key"
size="mini"
show-password
autocomplete="new-password"
@input="emitChange"
/>
</template>
</el-table-column>
<el-table-column :label="$t('Proxy')" min-width="160">
<template #default="{ row }">
<el-input
v-model="row.proxy"
size="mini"
placeholder="http://ip:port"
@input="emitChange"
/>
</template>
</el-table-column>
<el-table-column :label="$t('Actions')" width="100" align="center">
<template #default="{ $index }">
<el-button
size="mini"
type="text"
class="danger-text"
icon="el-icon-delete"
@click="removeProvider($index)"
/>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import cloneDeep from 'lodash/cloneDeep'
export default {
name: 'ChatProvidersField',
props: {
value: {
type: [Object, Array],
default: () => ({ providers: [], defaultProvider: '' })
},
typeOptions: {
type: Array,
default: () => ([
{ label: 'Ollama', value: 'ollama' },
{ label: 'OpenAI', value: 'openai' }
])
}
},
data() {
const { providers, defaultProvider } = this.normalizeValue(this.value)
return {
localProviders: providers,
defaultProvider: this.pickDefault(defaultProvider, providers),
openAIBaseUrl: 'https://api.openai.com/v1'
}
},
watch: {
value: {
handler(v) {
const { providers, defaultProvider } = this.normalizeValue(v)
this.localProviders = providers
this.defaultProvider = this.pickDefault(defaultProvider, providers) || ''
},
deep: true
}
},
methods: {
emptyProvider() {
return {
type: 'openai',
base_url: this.openAIBaseUrl,
api_key: '',
proxy: ''
}
},
normalizeValue(v) {
if (!v) {
return { providers: [], defaultProvider: '' }
}
// allow legacy array value
if (Array.isArray(v)) {
return { providers: cloneDeep(v), defaultProvider: '' }
}
const providers = Array.isArray(v.providers) ? cloneDeep(v.providers) : []
return { providers, defaultProvider: v.defaultProvider || '' }
},
pickDefault(current, providers) {
const enabledProviders = providers.filter(item => item?.enabled !== false)
if (current && providers.some(item => item.name === current)) {
return current
}
return enabledProviders[0]?.name || providers[0]?.name || ''
},
emitChange() {
const providers = cloneDeep(this.localProviders || [])
const defaultProvider = this.pickDefault(this.defaultProvider, providers)
this.defaultProvider = defaultProvider
this.$emit('input', { providers, defaultProvider })
this.$emit('change', { providers, defaultProvider })
},
handleAssistantChange(current) {
this.emitChange()
},
addProvider() {
this.localProviders.push(this.emptyProvider())
this.emitChange()
},
removeProvider(index) {
this.localProviders.splice(index, 1)
this.emitChange()
},
setDefault(name) {
this.defaultProvider = name
this.emitChange()
}
}
}
</script>
<style scoped lang="scss">
.providers-card {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px;
}
.providers-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
.title {
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.tip {
color: #909399;
font-weight: 400;
font-size: 12px;
}
}
.danger-text {
color: #f56c6c;
}
</style>

View File

@@ -6,7 +6,6 @@
:create-drawer="createDrawer"
:detail-drawer="detailDrawer"
:header-actions="iTicketAction"
:quick-filters="quickFilters"
:table-config="ticketTableConfig"
/>
</template>
@@ -42,77 +41,11 @@ export default {
loading: true,
getDrawerTitle: () => ' ',
createDrawer: () => import('@/views/tickets/RequestAssetPerm/CreateUpdate'),
quickFilters: [
{
label: this.$t('Type'),
options: [
{
label: this.$t('ApplyAsset'),
filter: {
type: 'apply_asset'
}
},
{
label: this.$t('LoginConfirm'),
filter: {
type: 'login_confirm'
}
},
{
label: this.$t('CommandConfirm'),
filter: {
type: 'command_confirm'
}
},
{
label: this.$t('LoginAssetConfirm'),
filter: {
type: 'login_asset_confirm'
}
}
]
},
{
label: this.$t('State'),
options: [
{
label: this.$t('All'),
filter: {
state: 'all'
}
},
{
label: this.$t('Open'),
filter: {
state: 'pending'
}
},
{
label: this.$t('Cancel'),
filter: {
state: 'closed'
}
},
{
label: this.$t('Approved'),
filter: {
state: 'approved'
}
},
{
label: this.$t('Rejected'),
filter: {
state: 'rejected'
}
}
]
}
],
detailDrawer: null,
ticketTableConfig: {
url: this.url,
extraQuery: this.extraQuery,
columnsExclude: ['process_map', 'rel_snapshot', 'status'],
columnsExclude: ['process_map', 'rel_snapshot'],
columnsShow: {
min: ['title', 'serial_num', 'type', 'state', 'date_created'],
default: ['title', 'serial_num', 'type', 'state', 'date_created']
@@ -161,8 +94,25 @@ export default {
return row.type.label
}
},
status: {
align: 'center',
sortable: 'custom',
formatter: TagChoicesFormatter,
formatterArgs: {
getTagLabel({ row }) {
return row.status.label
},
getTagType({ row }) {
if (row.status.value === 'open') {
return 'primary'
} else {
return 'danger'
}
}
}
},
state: {
label: this.$t('State'),
label: this.$t('Action'),
align: 'center',
sortable: 'custom',
formatter: TagChoicesFormatter,
@@ -192,7 +142,7 @@ export default {
}
},
defaultTicketActions: {
hasImport: false,
hasExport: false,
hasMoreActions: false,
hasLeftActions: true,
canCreate: this.$hasPerm('tickets.view_ticket'),
@@ -239,6 +189,10 @@ export default {
value: 'relevant_asset',
label: this.$t('RelevantAsset')
},
{
value: 'relevant_system_user',
label: this.$t('RelevantCommand')
},
{
value: 'relevant_command',
label: this.$t('ApplyRunCommand')

View File

@@ -217,14 +217,12 @@ export default {
const accounts = this.requestForm.accounts
if (this.object.approval_step.value === this.object.process_map.length) {
if (assets.length === 0 && nodes.length === 0) {
this.$message.error(this.$tc('SelectAtLeastOneAssetOrNodeErrMsg'))
return false
return this.$message.error(this.$tc('SelectAtLeastOneAssetOrNodeErrMsg'))
} else if (accounts.length === 0) {
this.$message.error(this.$tc('RequiredSystemUserErrMsg'))
return false
return this.$message.error(this.$tc('RequiredSystemUserErrMsg'))
}
}
return this.$axios.patch(`/api/v1/tickets/apply-asset-tickets/${this.object.id}/approve/`, {
this.$axios.patch(`/api/v1/tickets/apply-asset-tickets/${this.object.id}/approve/`, {
apply_nodes: nodes || [],
apply_assets: assets || [],
apply_accounts: accounts || [],
@@ -241,11 +239,11 @@ export default {
},
handleClose() {
const url = `/api/v1/tickets/apply-asset-tickets/${this.object.id}/close/`
return this.$axios.put(url).then(res => this.reloadPage()).catch(err => this.$message.error(err))
this.$axios.put(url).then(res => this.reloadPage()).catch(err => this.$message.error(err))
},
handleReject() {
const url = `/api/v1/tickets/apply-asset-tickets/${this.object.id}/reject/`
return this.$axios.put(url).then(res => this.reloadPage()).catch(err => this.$message.error(err))
this.$axios.put(url).then(res => this.reloadPage()).catch(err => this.$message.error(err))
}
}
}

View File

@@ -231,16 +231,7 @@ export default {
}
if (handler) {
const result = handler()
if (result === false) {
this.isDisabled = false
return
}
if (result && typeof result.finally === 'function') {
result.finally(() => {
this.isDisabled = false
})
}
handler()
} else {
this.$message.error('No handler for action')
}

View File

@@ -264,7 +264,7 @@ export default {
let mfa_level = null
// SECURITY_MFA_AUTH 0 不开启 1 全局开启 2 管理员开启
const securityMFAAuth = store.getters.publicSettings['SECURITY_MFA_AUTH']
const adminUserIsNeed = (user?.is_superuser || user?.is_org_admin) && this.$route.params.action === 'update' &&
const adminUserIsNeed = (user?.is_superuser || user?.is_org_admin) && this.$route.meta.action === 'update' &&
securityMFAAuth === MFASystemSetting.onlyAdminUsers
if (securityMFAAuth === MFASystemSetting.allUsers) {
options = [{ 'value': MFALevel.allUsers, 'label': this.$t('MFAAllUsers') }]

View File

@@ -19,7 +19,7 @@ export default {
data() {
return {
treeUrl: `/api/v1/perms/users/${this.object.id}/nodes/children/tree/`,
tableUrl: `/api/v1/perms/users/${this.object.id}/assets/?all=1`,
tableUrl: `/api/v1/perms/users/${this.object.id}/assets/`,
actions: {
has: false
},

View File

@@ -20,8 +20,6 @@ import { QuickActions } from '@/components'
import RelationCard from '@/components/Cards/RelationCard'
import AutoDetailCard from '@/components/Cards/DetailCard/auto'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
import store from '@/store'
import { MFASystemSetting } from '@/views/users/const'
export default {
name: 'UserInfo',
@@ -228,22 +226,7 @@ export default {
return <div>{doms}</div>
}
},
'wecom_id', 'dingtalk_id', 'feishu_id',
{
key: this.$t('MFA'),
formatter: (item, val) => {
const user = vm.object
const securityMFAAuth = store.getters.publicSettings['SECURITY_MFA_AUTH']
const adminUserIsNeed = (user?.is_superuser || user?.is_org_admin) && securityMFAAuth === MFASystemSetting.onlyAdminUsers
if (securityMFAAuth === MFASystemSetting.allUsers) {
return this.$t('MFAAllUsers')
}
if (securityMFAAuth === MFASystemSetting.onlyAdminUsers && adminUserIsNeed) {
return this.$t('MFAOnlyAdminUsers')
}
return user?.mfa_level.label
}
},
'wecom_id', 'dingtalk_id', 'feishu_id', 'mfa_level',
'source', 'labels',
'created_by', 'date_joined', 'date_expired',
'date_password_last_updated', 'last_login', 'comment'

View File

@@ -1069,6 +1069,11 @@
resolved "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@socket.io/component-emitter@~3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
"@soda/friendly-errors-webpack-plugin@^1.7.1":
version "1.8.1"
resolved "https://registry.npmmirror.com/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz#4d4fbb1108993aaa362116247c3d18188a2c6c85"
@@ -4767,6 +4772,13 @@ debug@^3.1.0, debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@~4.3.1, debug@~4.3.2:
version "4.3.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
dependencies:
ms "^2.1.3"
debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
@@ -5301,6 +5313,22 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
engine.io-client@~6.6.1:
version "6.6.3"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.6.3.tgz#815393fa24f30b8e6afa8f77ccca2f28146be6de"
integrity sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
engine.io-parser "~5.2.1"
ws "~8.17.1"
xmlhttprequest-ssl "~2.1.1"
engine.io-parser@~5.2.1:
version "5.2.3"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f"
integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==
enhanced-resolve@^4.1.0, enhanced-resolve@^4.5.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec"
@@ -12884,6 +12912,24 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
socket.io-client@^4.8.1:
version "4.8.1"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.8.1.tgz#1941eca135a5490b94281d0323fe2a35f6f291cb"
integrity sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.2"
engine.io-client "~6.6.1"
socket.io-parser "~4.2.4"
socket.io-parser@~4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
sockjs-client@^1.5.0:
version "1.6.1"
resolved "https://registry.npmmirror.com/sockjs-client/-/sockjs-client-1.6.1.tgz#350b8eda42d6d52ddc030c39943364c11dcad806"
@@ -14208,16 +14254,16 @@ utils-merge@1.0.1:
resolved "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@8.3.2, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v-sanitize@^0.0.13:
version "0.0.13"
resolved "https://registry.npmmirror.com/v-sanitize/-/v-sanitize-0.0.13.tgz#d98e3eee3277a2b3a632bc2ba119c088967da0c4"
@@ -14940,11 +14986,21 @@ ws@^6.0.0, ws@^6.2.1:
dependencies:
async-limiter "~1.0.0"
ws@~8.17.1:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
xmlhttprequest-ssl@~2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz#e9e8023b3f29ef34b97a859f584c5e6c61418e23"
integrity sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==
xss@^1.0.14, xss@^1.0.9:
version "1.0.15"
resolved "https://registry.npmmirror.com/xss/-/xss-1.0.15.tgz#96a0e13886f0661063028b410ed1b18670f4e59a"