Compare commits

...

49 Commits

Author SHA1 Message Date
Ewall555
1e678a81ea feat: Asset permossion add user selection component and its dialog 2025-06-19 07:10:11 +00:00
feng
eb4aebf637 perf: Translate 2025-06-19 11:52:26 +08:00
ibuler
91eb4b1c0d perf: some create title 2025-06-18 20:17:55 +08:00
Eric
8614798362 perf: fix loading when upload applet or virtualapp 2025-06-18 18:56:49 +08:00
Eric
405054034b perf: fix chat action name i18n 2025-06-18 18:56:12 +08:00
w940853815
d8766e1bdb Revert "fix: A scroll bar appears in the asset tree on the left side of the a…"
This reverts commit 59d6df5dce.
2025-06-18 18:54:47 +08:00
Chenyang Shen
6b11d284da Merge pull request #5057 from jumpserver/pr@dev@perf_perf_face_verify_style
perf: perf face verify dialog style
2025-06-18 18:28:32 +08:00
Aaron3S
12b41f5abb perf: perf face verify dialog style 2025-06-18 18:25:44 +08:00
w940853815
398c8b0eb6 fix: There is a problem with task description typesetting in system task English mode. 2025-06-18 17:58:06 +08:00
w940853815
357f68b3d2 fix: Playbook template clone variable lose 2025-06-18 14:20:39 +08:00
zhaojisen
5d74b0cd84 Fixed: Fix the error where the number of password change attempts redirects incorrectly. 2025-06-18 13:20:53 +08:00
fit2bot
d72671d2c9 perf: add insert event (#5052)
* perf: add insert event

perf: receive insert code

perf: add content

perf: update code

perf: save panel to LocalStorage

* perf: only terminal add insert action

---------

Co-authored-by: Eric <xplzv@126.com>
2025-06-17 19:23:19 +08:00
w940853815
8f17fa82a2 fix: Adhoc template clone variable lose 2025-06-17 14:34:20 +08:00
Chenyang Shen
111957b6e6 Merge pull request #5050 from jumpserver/pr@dev@feat_remove_port_disabled
feat: remove oracle endpoint form field oracle_port disabled
2025-06-17 14:20:07 +08:00
Aaron3S
aa3292f988 feat: remove oracle endpoint form field oracle_port disabled 2025-06-17 14:18:40 +08:00
halo
cc0a72cecc perf: update message 2025-06-17 10:13:38 +08:00
halo
fae8b57456 feat: Add batch activation and disable options in account push tasks 2025-06-17 10:13:38 +08:00
zhaojisen
f2c3beff13 Fixed: Fix the issue of being able to replay magnus proxy's mongodb without recordings 2025-06-16 18:48:33 +08:00
Chenyang Shen
03790194ac Merge pull request #5046 from jumpserver/pr@dev@feat_add_endpoint_form_field
feat: add update endpoint form field
2025-06-16 18:24:51 +08:00
Aaron3S
2b5aebbc1b feat: add update endpoint form field 2025-06-16 18:10:36 +08:00
github-actions[bot]
6183f38d8a perf: Update Dockerfile with new base image tag 2025-06-16 17:59:54 +08:00
ewall555
29567c5392 feat: Remove vue-moment dependency, use moment library directly 2025-06-16 17:59:54 +08:00
Jackbewater
43f9b8cf68 perf: change secret add bulk action 2025-06-16 14:40:46 +08:00
w940853815
783070c74d fix: Improve text representation for boolean values in ChoicesFormatter 2025-06-16 14:33:33 +08:00
w940853815
59d6df5dce fix: A scroll bar appears in the asset tree on the left side of the adhoc. 2025-06-16 14:07:57 +08:00
w940853815
5fc93f61a2 perf: Clean up error message formatting in form validation 2025-06-16 11:08:54 +08:00
Eric
abee8aa7c9 perf: fix new chat click window 2025-06-16 11:08:13 +08:00
Eric
18d5194c38 perf: chat websocket init 2025-06-16 11:05:26 +08:00
ibuler
4f794d299a fix: bitwarden passkey reigster error 2025-06-16 10:58:53 +08:00
w940853815
623499b13b perf: Help text as tooltip for var_name field 2025-06-13 17:36:23 +08:00
feng
d48cb6b1f3 perf: Translate 2025-06-11 16:35:39 +08:00
zhaojisen
945ff8fc44 Fixed: Fix the issue of incomplete display of the action column in different languages 2025-06-11 15:07:21 +08:00
zhaojisen
3a823c786e Fixed: Fix the issue of platform list content exceeding the visible range and the problem of asset quantity not refreshing when renaming the node tree 2025-06-11 14:47:24 +08:00
zhaojisen
68a474644c Fix the issue of missing Oracle port in the endpoint order. 2025-06-11 11:25:16 +08:00
feng
c8b2ec9cdb perf: Translate 2025-06-10 19:14:42 +08:00
feng
67d4fdd175 perf: Change secret after successful login 2025-06-10 16:59:13 +08:00
feng
0f40b38abe perf: create account template secret type 2025-06-10 16:14:42 +08:00
w940853815
714350d40e fix: Update last published time field 2025-06-10 15:13:39 +08:00
w940853815
d6ac0db0e6 perf: Remove username hint 2025-06-09 17:00:09 +08:00
Chenyang Shen
ddae51cefc Merge pull request #5023 from jumpserver/pr@dev@feat_add_mongodb_endpoint
feat: add mongodb endpoint
2025-06-09 16:11:53 +08:00
Aaron3S
eb9f7c6cb5 feat: add mongodb endpoint 2025-06-09 16:09:46 +08:00
w940853815
a22f46087f perf: leak password can bulk delete 2025-06-06 17:09:34 +08:00
w940853815
2eedb361a0 perf: add SECURITY_EXPIRED_TOKEN_RECORD_KEEP_DAYS to password settings 2025-06-04 19:08:32 +08:00
w940853815
d893964947 perf: Language settings in personal settings 2025-05-29 11:13:35 +08:00
halo
13838f66a9 feat: Cloud sync support smartx 2025-05-28 15:12:25 +08:00
feng
6dccdae9b4 fix: login title does not exist 2025-05-23 11:02:26 +08:00
w940853815
3f31fa9810 fix: Community edition open watermark 2025-05-16 18:06:14 +08:00
w940853815
a17025bd3a fix: open monitor link in a new tab 2025-05-16 15:42:07 +08:00
ibuler
f08ce0ee1a perf: change session id width 2025-05-16 14:46:11 +08:00
52 changed files with 957 additions and 145 deletions

View File

@@ -1,4 +1,4 @@
FROM jumpserver/lina-base:20250508_085854 AS stage-build
FROM jumpserver/lina-base:20250616_083043 AS stage-build
ARG VERSION
ENV VERSION=$VERSION

View File

@@ -76,7 +76,6 @@
"vue-i18n": "^8.15.5",
"vue-json-editor": "^1.4.3",
"vue-markdown": "^2.2.4",
"vue-moment": "^4.1.0",
"vue-password-strength-meter": "^1.7.2",
"vue-router": "3.0.6",
"vue-select": "^3.9.5",

View File

@@ -74,7 +74,7 @@ export default {
},
createWatermark() {
if (this.currentUser?.username && this.publicSettings?.SECURITY_WATERMARK_ENABLED && this.$store.getters.hasValidLicense) {
if (this.currentUser?.username && this.publicSettings?.SECURITY_WATERMARK_ENABLED) {
this.watermark = new Watermark({
content: this.getWaterMarkContent(),
width: this.publicSettings?.SECURITY_WATERMARK_WIDTH,

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1748326203303" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2853" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M10.24 605.184l839.168-481.28L1013.76 220.672v191.488L174.592 895.488 10.24 804.352z" fill="#0096FF" p-id="2854"></path><path d="M10.24 416.768V220.672l168.96-96.768 308.736 178.688-331.776 193.536zM541.184 717.312l331.264-195.072 141.312 88.064v194.048l-165.376 95.744z" fill="#25C764" p-id="2855"></path></svg>

After

Width:  |  Height:  |  Size: 645 B

View File

@@ -1,6 +1,6 @@
<template>
<div class="container">
<div class="chat-action">
<div v-if="hasPrompt" class="chat-action">
<Select2
v-model="select.value"
:disabled="isLoading || isSelectDisabled"
@@ -36,6 +36,10 @@ export default {
expanded: {
type: Boolean,
default: false
},
hasPrompt: {
type: Boolean,
default: true
}
},
data() {

View File

@@ -33,7 +33,7 @@
<!-- eslint-disable-next-line -->
<div class="divider"></div>
<p>
<MessageText :message="item.reasoning" />
<MessageText :message="item.reasoning" @insert-code="handleInsertCode" />
</p>
</div>
@@ -41,8 +41,7 @@
<span v-if="isServerError" class="error">
{{ isServerError }}
</span>
<MessageText :message="item.result" />
</div>
<MessageText :message="item.result" :is-terminal="isTerminal" @insert-code="handleInsertCode" /></div>
</div>
</div>
<div class="action">
@@ -93,6 +92,10 @@ export default {
type: Object,
default: () => {
}
},
isTerminal: {
type: Boolean,
default: false
}
},
data() {
@@ -142,6 +145,9 @@ export default {
if (value === 'copy') {
copy(this.item.result.content)
}
},
handleInsertCode(code) {
this.$emit('insert-code', code)
}
}
}

View File

@@ -25,6 +25,10 @@ export default {
type: Object,
default: () => {
}
},
isTerminal: {
type: Boolean,
default: false
}
},
data() {
@@ -45,10 +49,10 @@ export default {
this.init()
},
updated() {
this.addCopyEvents()
this.addEvents()
},
destroyed() {
this.removeCopyEvents()
this.removeEvents()
},
methods: {
init() {
@@ -69,26 +73,64 @@ export default {
this.markdown.use(mdKatex, { blockClass: 'katexmath-block rounded-md', errorColor: ' #cc0000' })
},
highlightBlock(str, lang) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${'Copy'}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
let insertSpanHtml = `<span class="code-block-header__insert">${this.$t('Insert')}</span>`
if (!this.isTerminal) {
insertSpanHtml = ''
}
return `<pre class="code-block-wrapper">
<div class="code-block-header">
<span class="code-block-header__lang">${lang}</span>
<span class="code-block-header__actions">
${insertSpanHtml}
<span class="code-block-header__copy">${this.$t('Copy')}</span>
</span>
</div>
<code class="hljs code-block-body ${lang}">${str}</code></pre>`
},
addCopyEvents() {
const copyBtn = document.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.addEventListener('click', () => {
const code = btn.parentElement?.nextElementSibling?.textContent
if (code) {
copy(code)
}
addEvents() {
this.addBtnClickEvents('.code-block-header__copy', this.handlerClickCopy)
this.addBtnClickEvents('.code-block-header__insert', this.handlerClickInsert)
},
handlerClickCopy(event) {
const wrapper = event.target.closest('.code-block-wrapper')
if (wrapper) {
// 查找里面的 code 元素
const codeElement = wrapper.querySelector('code.code-block-body')
if (codeElement) {
const codeText = codeElement.textContent
copy(codeText)
}
}
},
handlerClickInsert(event) {
const wrapper = event.target.closest('.code-block-wrapper')
if (wrapper) {
// 查找里面的 code 元素
const codeElement = wrapper.querySelector('code.code-block-body')
if (codeElement) {
const codeText = codeElement.textContent
this.$emit('insert-code', codeText)
}
}
},
addBtnClickEvents(selector, callback) {
const buttons = this.$refs.textRef.querySelectorAll(selector)
buttons.forEach((btn) => {
btn.addEventListener('click', callback)
})
},
removeBtnClickEvent(selector) {
const buttons = this.$refs.textRef.querySelectorAll(selector)
buttons.forEach((btn) => {
btn.removeEventListener('click', () => {
})
})
},
removeCopyEvents() {
removeEvents() {
if (this.$refs.textRef) {
const copyBtn = this.$refs.textRef.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.removeEventListener('click', () => {
})
})
this.removeBtnClickEvent('.code-block-header__copy')
this.addBtnClickEvents('.code-block-header__insert')
}
}
}
@@ -98,6 +140,7 @@ export default {
<style lang="scss" scoped>
.markdown-body {
font-size: 13px;
max-width: 300px;;
&::v-deep p {
margin-bottom: 0 !important;
@@ -115,26 +158,46 @@ export default {
&::v-deep .code-block-wrapper {
background: #1F2329;
padding: 2px 6px;
padding: 0;
margin: 5px 0;
display: flex;
flex-direction: column;
overflow: hidden;
.code-block-body {
padding: 5px 10px 0;
padding: 5px 10px;
}
;
.code-block-header {
margin-bottom: 4px;
overflow: hidden;
background: #353946;
color: #c2d1e1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
width: 100%;
box-sizing: border-box;
.code-block-header__copy {
float: right;
cursor: pointer;
.code-block-header__actions {
display: flex;
gap: 8px;
&:hover {
color: #6e747b;
.code-block-header__copy {
cursor: pointer;
&:hover {
color: #6e747b;
}
}
.code-block-header__insert {
cursor: pointer;
&:hover {
color: #6e747b;
}
}
}
}
@@ -178,6 +241,7 @@ export default {
0% {
opacity: 1;
}
100% {
opacity: 0;
}

View File

@@ -17,7 +17,7 @@
</div>
</div>
</div>
<ChatMessage v-for="(item, index) in activeChat.chats" :key="index" :item="item" />
<ChatMessage v-for="(item, index) in activeChat.chats" :key="index" :item="item" :is-terminal="isTerminal" @insert-code="insertCode" />
</div>
<div class="input-box">
<el-button
@@ -28,7 +28,7 @@
size="small"
@click="onStopHandle"
>{{ $tc('Stop') }}</el-button>
<ChatInput ref="chatInput" :expanded="expanded" @send="onSendHandle" @select-prompt="onSelectPromptHandle" />
<ChatInput ref="chatInput" :expanded="expanded" :has-prompt="!isTerminal" @send="onSendHandle" @select-prompt="onSelectPromptHandle" />
</div>
</div>
</template>
@@ -68,7 +68,10 @@ export default {
prompt: '',
conversationId: '',
showIntroduction: false,
introduction: []
introduction: [],
terminalContext: null,
isTerminal: false,
sessionChat: {}
}
},
computed: {
@@ -100,6 +103,9 @@ export default {
this.showIntroduction = true
this.conversationId = ''
this.$refs.chatInput.select.value = ''
if (this.terminalContext) {
this.prompt = this.terminalContext.content || ''
}
const chat = {
message: {
content: this.$t('ChatHello'),
@@ -150,6 +156,32 @@ export default {
addMessageToActiveChat(data)
setLoading(true)
},
onTerminalContext(terminalContext) {
const originSessionId = this.terminalContext?.sessionId
const newSessionId = terminalContext.sessionId || ''
if (originSessionId) {
this.saveSessionChat(originSessionId)
}
this.terminalContext = terminalContext
this.isTerminal = true
this.prompt = terminalContext.content || ''
if (originSessionId !== newSessionId) {
if (this.sessionChat[newSessionId]) {
clearChats()
for (const chat of this.sessionChat[newSessionId]) {
addChatMessageById(chat)
}
} else {
this.onNewChat()
}
}
},
saveSessionChat(sessionId) {
if (this.terminalContext) {
this.sessionChat[sessionId] = JSON.parse(JSON.stringify(this.activeChat.chats))
}
},
onSendHandle(value) {
this.showIntroduction = false
this.socket = ws || {}
@@ -204,6 +236,15 @@ export default {
sendIntroduction(item) {
this.showIntroduction = false
this.onSendHandle(item.content)
},
insertCode(code) {
this.sendPostMessage({
name: 'INSERT_TERMINAL_CODE',
data: code.replace(/^[\s\r\n]+|[\s\r\n]+$/g, '')
})
},
sendPostMessage(data) {
window.parent.postMessage(data)
}
}
}

View File

@@ -47,7 +47,9 @@ import Chat from './components/ChitChat/index.vue'
import { getInputFocus } from './useChat.js'
import { ws } from '@/utils/socket'
import DrawerPanel from '@/components/Apps/DrawerPanel/index.vue'
import { ObjectLocalStorage } from '@/utils/common'
const aiPannelLocalStorage = new ObjectLocalStorage('ai_panel_settings')
export default {
components: {
DrawerPanel,
@@ -76,12 +78,15 @@ export default {
robotUrl: require('@/assets/img/robot-assistant.png'),
height: '400px',
expanded: false,
clientOffset: {}
clientOffset: {},
currentTerminalContent: {}
}
},
watch: {
},
mounted() {
const expanded = aiPannelLocalStorage.get('expanded')
this.updateExpandedState(expanded)
this.handlePostMessage()
},
methods: {
@@ -89,6 +94,17 @@ export default {
window.addEventListener('message', (event) => {
if (event.data === 'show-chat-panel') {
this.$refs.drawer.show = true
this.initWebSocket()
return
}
const msg = event.data
switch (msg.name) {
case 'current_terminal_content':
// {content: '...', terminalId: '',sessionId: '',viewId: '',viewName: ''}
this.$log.debug('current_terminal_content', msg)
this.currentTerminalContent = msg.data
this.$refs.component?.onTerminalContext(msg.data)
break
}
})
},
@@ -96,6 +112,11 @@ export default {
this.$refs.drawer.handleHeaderMoveDown(event)
},
handleMouseMoveUp(event) {
// Prevent the new chat button from triggering the header move up
const newButton = event.target.closest('.new')
if (newButton) {
return
}
this.$refs.drawer.handleHeaderMoveUp(event)
},
initWebSocket() {
@@ -107,12 +128,20 @@ export default {
this.$refs.drawer.show = false
},
expandFull() {
this.height = '100%'
this.expanded = true
this.updateExpandedState(true)
this.save_pannel_settings()
},
compress() {
this.height = '400px'
this.expanded = false
this.updateExpandedState(false)
this.save_pannel_settings()
},
save_pannel_settings() {
aiPannelLocalStorage.set('expanded', this.expanded)
console.log('AI panel settings saved:', this.expanded)
},
updateExpandedState(expanded) {
this.expanded = expanded
this.height = expanded ? '100%' : '400px'
},
onNewChat() {
this.active = 'chat'

View File

@@ -42,11 +42,12 @@ export default {
hasCreate: true,
hasSearch: true,
hasRefresh: true,
hasBulkDelete: false,
hasBulkDelete: true,
hasBulkUpdate: false,
hasLeftActions: true,
hasRightActions: true,
canCreate: this.$hasPerm('settings.change_security')
canCreate: this.$hasPerm('settings.change_security'),
canBulkDelete: this.$hasPerm('settings.change_security')
}
}
}

View File

@@ -82,7 +82,7 @@
:src="faceCaptureUrl"
allow="camera"
sandbox="allow-scripts allow-same-origin"
style="width: 100%; height: 800px;border: none;"
style="width: 100%; height: 600px;border: none;"
/>
</el-col>
</el-row>
@@ -99,7 +99,7 @@
</el-button>
<el-button
v-if="subTypeSelected === 'face'"
:disabled="isFaceCaptureVisible"
v-show="!isFaceCaptureVisible"
class="confirm-btn"
size="mini"
type="primary"

View File

@@ -0,0 +1,162 @@
<template>
<Dialog
:close-on-click-modal="false"
:title="$tc('Users')"
custom-class="user-select-dialog"
v-bind="$attrs"
@cancel="handleCancel"
@confirm="handleConfirm"
v-on="$listeners"
>
<ListTable
ref="ListPage"
:header-actions="headerActions"
:sync-select-to-url="false"
:table-config="tableConfig"
:url="baseUrl"
v-bind="$attrs"
@loaded="handleTableLoaded"
v-on="$listeners"
/>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
import ListTable from '@/components/Table/ListTable'
export default {
componentName: 'AssetSelectDialog',
components: { Dialog, ListTable },
props: {
baseUrl: {
type: String,
default: '/api/v1/users/users/?fields_size=small'
},
value: {
type: Array,
default: () => []
},
canSelect: {
type: Function,
default(row, index) {
return true
}
},
disabled: {
type: [Boolean, Function],
default: false
}
},
data() {
const vm = this
return {
isLoaded: false,
dialogVisible: false,
rowSelected: _.cloneDeep(this.value) || [],
rowsAdd: [],
tableConfig: {
url: this.baseUrl,
canSelect: this.canSelect,
columns: [
{
prop: 'name',
label: this.$t('Name'),
sortable: true
},
{
prop: 'username',
label: this.$t('Username'),
sortable: true
},
{
prop: 'email',
label: this.$t('Email'),
sortable: true
},
{
prop: 'mfa_level.label',
label: this.$t('MfaLevel'),
sortable: true,
formatter: function(row) {
return row.mfa_level.label
}
},
{
prop: 'source.label',
label: this.$t('Source'),
sortable: true,
formatter: function(row) {
return row.source.label
}
},
{
prop: 'org_roles.display_name',
label: this.$t('OrgRoles'),
sortable: true,
formatter: function(row) {
return row.org_roles.map(role => role.display_name).join(', ')
}
}
],
columnsMeta: {
actions: {
has: false
}
},
listeners: {
'toggle-row-selection': (isSelected, row) => {
if (isSelected) {
vm.addRowToSelect(row)
} else {
vm.removeRowFromSelect(row)
}
}
},
theRowDefaultIsSelected: (row) => {
return this.value.indexOf(row.id) > -1
}
},
headerActions: {
hasLeftActions: false,
hasImport: false,
hasExport: false
}
}
},
methods: {
handleTableLoaded() {
this.isLoaded = true
},
handleConfirm() {
this.$emit('confirm', this.rowSelected, this.rowsAdd)
},
handleCancel() {
this.$emit('cancel')
},
addRowToSelect(row) {
const selectValueIndex = this.rowSelected.indexOf(row.id)
if (selectValueIndex === -1) {
this.rowSelected.push(row.id)
this.rowsAdd.push(row)
}
},
removeRowFromSelect(row) {
const selectValueIndex = this.rowSelected.indexOf(row.id)
if (selectValueIndex > -1) {
this.rowSelected.splice(selectValueIndex, 1)
}
}
}
}
</script>
<style lang="scss" scoped>
.page ::v-deep .page-heading {
display: none;
}
.user-select-dialog ::v-deep .el-icon-circle-check {
display: none;
}
</style>

View File

@@ -0,0 +1,130 @@
<template>
<div class="user-select-formatter">
<Select2
ref="select2"
v-model="select2Config.value"
v-bind="select2Config"
@input="onInputChange"
v-on="$listeners"
@focus.stop="handleFocus"
/>
<UserSelectDialog
v-if="dialogVisible"
ref="dialog"
:base-url="baseUrl"
:value="value"
:visible.sync="dialogVisible"
v-bind="$attrs"
@cancel="handleCancel"
@confirm="handleConfirm"
v-on="$listeners"
/>
</div>
</template>
<script>
import Select2 from '@/components/Form/FormFields/Select2.vue'
import UserSelectDialog from './dialog.vue'
export default {
componentName: 'UserSelect',
components: { UserSelectDialog, Select2 },
props: {
baseUrl: {
type: String,
default: '/api/v1/users/users/?fields_size=small'
},
defaultPageSize: {
type: Number,
default: 10
},
value: {
type: Array,
default: () => []
},
disabled: {
type: [Boolean, Function],
default: false
}
},
data() {
const iValue = []
for (const item of this.value) {
if (typeof item === 'object') {
iValue.push(item.id)
} else {
iValue.push(item)
}
}
return {
dialogVisible: false,
initialValue: _.cloneDeep(iValue),
select2Config: {
disabled: this.disabled,
value: iValue,
multiple: true,
clearable: true,
defaultPageSize: this.defaultPageSize,
ajax: {
url: this.baseUrl,
transformOption: (item) => {
return { label: item.name + '(' + item.username + ')', value: item.id }
}
}
}
}
},
methods: {
handleFocus() {
this.$refs.select2.selectRef.blur()
this.dialogVisible = true
},
handleConfirm(valueSelected, rowsAdd) {
if (valueSelected === undefined) {
return
}
this.$refs.select2.iValue = valueSelected
this.addRowsToSelect(rowsAdd)
this.onInputChange(valueSelected)
this.dialogVisible = false
},
handleCancel() {
this.dialogVisible = false
},
onInputChange(val) {
this.$emit('change', val)
},
addToSelect(options, row) {
const selectOptionsHas = options.find(item => item.value === row.id)
// 如果select2的options中没有那么可能无法显示正常的值
if (selectOptionsHas === undefined) {
const option = {
label: `${row.name}(${row.username})`,
value: row.id
}
options.push(option)
}
},
addRowsToSelect(rows) {
const outSelectOptions = this.$refs.select2.options
for (const row of rows) {
this.addToSelect(outSelectOptions, row)
}
}
}
}
</script>
<style lang="scss" scoped>
.el-select {
width: 100%;
}
.page ::v-deep .page-heading {
display: none;
}
.page ::v-deep .treebox {
height: inherit !important;
}
</style>

View File

@@ -39,6 +39,9 @@ export default {
['', ['name', 'var_name', 'type', 'text_default_value', 'select_default_value', 'extra_args', 'tips', 'required']]
],
fieldsMeta: {
var_name: {
helpTextAsTip: false
},
text_default_value: {
label: this.$t('DefaultValue'),
hidden: (formValue) => {

View File

@@ -140,14 +140,11 @@ export default {
this._cleanFormValue(this.iForm, this.remoteMeta)
},
setFieldError(name, error) {
error = error.replace(/[。.]+$/, '')
const field = this.totalFields.find((v) => v.prop === name)
if (!field) {
return
}
if (field.attrs.error === error) {
error += '.'
}
if (typeof error === 'string') {
field.el.errors = error
field.attrs.error = error

View File

@@ -21,13 +21,16 @@ export class TableColumnsGenerator {
}
dynamicActionWidth() {
if (i18n.locale === 'en') {
console.log(i18n.locale)
if (i18n.locale === 'zh-hans' || i18n.locale === 'zh-hant') {
return '100px'
}
if (i18n.locale === 'ja' || i18n.locale === 'ko') {
return '120px'
}
if (i18n.locale === 'pt-br') {
return '160px'
}
return '100px'
return '160px'
}
generateColumns() {

View File

@@ -24,16 +24,17 @@ const formatterArgsDefault = {
true: 'text-primary',
false: 'text-danger'
},
textChoices: {
true: i18n.t('Yes'),
false: i18n.t('No')
},
getKey({ row, cellValue }) {
return (cellValue && typeof cellValue === 'object') ? cellValue.value : cellValue
},
getText({ row, cellValue }) {
const key = this.getKey({ row, cellValue })
return (cellValue && typeof cellValue === 'object') ? cellValue.label : this.textChoices[key] || cellValue
if (cellValue && typeof cellValue === 'object') {
return cellValue.label
}
if (key === true || key === 'true') return i18n.t('Yes')
if (key === false || key === 'false') return i18n.t('No')
return cellValue
},
getIcon({ row, cellValue }) {
const key = this.getKey({ row, cellValue })

View File

@@ -178,7 +178,18 @@ export default {
return
}
if (currentNode) {
currentNode.name = currentNode.meta.data.value
// 从节点名称中提取资产数量并保存
const nameMatch = currentNode.name.match(/^(.+?)\s*\((\d+)\)$/)
if (nameMatch) {
const pureName = nameMatch[1]
const assetsAmount = parseInt(nameMatch[2])
currentNode.name = pureName
currentNode.meta.data['assetsAmount'] = assetsAmount // 保存资产数量,确保重命名时不会丢失
} else {
currentNode.name = currentNode.meta.data.value
}
}
this.zTree.editName(currentNode)
},
@@ -237,13 +248,14 @@ export default {
if (isCancel) {
return
}
const originalAssetsAmount = treeNode.meta.data['assetsAmount'] || 0
this.$axios.patch(url, { 'value': treeNode.name }).then(res => {
let assetsAmount = treeNode.meta.data['assetsAmount']
if (!assetsAmount) {
assetsAmount = 0
}
treeNode.name = treeNode.name + ' (' + assetsAmount + ')'
treeNode.meta.data = res
treeNode.name = treeNode.name + ' (' + originalAssetsAmount + ')'
treeNode.meta.data = Object.assign({}, treeNode.meta.data, res)
treeNode.meta.data['assetsAmount'] = originalAssetsAmount
this.zTree.updateNode(treeNode)
this.$message.success(this.$tc('UpdateSuccessMsg'))
}).finally(() => {

View File

@@ -24,6 +24,9 @@ import { message } from '@/utils/message'
import xss from '@/utils/xss'
import ElTableTooltipPatch from '@/utils/elTableTooltipPatch.js'
import VSanitize from 'v-sanitize'
import moment from 'moment'
moment.locale('zh-cn')
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
@@ -50,11 +53,7 @@ Vue.config.productionTip = false
Vue.use(VueCookie)
window.$cookie = VueCookie
const moment = require('moment')
require('moment/locale/zh-cn')
Vue.use(require('vue-moment'), {
moment
})
Vue.prototype.$moment = moment
Vue.use(VueLogger, loggerOptions)

View File

@@ -62,7 +62,7 @@ const actions = {
link.href = faviconURL
}
// 动态修改Title
document.title = data['INTERFACE']['login_title']
document.title = data?.INTERFACE?.login_title || ''
}
const themeColors = data?.INTERFACE?.theme_info?.colors || {}
commit('SET_PUBLIC_SETTINGS', data)

View File

@@ -8,7 +8,9 @@ for (let i = 0; i < chars.length; i++) {
const encode = function(arraybuffer) {
const bytes = new Uint8Array(arraybuffer)
let i; const len = bytes.length; let base64url = ''
let i
const len = bytes.length
let base64url = ''
for (i = 0; i < len; i += 3) {
base64url += chars[bytes[i] >> 2]
@@ -17,7 +19,7 @@ const encode = function(arraybuffer) {
base64url += chars[bytes[i + 2] & 63]
}
if ((len % 3) === 2) {
if (len % 3 === 2) {
base64url = base64url.substring(0, base64url.length - 1)
} else if (len % 3 === 1) {
base64url = base64url.substring(0, base64url.length - 2)
@@ -28,8 +30,13 @@ const encode = function(arraybuffer) {
const decode = function(base64string) {
const bufferLength = base64string.length * 0.75
const len = base64string.length; let i; let p = 0
let encoded1; let encoded2; let encoded3; let encoded4
const len = base64string.length
let i
let p = 0
let encoded1
let encoded2
let encoded3
let encoded4
const bytes = new Uint8Array(bufferLength)
@@ -47,15 +54,16 @@ const decode = function(base64string) {
return bytes.buffer
}
const publicKeyCredentialToJSON = (pubKeyCred) => {
const publicKeyCredentialToJSON = pubKeyCred => {
if (pubKeyCred instanceof Array) {
const arr = []
for (const i of pubKeyCred) { arr.push(publicKeyCredentialToJSON(i)) }
for (const i of pubKeyCred) {
arr.push(publicKeyCredentialToJSON(i))
}
return arr
}
if (pubKeyCred instanceof ArrayBuffer) {
if (pubKeyCred instanceof ArrayBuffer || pubKeyCred instanceof Uint8Array) {
return encode(pubKeyCred)
}
@@ -73,8 +81,7 @@ const publicKeyCredentialToJSON = (pubKeyCred) => {
}
export default {
'decode': decode,
'encode': encode,
'publicKeyCredentialToJSON': publicKeyCredentialToJSON
decode: decode,
encode: encode,
publicKeyCredentialToJSON: publicKeyCredentialToJSON
}

View File

@@ -63,7 +63,7 @@ export default {
executed_amount: {
formatter: (row) => {
const can = vm.$hasPerm('accounts.view_changesecretexecution')
return <el-link onClick={ () => this.handleExecAmount(row) } disabled={ !can }>{ row.executed_amount }</el-link>
return <el-link onClick={() => this.handleExecAmount(row)} disabled={!can}>{row.executed_amount}</el-link>
}
},
actions: {
@@ -101,7 +101,23 @@ export default {
hasRefresh: true,
hasExport: false,
hasImport: false,
createRoute: 'AccountChangeSecretCreate'
createRoute: 'AccountChangeSecretCreate',
extraMoreActions: [
{
name: 'BatchDisable',
title: this.$t('DisableSelected'),
icon: 'fa fa-ban',
can: ({ selectedRows }) => selectedRows.length > 0 && this.$hasPerm('accounts.change_changesecretautomation'),
callback: ({ selectedRows, reloadTable }) => this.bulkDisableCallback(selectedRows, reloadTable)
},
{
name: 'BatchActivate',
title: this.$t('ActivateSelected'),
icon: 'fa fa-check-circle-o',
can: ({ selectedRows }) => selectedRows.length > 0 && this.$hasPerm('accounts.change_changesecretautomation'),
callback: ({ selectedRows, reloadTable }) => this.bulkActivateCallback(selectedRows, reloadTable)
}
]
}
}
},
@@ -114,6 +130,32 @@ export default {
automation_id: row.id
}
})
},
bulkDisableCallback(selectedRows, reloadTable) {
const url = '/api/v1/accounts/change-secret-automations/'
const data = selectedRows.map(row => {
return { id: row.id, is_active: false }
})
if (data.length === 0) return
this.$axios.patch(url, data).then(() => {
reloadTable()
this.$message.success(this.$t('DisableSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('UpdateErrorMsg') + ' ' + error)
})
},
bulkActivateCallback(selectedRows, reloadTable) {
const url = '/api/v1/accounts/change-secret-automations/'
const data = selectedRows.map(row => {
return { id: row.id, is_active: true }
})
if (data.length === 0) return
this.$axios.patch(url, data).then(() => {
reloadTable()
this.$message.success(this.$t('ActivateSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('UpdateErrorMsg') + ' ' + error)
})
}
}
}

View File

@@ -0,0 +1,192 @@
<template>
<div>
<GenericListTable ref="ListTable" :header-actions="headerActions" :table-config="tableConfig" />
</div>
</template>
<script>
import { GenericListTable } from '@/layout/components'
import { ActionsFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
export default {
name: 'AccountChangeSecret',
components: {
GenericListTable
},
data() {
const vm = this
return {
secretUrl: '',
showViewSecretDialog: false,
tableConfig: {
url: '/api/v1/accounts/change-secret-status/',
columns: [
'execution_id', 'asset', 'account', 'status', 'ttl', 'actions'
],
columnsMeta: {
asset: {
formatter: DetailFormatter,
formatterArgs: {
drawer: true,
can: this.$hasPerm('assets.view_asset'),
getTitle: ({ row }) => row.asset.name,
getDrawerTitle: ({ row }) => row.asset.name,
getRoute: ({ row }) => ({
name: 'AssetDetail',
params: { id: row.asset.id },
query: { tab: 'Basic' }
})
}
},
account: {
label: vm.$t('Username'),
formatter: DetailFormatter,
formatterArgs: {
drawer: true,
can: this.$hasPerm('accounts.view_account'),
getTitle: ({ row }) => row.username,
getDrawerTitle: ({ row }) => row.username,
getRoute: ({ row }) => ({
name: 'AssetAccountDetail',
params: { id: row.id },
query: { tab: 'Basic' }
})
}
},
ttl: {
label: vm.$t('TTL')
},
execution_id: {
width: '200px',
label: vm.$t('ExecutionID'),
formatter: DetailFormatter,
formatterArgs: {
drawer: true,
can: true,
getTitle: ({ row }) => row.meta?.execution_id ? row.meta.execution_id : '-',
getDrawerTitle: ({ row }) => row.meta?.execution_id,
getRoute: ({ row }) => ({
name: 'AccountChangeSecretExecutionDetail',
params: { id: row.meta?.execution_id }
})
}
},
status: {
width: '100px',
label: vm.$t('Status'),
formatter: (row) => {
const statusMap = {
queued: 'Queued',
ready: 'Ready',
processing: 'Processing'
}
if (statusMap[row.meta.status]) {
return <span>{ vm.$t(statusMap[row.meta.status]) }</span>
}
return <span></span>
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false,
hasDelete: false,
hasClone: false,
moreActionsTitle: this.$t('More'),
extraActions: [
{
name: 'Delete',
title: this.$t('Delete'),
can: this.$hasPerm('accounts.add_changesecretexecution'),
type: 'danger',
callback: ({ row }) => {
this.$axios.delete(
'/api/v1/accounts/change-secret-status/',
{
data: {
account_ids: [row.id]
}
}
)
vm.$refs.ListTable.reloadTable()
}
}
]
}
}
}
},
headerActions: {
hasSearch: true,
hasRefresh: true,
hasLeftActions: true,
hasRightActions: true,
hasExport: false,
hasImport: false,
hasCreate: false,
hasBulkDelete: false,
hasBulkUpdate: false,
searchConfig: {
getUrlQuery: true,
options: [
{
label: this.$t('AssetName'),
value: 'asset_name'
},
{
label: this.$t('ExecutionID'),
value: 'execution_id'
},
{
value: 'status',
label: this.$t('Status'),
type: 'choice',
children: [
{
default: true,
value: 'queued',
label: this.$t('Queued')
},
{
value: 'ready',
label: this.$t('Ready')
},
{
value: 'processing',
label: this.$t('Processing')
}
]
}
]
},
extraMoreActions: [
{
name: 'DeleteSelected',
title: this.$t('DeleteSelected'),
type: 'primary',
fa: 'fa-retweet',
can: ({ selectedRows }) => {
return selectedRows.length > 0
},
callback: function({ selectedRows }) {
const ids = selectedRows.map(v => {
return v.id
})
this.$axios.delete(
'/api/v1/accounts/change-secret-status/',
{
data: {
account_ids: ids
}
}
)
vm.$refs.ListTable.reloadTable()
}.bind(this)
}
]
}
}
}
}
</script>

View File

@@ -17,9 +17,8 @@ export default {
BaseExecutionList
},
data() {
const params = new URLSearchParams(this.$route.params).toString()
return {
url: `/api/v1/accounts/change-secret-executions/?${params}`,
url: `/api/v1/accounts/change-secret-executions/`,
detailRoute: 'AccountChangeSecretExecutionDetail',
automationRoute: 'AccountChangeSecretDetail',
resource: 'changesecretexecution',

View File

@@ -5,6 +5,7 @@
<script>
import { TabPage } from '@/layout/components'
import { mapGetters } from 'vuex'
import store from '@/store'
export default {
name: 'Index',
@@ -38,13 +39,22 @@ export default {
name: 'ChangeSecretRecord',
hidden: () => !this.$hasPerm('accounts.view_changesecretrecord'),
component: () => import('@/views/accounts/AccountChangeSecret/ExecutionDetail/AccountChangeSecretRecord.vue')
},
{
title: this.$t('ChangeSecretStatus'),
name: 'ChangeSecretStatus',
hidden: () => !this.$hasPerm('accounts.view_changesecretexecution') || !this.ChangeSecretAfterSessionEnd,
component: () => import('@/views/accounts/AccountChangeSecret/AccountList.vue')
}
]
}
}
},
computed: {
...mapGetters(['hasValidLicense'])
...mapGetters(['hasValidLicense']),
ChangeSecretAfterSessionEnd() {
return store.getters.publicSettings?.CHANGE_SECRET_AFTER_SESSION_END
}
},
mounted() {
this.$eventBus.$on('change-tab', this.handleChangeTab)

View File

@@ -3,7 +3,7 @@
:create-drawer="createDrawer"
:detail-drawer="detailDrawer"
:header-actions="headerActions"
:resource="$tc('AccountDiscoverTask')"
:resource="$tc('DiscoverAccountTask')"
:table-config="tableConfig"
/>
</template>

View File

@@ -104,7 +104,23 @@ export default {
hasRefresh: true,
hasExport: false,
hasImport: false,
createRoute: 'AccountPushCreate'
createRoute: 'AccountPushCreate',
extraMoreActions: [
{
name: 'BatchDisable',
title: this.$t('DisableSelected'),
icon: 'fa fa-ban',
can: ({ selectedRows }) => selectedRows.length > 0 && this.$hasPerm('accounts.change_pushaccountautomation'),
callback: ({ selectedRows, reloadTable }) => this.bulkDisableCallback(selectedRows, reloadTable)
},
{
name: 'BatchActivate',
title: this.$t('ActivateSelected'),
icon: 'fa fa-check-circle-o',
can: ({ selectedRows }) => selectedRows.length > 0 && this.$hasPerm('accounts.change_pushaccountautomation'),
callback: ({ selectedRows, reloadTable }) => this.bulkActivateCallback(selectedRows, reloadTable)
}
]
}
}
},
@@ -117,6 +133,32 @@ export default {
automation_id: row.id
}
})
},
bulkDisableCallback(selectedRows, reloadTable) {
const url = '/api/v1/accounts/push-account-automations/'
const data = selectedRows.map(row => {
return { id: row.id, is_active: false }
})
if (data.length === 0) return
this.$axios.patch(url, data).then(() => {
reloadTable()
this.$message.success(this.$t('DisableSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('UpdateErrorMsg') + ' ' + error)
})
},
bulkActivateCallback(selectedRows, reloadTable) {
const url = '/api/v1/accounts/push-account-automations/'
const data = selectedRows.map(row => {
return { id: row.id, is_active: true }
})
if (data.length === 0) return
this.$axios.patch(url, data).then(() => {
reloadTable()
this.$message.success(this.$t('DisableSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('UpdateErrorMsg') + ' ' + error)
})
}
}
}

View File

@@ -69,7 +69,35 @@ export default {
updateSuccessNextRoute: { name: 'AccountTemplateList' }
}
},
async mounted() {
this.setSecretTypeOptions()
},
methods: {
setSecretTypeOptions() {
const choices = [
{
label: this.$t('Password'),
value: 'password'
},
{
label: this.$t('SSHKey'),
value: 'ssh_key'
},
{
label: this.$t('Token'),
value: 'token'
},
{
label: this.$t('AccessKey'),
value: 'access_key'
},
{
label: this.$t('ApiKey'),
value: 'api_key'
}
]
this.fieldsMeta.secret_type.options = choices
},
getObjectDone(obj) {
if (['token', 'access_key', 'api_key'].includes(obj.secret_type.value)) {
this.fieldsMeta.auto_push.el.disabled = true

View File

@@ -5,6 +5,7 @@
:detail-drawer="detailDrawer"
:header-actions="headerActions"
:table-config="tableConfig"
:resource="$t('AccountTemplate')"
/>
<ViewSecret
v-if="showViewSecretDialog"

View File

@@ -155,11 +155,6 @@ export default {
}
},
mounted() {
const automation_id = this.$route.query.automation_id
if (automation_id !== undefined) {
this.tableConfig.url = `${this.tableConfig.url}?automation_id=${automation_id}`
}
const defaultExtraActions = this.tableConfig.columnsMeta.actions.formatterArgs.extraActions
if (this.customActions) {

View File

@@ -1,5 +1,10 @@
<template>
<GenericListTable ref="listTable" :header-actions="headerActions" :table-config="tableConfig" />
<GenericListTable
ref="listTable"
:header-actions="headerActions"
:table-config="tableConfig"
:resource="$t('DetectTasks')"
/>
</template>
<script>

View File

@@ -163,6 +163,7 @@ export default {
::v-deep .el-drawer__body {
padding: 0 20px;
overflow-y: scroll;
}
.platform-content {

View File

@@ -28,19 +28,20 @@ export const fc = 'fc'
export const scp = 'scp'
export const apsara_stack = 'apsara_stack'
export const lan = 'lan'
export const smartx = 'smartx'
export const publicHostProviders = [
aliyun, qcloud, qcloud_lighthouse, huaweicloud,
baiducloud, jdcloud, kingsoftcloud, aws_china,
aws_international, azure, azure_international,
gcp, ucloud, volcengine
gcp, ucloud, volcengine, smartx
]
export const publicDBProviders = [aliyun]
export const privateCloudProviders = [
vmware, qingcloud_private, huaweicloud_private, ctyun_private,
openstack, zstack, nutanix, fc, scp, apsara_stack
openstack, zstack, nutanix, fc, scp, apsara_stack, smartx
]
export const ACCOUNT_PROVIDER_ATTRS_MAP = {
@@ -164,6 +165,12 @@ export const ACCOUNT_PROVIDER_ATTRS_MAP = {
attrs: ['access_key_id', 'access_key_secret', 'api_endpoint'],
image: require('@/assets/img/cloud/zstack.svg')
},
[smartx]: {
name: smartx,
title: i18n.t('SmartX CloudTower'),
attrs: ['username', 'password', 'api_endpoint'],
image: require('@/assets/img/cloud/smartx.svg')
},
[fc]: {
name: fc,
title: i18n.t('FC'),

View File

@@ -157,10 +157,6 @@ export default {
assets: hosts,
query: query
}).then(data => {
if (Array.isArray(data) && data.length === 0) {
this.$message.info(`${this.$t('NoAccountFound')}`)
return cb([])
}
const ns = data.map(item => {
return { value: item.username }
})

View File

@@ -37,7 +37,7 @@ export default {
name: 'Template'
},
cleanFormValue(value) {
const isClone = this?.$route?.query.clone_from !== undefined
const isClone = this?.action === 'clone'
if (isClone) {
value?.variable.map((item) => {
delete item.id

View File

@@ -28,7 +28,7 @@ export default {
name: 'Template'
},
cleanFormValue(value) {
const isClone = this?.$route?.query.clone_from !== undefined
const isClone = this?.action === 'clone'
if (isClone) {
value?.variable.map((item) => {
delete item.id

View File

@@ -10,6 +10,7 @@
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import UserSelect from '@/components/Apps/UserSelect'
import AssetSelect from '@/components/Apps/AssetSelect'
import AccountFormatter from './components/AccountFormatter'
import { AllAccount } from '../const'
@@ -49,14 +50,15 @@ export default {
createSuccessNextRoute: { name: 'AssetPermissionDetail' },
fieldsMeta: {
users: {
type: 'userSelect',
component: UserSelect,
rules: [{
required: false
}],
el: {
value: [],
ajax: {
url: '/api/v1/users/users/?fields_size=mini',
transformOption: (item) => {
return { label: item.name + '(' + item.username + ')', value: item.id }
}
}
defaultPageSize: 300,
baseUrl: '/api/v1/users/users/?fields_size=small'
}
},
user_groups: {

View File

@@ -22,6 +22,19 @@ export default {
component: BoolTextReadonly
}
}
},
basic: {
fieldsMeta: {
lang: {
on: {
change: ([value], updateForm) => {
this.$axios.get(`/core/i18n/${value}/`).then(() => {
window.location.reload()
})
}
}
}
}
}
}
}

View File

@@ -39,7 +39,8 @@ export default {
},
columnsMeta: {
type: Object,
default: () => {}
default: () => {
}
},
columnsExclude: {
type: Array,
@@ -57,12 +58,15 @@ export default {
id: {
prop: 'id',
label: this.$t('Number'),
width: '80px',
align: 'center',
formatter: DetailFormatter,
formatterArgs: {
drawer: true,
can: this.$hasPerm('assets.view_asset'),
getTitle: ({ row, col, cellValue, index }) => { return index + 1 },
getTitle: ({ row, col, cellValue, index }) => {
return index + 1
},
getDrawerTitle: ({ row }) => {
return row.id
},
@@ -78,7 +82,9 @@ export default {
formatter: DetailFormatter,
formatterArgs: {
drawer: true,
getTitle: ({ row }) => { return row.user },
getTitle: ({ row }) => {
return row.user
},
getRoute: ({ row }) => {
return {
name: 'UserDetail',

View File

@@ -25,7 +25,8 @@ export default {
name: 'replay',
title: this.$t('Replay'),
type: 'warning',
can: ({ row }) => vm.hasPerms(row, 'view'),
// TODO 当前版本 magnus 代理的 mongodb 协议的 session 不支持 replay
can: ({ row }) => vm.hasPerms(row, 'view') && !(row.protocol === 'mongodb' && row.terminal.type === 'magnus'),
callback: function({ row, tableData }) {
// 跳转到luna页面
const replayUrl = '/luna/replay/' + row.id
@@ -36,7 +37,7 @@ export default {
name: 'download',
title: this.$t('Download'),
type: 'primary',
can: ({ row }) => vm.hasPerms(row, 'download'),
can: ({ row }) => vm.hasPerms(row, 'download') && !(row.protocol === 'mongodb' && row.terminal.type === 'magnus'),
callback: function({ row, tableData }) {
// 跳转下载页面
download(`/api/v1/terminal/sessions/${row.id}/replay/download/`)

View File

@@ -1,7 +1,7 @@
<template>
<Dialog
:before-close="handleClose"
:loading-status="!isFinished"
:disabled-status="!isFinished"
:show-cancel="false"
:title="$tc('OfflineUpload')"
v-bind="$attrs"
@@ -104,6 +104,8 @@ export default {
const error = err.response.data
const msg = error?.message || error?.detail || error?.error || JSON.stringify(error)
this.$message.error(msg)
}).finally(() => {
this.$refs.upload.clearFiles()
})
setTimeout(() => {

View File

@@ -7,6 +7,7 @@
ref="table"
class="applet-host"
:create-drawer="createDrawer"
:resource="$t('AppletHosts')"
v-bind="$data"
/>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<Dialog
:before-close="handleClose"
:loading-status="!isFinished"
:disabled-status="!isFinished"
:show-cancel="false"
:title="$tc('OfflineUpload')"
v-bind="$attrs"
@@ -102,11 +102,9 @@ export default {
const error = err.response.data
const msg = error?.message || error?.detail || error?.error || JSON.stringify(error)
this.$message.error(msg)
})
setTimeout(() => {
}).finally(() => {
this.$refs.upload.clearFiles()
}, 400)
})
}
}
}

View File

@@ -30,7 +30,8 @@ export default {
this.$t('Basic'),
[
'SECURITY_PASSWORD_EXPIRATION_TIME',
'OLD_PASSWORD_HISTORY_LIMIT_COUNT'
'OLD_PASSWORD_HISTORY_LIMIT_COUNT',
'SECURITY_EXPIRED_TOKEN_RECORD_KEEP_DAYS'
]
],
[

View File

@@ -58,7 +58,7 @@ export default {
'SECURITY_WATERMARK_HEIGHT',
'SECURITY_WATERMARK_WIDTH',
'SECURITY_WATERMARK_ROTATE'
] : []
] : ['SECURITY_WATERMARK_ENABLED']
]
],
fieldsMeta: {

View File

@@ -1,5 +1,5 @@
<template>
<el-row :gutter="20">
<el-row :gutter="20" class="task-detail">
<el-col :md="20" :sm="24">
<DetailCard :items="detailCardItems" :title="cardTitle" />
</el-col>
@@ -43,7 +43,7 @@ export default {
},
{
key: this.$t('LastPublishedTime'),
value: this.object.last_published_time
value: this.object.date_last_publish
},
{
key: this.$t('Description'),
@@ -55,7 +55,8 @@ export default {
methods: {}
}
</script>
<style lang="less" scoped>
.task-detail /deep/ .item-value span {
white-space: normal !important;
}
</style>

View File

@@ -30,7 +30,8 @@ export default {
'',
[
'mysql_port', 'mariadb_port', 'postgresql_port',
'redis_port', 'sqlserver_port', 'oracle_port_range'
'redis_port', 'sqlserver_port', 'oracle_port',
'mongodb_port'
]
],
[this.$t('Other'), ['is_active', 'comment']]
@@ -39,9 +40,6 @@ export default {
host: {
disabled: this.$route.params.id === '00000000-0000-0000-0000-000000000001'
},
oracle_port_range: {
disabled: true
},
is_active: {
disabled: this.$route.params.id === '00000000-0000-0000-0000-000000000001'
}

View File

@@ -6,6 +6,7 @@
:header-actions="headerActions"
:table-config="tableConfig"
:create-drawer="createDrawer"
:resource="$t('Endpoint')"
/>
</div>
</template>
@@ -31,7 +32,7 @@ export default {
'name', 'host', 'actions',
'http_port', 'https_port', 'ssh_port', 'rdp_port', 'vnc_port',
'mysql_port', 'mariadb_port', 'postgresql_port',
'redis_port', 'sqlserver_port', 'oracle_port', 'is_active'
'redis_port', 'sqlserver_port', 'oracle_port', 'mongodb_port', 'is_active'
]
},
columnsMeta: {
@@ -43,7 +44,8 @@ export default {
canUpdate: this.$hasPerm('terminal.change_endpoint'),
updateRoute: 'EndpointUpdate',
cloneRoute: 'EndpointCreate',
canDelete: ({ row }) => row.id !== '00000000-0000-0000-0000-000000000001' && this.$hasPerm('terminal.delete_endpoint')
canDelete: ({ row }) => row.id !== '00000000-0000-0000-0000-000000000001' &&
this.$hasPerm('terminal.delete_endpoint')
}
}
}

View File

@@ -6,6 +6,7 @@
:header-actions="headerActions"
:table-config="tableConfig"
:create-drawer="createDrawer"
:resource="$t('EndpointRules')"
/>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<GenericDetailPage :active-menu.sync="config.activeMenu" :object.sync="ticket" v-bind="config" v-on="$listeners">
<GenericDetailPage :title="ticket.title" :active-menu.sync="config.activeMenu" :object.sync="ticket" v-bind="config" v-on="$listeners">
<component :is="config.activeMenu" :object="ticket" />
</GenericDetailPage>
</template>

View File

@@ -134,7 +134,7 @@ export default {
},
onMonitor() {
const joinUrl = `/luna/monitor/${this.session.id}?ticket_id=${this.object.id}`
window.open(joinUrl, 'height=600, width=850, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')
window.open(joinUrl, '_blank', 'height=600, width=850, top=400, left=400, toolbar=no, menubar=no, scrollbars=no, location=no, status=no')
},
onToggleLock() {
const url = '/api/v1/terminal/tasks/toggle-lock-session-for-ticket/'

View File

@@ -8914,7 +8914,7 @@ moment-parseformat@^4.0.0:
resolved "https://registry.npmmirror.com/moment-parseformat/-/moment-parseformat-4.0.0.tgz"
integrity sha512-0V4ICKnI1npglqrMSDK2y8WxOdN79DkMoIexzY3P+jr2wNfbB4J81BgjFfHsj18wBsV7FdKCWyCHcezzH0xlyg==
moment@^2.19.2, moment@^2.29.4:
moment@^2.29.4:
version "2.29.4"
resolved "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
@@ -12075,7 +12075,7 @@ string-length@^2.0.0:
astral-regex "^1.0.0"
strip-ansi "^4.0.0"
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -12093,6 +12093,15 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.npmmirror.com/string-width/-/string-width-2.1.1.tgz"
@@ -12193,7 +12202,7 @@ stringify-package@^1.0.1:
resolved "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz"
integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -12221,6 +12230,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz"
@@ -13215,13 +13231,6 @@ vue-markdown@^2.2.4:
markdown-it-task-lists "^2.0.1"
markdown-it-toc-and-anchor "^4.1.2"
vue-moment@^4.1.0:
version "4.1.0"
resolved "https://registry.npmmirror.com/vue-moment/-/vue-moment-4.1.0.tgz"
integrity sha512-Gzisqpg82ItlrUyiD9d0Kfru+JorW2o4mQOH06lEDZNgxci0tv/fua1Hl0bo4DozDV2JK1r52Atn/8QVCu8qQw==
dependencies:
moment "^2.19.2"
vue-password-strength-meter@^1.7.2:
version "1.7.2"
resolved "https://registry.npmmirror.com/vue-password-strength-meter/-/vue-password-strength-meter-1.7.2.tgz"