Compare commits

...

30 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
35 changed files with 653 additions and 113 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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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>
@@ -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'
'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: {
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>

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

@@ -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"