Compare commits

...

23 Commits

Author SHA1 Message Date
Eric
4917212fff perf: add content 2025-06-13 19:05:42 +08:00
Eric
d84a7c824b perf: receive insert code 2025-06-12 18:00:27 +08:00
Eric
56f4f3f144 perf: add insert event 2025-06-12 17:51:06 +08:00
Eric
fea1d35981 perf: fix new chat click window 2025-06-12 17:51:06 +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
24 changed files with 415 additions and 60 deletions

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

@@ -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,7 +41,7 @@
<span v-if="isServerError" class="error">
{{ isServerError }}
</span>
<MessageText :message="item.result" />
<MessageText :message="item.result" @insert-code="handleInsertCode" />
</div>
</div>
</div>
@@ -142,6 +142,9 @@ export default {
if (value === 'copy') {
copy(this.item.result.content)
}
},
handleInsertCode(code) {
this.$emit('insert-code', code)
}
}
}

View File

@@ -69,26 +69,61 @@ 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>`
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">
<span class="code-block-header__insert">${'insert'}</span>
<span class="code-block-header__copy">${'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)
}
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
console.log('insert code', codeText)
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() {
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')
}
}
}
@@ -115,26 +150,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 +233,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" @insert-code="insertCode" />
</div>
<div class="input-box">
<el-button
@@ -60,6 +60,10 @@ export default {
expanded: {
type: Boolean,
default: false
},
terminalContent: {
type: Object,
default: null
}
},
data() {
@@ -68,7 +72,8 @@ export default {
prompt: '',
conversationId: '',
showIntroduction: false,
introduction: []
introduction: [],
terminalContext: this.terminalContent || null
}
},
computed: {
@@ -204,7 +209,11 @@ export default {
sendIntroduction(item) {
this.showIntroduction = false
this.onSendHandle(item.content)
},
insertCode(code) {
console.log(' receive insertCode', code)
}
}
}
</script>

View File

@@ -76,7 +76,8 @@ export default {
robotUrl: require('@/assets/img/robot-assistant.png'),
height: '400px',
expanded: false,
clientOffset: {}
clientOffset: {},
currentTerminalContent: {}
}
},
watch: {
@@ -89,6 +90,15 @@ export default {
window.addEventListener('message', (event) => {
if (event.data === 'show-chat-panel') {
this.$refs.drawer.show = true
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
break
}
})
},
@@ -96,6 +106,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() {

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

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

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

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

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

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

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

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

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

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

@@ -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'),

View File

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

View File

@@ -31,7 +31,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 +43,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

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