Compare commits

...

10 Commits

13 changed files with 302 additions and 98 deletions

View File

@@ -107,6 +107,10 @@ export default {
this.$emit('add', true)
}
}).catch(error => {
if (error?.response?.data?.code === 'no_valid_assets') {
this.$message.error(error?.response?.data?.detail)
return
}
this.iVisible = true
this.handleResult(null, error)
})

View File

@@ -223,9 +223,21 @@ export default {
const mapped = {}
Object.entries(errors || {}).forEach(([k, v]) => {
let msg = v
console.log(k, v)
// v是数组并且数组都是字符串则拼接为字符串
if (Array.isArray(v) && v.every(item => typeof item === 'string')) msg = v.join('; ')
else if (typeof v === 'object' && v !== null) msg = JSON.stringify(v)
// 处理 [{"port":["请确保该值小于或者等于 65535。"]},{},{}] 这种情况
else if (Array.isArray(v) && v.every(item => _.isPlainObject(item))) {
const subMsg = []
v.forEach((subItem) => {
Object.values(subItem).forEach((subMsgArr) => {
if (Array.isArray(subMsgArr)) {
subMsg.push(...subMsgArr)
}
})
})
msg = subMsg.join(' ')
} else if (typeof v === 'object' && v !== null) msg = JSON.stringify(v)
mapped[k] = String(msg || '')
})
this.serverErrors = mapped

View File

@@ -65,6 +65,17 @@ export default {
'usingOrgs',
'currentViewRoute'
]),
currentOrgDisplayName() {
const currentOrgId = this.currentOrg?.id
if (!currentOrgId) {
return this.$tc('Select')
}
const matchedOrg = this.usingOrgs.find(item => item.id === currentOrgId)
if (matchedOrg?.name) {
return matchedOrg.name
}
return this.currentOrg.name || this.$tc('Select')
},
orgActionsGroup() {
const orgActions = {
label: this.$t('OrganizationList'),
@@ -110,11 +121,8 @@ export default {
}
},
watch: {
currentOrg: {
handler() {
this.updateWidth()
},
deep: true
currentOrgDisplayName() {
this.updateWidth()
}
},
mounted() {
@@ -133,8 +141,9 @@ export default {
tempSpan.style.fontWeight = 'normal'
tempSpan.style.letterSpacing = 'normal'
// 获取当前组织名称
const orgName = this.currentOrg.name || this.$tc('Select')
// 获取当前组织显示名称
const orgName = this.currentOrgDisplayName
tempSpan.textContent = orgName
document.body.appendChild(tempSpan)

View File

@@ -167,8 +167,7 @@ export default {
}
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.page.no-title {
::v-deep {
.page-submenu .el-tabs__header {
@@ -199,6 +198,14 @@ export default {
opacity: 1;
}
}
&.is-disabled {
cursor: not-allowed;
&:hover {
color: #c0c4cc;
}
}
}
.el-tabs__nav-next {

View File

@@ -149,6 +149,11 @@ export function getErrorResponseMsg(error) {
.join('; ')
} else if (typeof data === 'string') {
return data
} else if (_.isPlainObject(data)) {
return Object.values(data)
.map(item => getErrorResponseMsg(item))
.filter(i => i)
.join('; ')
} else {
msg = error.toString()
}

View File

@@ -43,7 +43,9 @@ export default {
asset: ''
},
treeSetting: {
showMenu: true,
showMenu: (node) => {
return node?.meta?.type === 'asset'
},
showRefresh: true,
showSearch: true,
showAssets: true,
@@ -55,9 +57,8 @@ export default {
menu: [
{
id: 'check',
name: this.$t('Check'),
name: this.$t('RiskDetection'),
icon: 'scan',
has: (node) => node?.meta?.type === 'asset',
callback: (node) => {
vm.detectDialog.asset = node.id
setTimeout(() => {

View File

@@ -6,7 +6,6 @@
class="risk-review-drawer"
destroy-on-close
direction="rtl"
style="z-index: 999"
>
<div class="drawer-container">
<div class="drawer-body">

View File

@@ -408,7 +408,6 @@ export default {
createJob(data).then(res => {
this.progressLength = 0
this.executionInfo.timeCost = 0
this.showProgress = true
this.speedText = ''
const form = new FormData()
const start = Date.now()
@@ -436,6 +435,7 @@ export default {
}
}
}).then(res => {
this.showProgress = true
this.executionInfo.status = 'running'
this.currentTaskId = res.task_id
this.xtermConfig = { taskId: this.currentTaskId, type: 'shortcut_cmd' }

View File

@@ -32,6 +32,22 @@ export default {
}
}
},
computed: {
passwordMenuDisabled() {
return this.$store.state.users.profile.source.value !== 'local'
}
},
watch: {
passwordMenuDisabled: {
immediate: true,
handler(disabled) {
this.config.submenu = this.getSubmenu()
if (disabled && this.config.activeMenu === 'Password') {
this.config.activeMenu = 'SSHKeyList'
}
}
}
},
methods: {
getSubmenu() {
return [

View File

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

View File

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

View File

@@ -147,7 +147,7 @@ export default {
this.loading = true
const url = `/api/v1/tickets/comments/?ticket_id=${this.object.id}`
this.$axios.get(url).then(res => {
this.comments = res.results
this.comments = res
}).catch(err => {
this.$message.error(err)
}).finally(() => {

View File

@@ -161,8 +161,7 @@ module.exports = {
target: process.env.VUE_APP_CORE_HOST || 'http://127.0.0.1:8080',
changeOrigin: true
}
},
after: require('./mock/mock-server.js')
}
},
css: {},
configureWebpack: {