Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
2f8be1c398 perf: Update Dockerfile with new base image tag 2025-04-08 11:33:43 +00:00
398 changed files with 3473 additions and 9501 deletions

View File

@@ -14,97 +14,64 @@ module.exports = {
window: true,
_: true
},
plugins: ['vue', 'spellcheck'],
// add your custom rules here
// it is base on https://github.com/vuejs/eslint-config-vue
rules: {
'vue/max-attributes-per-line': [
2,
{
singleline: 10,
multiline: {
max: 1,
allowFirstLine: false
}
'vue/max-attributes-per-line': [2, {
'singleline': 10,
'multiline': {
'max': 1,
'allowFirstLine': false
}
],
}],
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/name-property-casing': ['error', 'PascalCase'],
'vue/no-v-html': 'off',
'accessor-pairs': 2,
'arrow-spacing': [
2,
{
before: true,
after: true
}
],
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [
2,
'1tbs',
{
allowSingleLine: true
}
],
camelcase: [
0,
{
properties: 'always'
}
],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [
2,
{
before: false,
after: true
}
],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
curly: [2, 'multi-line'],
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
eqeqeq: ['error', 'always', { null: 'ignore' }],
'generator-star-spacing': [
2,
{
before: true,
after: true
}
],
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
indent: [
2,
2,
{
SwitchCase: 1
}
],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [
2,
{
beforeColon: false,
afterColon: true
}
],
'keyword-spacing': [
2,
{
before: true,
after: true
}
],
'new-cap': [
2,
{
newIsCap: true,
capIsNew: false
}
],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
@@ -135,23 +102,17 @@ module.exports = {
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [
2,
{
allowLoop: false,
allowSwitch: false
}
],
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [
2,
{
max: 1
}
],
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
@@ -179,125 +140,62 @@ module.exports = {
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [
2,
{
defaultAssignment: false
}
],
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [
2,
{
vars: 'all',
args: 'none'
}
],
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [
2,
{
initialized: 'never'
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
],
'operator-linebreak': [
2,
'after',
{
overrides: {
'?': 'before',
':': 'before'
}
}
],
}],
'padded-blocks': [2, 'never'],
quotes: [
2,
'single',
{
avoidEscape: true,
allowTemplateLiterals: true
}
],
semi: [2, 'never'],
'semi-spacing': [
2,
{
before: false,
after: true
}
],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [
2,
{ anonymous: 'never', named: 'never', asyncArrow: 'always' }
],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [
2,
{
words: true,
nonwords: false
}
],
'object-curly-spacing': [2, 'always'],
'spaced-comment': [
2,
'always',
{
markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}
],
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
yoda: [2, 'never'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'array-bracket-spacing': [2, 'never'],
'spellcheck/spell-checker': [
'warn',
{
comments: true,
strings: false,
identifiers: false,
lang: 'en_US',
skipWords: [
'echarts',
'resize',
'vue',
'eslint',
'babel',
'jsx',
'scss',
'v-deep',
'calc',
'vw',
'vh',
'px',
'rgba',
'rgb',
'var',
'lang',
'scoped',
'pdf',
'rbac'
],
skipIfMatch: [
'http://[^s]*',
'^[-\\w]+/[-\\w\\.]+$' // For import paths
],
minLength: 3
}
]
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

View File

@@ -1,11 +0,0 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": false,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}

View File

@@ -1,11 +0,0 @@
module.exports = {
singleQuote: true,
semi: false,
trailingComma: 'none',
printWidth: 100,
tabWidth: 2,
useTabs: false,
bracketSpacing: true,
arrowParens: 'avoid',
endOfLine: 'auto'
}

View File

@@ -1,4 +1,4 @@
FROM jumpserver/lina-base:20250805_081024 AS stage-build
FROM jumpserver/lina-base:20250408_112930 AS stage-build
ARG VERSION
ENV VERSION=$VERSION

View File

@@ -1,25 +1,13 @@
import Mock from 'mockjs'
import { param2Obj } from '../src/utils'
import user from './user'
import table from './table'
export function param2Obj(url) {
const search = url.split('?')[1]
if (!search) {
return {}
}
return JSON.parse(
'{"' +
decodeURIComponent(search)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"')
.replace(/\+/g, ' ') +
'"}'
)
}
const mocks = [...user, ...table]
const mocks = [
...user,
...table
]
// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,

View File

@@ -1,150 +1,148 @@
{
"name": "lina",
"version": "v4.0.0",
"description": "JumpServer Web UI",
"author": "JumpServer Team <support@lxware.hk>",
"license": "GPL-3.0-or-later",
"scripts": {
"dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"fix": "eslint --ext .js,.vue --fix src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icas/svgo.yml",
"vue-i18n-extract": "vue-i18n-extract",
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'",
"vue-i18n-report-json": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -o /tmp/abc.json",
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a",
"diff-i18n": "python ./src/i18n/langs/i18n-util.py diff en ja zh_Hant",
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja zh_Hant"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@fontsource/open-sans": "^5.0.24",
"@traptitech/markdown-it-katex": "^3.6.0",
"@ztree/ztree_v3": "3.5.44",
"axios": "0.28.0",
"axios-retry": "^3.1.9",
"caniuse-lite": "^1.0.30001642",
"cron-parser": "^4.0.0",
"crypto-js": "^4.1.1",
"css-color-function": "^1.3.3",
"decimal.js": "^10.4.3",
"deepmerge": "^4.2.2",
"dompurify": "^3.1.6",
"echarts": "4.7.0",
"element-ui": "^2.15.14",
"elementui-lts": "^2.16.0",
"eslint-plugin-html": "^6.0.0",
"highlight.js": "^11.9.0",
"install": "^0.13.0",
"jquery": "^3.6.1",
"js-cookie": "2.2.0",
"jsencrypt": "^3.2.1",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"lodash": "^4.17.21",
"lodash.clonedeep": "^4.5.0",
"lodash.frompairs": "^4.0.1",
"lodash.get": "^4.4.2",
"lodash.has": "^4.5.2",
"lodash.includes": "^4.3.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isplainobject": "^4.0.6",
"lodash.set": "^4.3.2",
"lodash.topairs": "^4.3.0",
"lodash.values": "^4.3.0",
"markdown-it": "^13.0.2",
"markdown-it-link-attributes": "^4.0.1",
"moment": "^2.29.4",
"moment-parseformat": "^4.0.0",
"normalize.css": "7.0.0",
"npm": "^7.8.0",
"nprogress": "0.2.0",
"path-to-regexp": "3.3.0",
"sortablejs": "^1.15.6",
"v-sanitize": "^0.0.13",
"vue": "2.6.10",
"vue-codemirror": "4.0.6",
"vue-cookie": "^1.1.4",
"vue-echarts": "^5.0.0-beta.0",
"vue-i18n": "^8.15.5",
"vue-json-editor": "^1.4.3",
"vue-markdown": "^2.2.4",
"vue-password-strength-meter": "^1.7.2",
"vue-router": "3.0.6",
"vue-select": "^3.9.5",
"vuejs-logger": "^1.5.4",
"vuex": "3.1.0",
"watermark-js-plus": "^1.5.8",
"xss": "^1.0.14",
"xterm": "^4.5.0",
"xterm-addon-fit": "^0.3.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "7.18.6",
"@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.6.0",
"@vue/cli-plugin-eslint": "^3.9.1",
"@vue/cli-plugin-unit-jest": "3.6.3",
"@vue/cli-service": "3.6.0",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "^9.5.1",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
"chalk": "2.4.2",
"compression-webpack-plugin": "^6.1.1",
"connect": "3.6.6",
"deasync": "^0.1.29",
"eslint": "^5.15.3",
"eslint-plugin-spellcheck": "^0.0.20",
"eslint-plugin-vue": "5.2.2",
"eslint-plugin-vue-i18n": "^0.3.0",
"github-markdown-css": "^5.1.0",
"html-webpack-plugin": "3.2.0",
"husky": "^4.2.3",
"less-loader": "^5.0.0",
"lint-staged": "^10.1.2",
"mockjs": "1.0.1-beta3",
"pretty-bytes": "^5.6.0",
"runjs": "^4.3.2",
"sass": "~1.32.6",
"sass-loader": "^7.1.0",
"script-ext-html-webpack-plugin": "2.1.3",
"script-loader": "0.7.2",
"serve-static": "^1.16.0",
"strip-ansi": "^7.1.0",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"vue-i18n-extract": "^1.1.1",
"vue-template-compiler": "2.6.10"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 4 versions",
"ie 11"
],
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix"
]
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
"name": "lina",
"version": "v4.0.0",
"description": "JumpServer Web UI",
"author": "JumpServer Team <support@lxware.hk>",
"license": "GPL-3.0-or-later",
"scripts": {
"dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"fix": "eslint --ext .js,.vue --fix src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icas/svgo.yml",
"vue-i18n-extract": "vue-i18n-extract",
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'",
"vue-i18n-report-json": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -o /tmp/abc.json",
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a",
"diff-i18n": "python ./src/i18n/langs/i18n-util.py diff en ja zh_Hant",
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja zh_Hant"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@fontsource/open-sans": "^5.0.24",
"@traptitech/markdown-it-katex": "^3.6.0",
"@ztree/ztree_v3": "3.5.44",
"axios": "0.28.0",
"axios-retry": "^3.1.9",
"caniuse-lite": "^1.0.30001642",
"cron-parser": "^4.0.0",
"crypto-js": "^4.1.1",
"css-color-function": "^1.3.3",
"decimal.js": "^10.4.3",
"deepmerge": "^4.2.2",
"dompurify": "^3.1.6",
"echarts": "4.7.0",
"element-ui": "2.15.14",
"eslint-plugin-html": "^6.0.0",
"highlight.js": "^11.9.0",
"install": "^0.13.0",
"jquery": "^3.6.1",
"js-cookie": "2.2.0",
"jsencrypt": "^3.2.1",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"lodash": "^4.17.21",
"lodash.clonedeep": "^4.5.0",
"lodash.frompairs": "^4.0.1",
"lodash.get": "^4.4.2",
"lodash.has": "^4.5.2",
"lodash.includes": "^4.3.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isplainobject": "^4.0.6",
"lodash.set": "^4.3.2",
"lodash.topairs": "^4.3.0",
"lodash.values": "^4.3.0",
"markdown-it": "^13.0.2",
"markdown-it-link-attributes": "^4.0.1",
"moment": "^2.29.4",
"moment-parseformat": "^4.0.0",
"normalize.css": "7.0.0",
"npm": "^7.8.0",
"nprogress": "0.2.0",
"path-to-regexp": "3.3.0",
"v-sanitize": "^0.0.13",
"vue": "2.6.10",
"vue-codemirror": "4.0.6",
"vue-cookie": "^1.1.4",
"vue-echarts": "^5.0.0-beta.0",
"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",
"vuejs-logger": "^1.5.4",
"vuex": "3.1.0",
"watermark-js-plus": "^1.5.8",
"xss": "^1.0.14",
"xterm": "^4.5.0",
"xterm-addon-fit": "^0.3.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "7.18.6",
"@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.6.0",
"@vue/cli-plugin-eslint": "^3.9.1",
"@vue/cli-plugin-unit-jest": "3.6.3",
"@vue/cli-service": "3.6.0",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "^9.5.1",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
"chalk": "2.4.2",
"compression-webpack-plugin": "^6.1.1",
"connect": "3.6.6",
"deasync": "^0.1.29",
"element-theme-chalk": "^2.13.1",
"eslint": "^5.15.3",
"eslint-plugin-vue": "5.2.2",
"eslint-plugin-vue-i18n": "^0.3.0",
"github-markdown-css": "^5.1.0",
"html-webpack-plugin": "3.2.0",
"husky": "^4.2.3",
"less-loader": "^5.0.0",
"lint-staged": "^10.1.2",
"mockjs": "1.0.1-beta3",
"runjs": "^4.3.2",
"sass": "~1.32.6",
"sass-loader": "^7.1.0",
"script-ext-html-webpack-plugin": "2.1.3",
"script-loader": "0.7.2",
"serve-static": "^1.16.0",
"strip-ansi": "^7.1.0",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"vue-i18n-extract": "^1.1.1",
"vue-template-compiler": "2.6.10"
},
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 4 versions",
"ie 11"
],
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix"
]
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -44,48 +44,17 @@ export default {
}
},
methods: {
getWaterMarkFields() {
const user = this.currentUser
const userId = user?.id || ''
const name = user?.name || ''
const userName = user?.username || ''
const currentTime = this.$moment(new Date()).format('YYYY-MM-DD HH:mm:ss')
return { userId, name, userName, currentTime }
},
getWaterMarkContent() {
const fields = this.getWaterMarkFields()
const template = this.publicSettings.SECURITY_WATERMARK_CONSOLE_CONTENT || ''
// 找出模板中所有的变量占位符 ${xxx}
const placeholders = template.match(/\${([^}]+)}/g) || []
const allVariables = {}
// 为模板中的每个变量准备值
placeholders.forEach(placeholder => {
const varName = placeholder.slice(2, -1) // 提取变量名,去掉 ${ 和 }
allVariables[varName] = fields[varName] !== undefined ? fields[varName] : 'N/A'
})
// 合并用户现有的字段和模板中可能缺失的字段
const safeFields = { ...fields, ...allVariables }
// 安全解析模板
return new Function(...Object.keys(safeFields), `return \`${template}\`;`)(...Object.values(safeFields))
},
createWatermark() {
if (this.currentUser?.username && this.publicSettings?.SECURITY_WATERMARK_ENABLED) {
this.watermark = new Watermark({
content: this.getWaterMarkContent(),
width: this.publicSettings?.SECURITY_WATERMARK_WIDTH,
height: this.publicSettings?.SECURITY_WATERMARK_HEIGHT,
rotate: this.publicSettings?.SECURITY_WATERMARK_ROTATE,
content: `${this.currentUser.username}(${this.currentUser.name})`,
width: 200,
height: 200,
rotate: 45,
fontWeight: 'normal',
fontSize: this.publicSettings?.SECURITY_WATERMARK_FONT_SIZE + 'px',
fontColor: this.publicSettings?.SECURITY_WATERMARK_COLOR,
contentType: 'multi-line-text',
lineHeight: this.publicSettings?.SECURITY_WATERMARK_FONT_SIZE
fontColor: 'rgba(128, 128, 128, 0.2)'
})
this.watermark.create()
}
}

View File

@@ -16,9 +16,9 @@ export function getSystemUserList(data) {
})
}
export function getZoneList(data) {
export function getDomainList(data) {
return request({
url: '/api/v1/assets/zones/',
url: '/api/v1/assets/domains/',
method: 'get',
params: data
})

View File

@@ -8,20 +8,11 @@ export function login(data) {
})
}
export async function getProfile(token) {
let profile = await request({
export function getProfile(token) {
return request({
url: '/api/v1/users/profile/',
method: 'get'
})
const perms = await request({
url: '/api/v1/users/profile/permissions/',
method: 'get'
})
profile = {
...profile,
...perms
}
return profile
}
export function getUserList(data) {
@@ -31,7 +22,6 @@ export function getUserList(data) {
params: data
})
}
export function getUserGroupList(params) {
return request({
url: '/api/v1/users/groups/',
@@ -39,7 +29,6 @@ export function getUserGroupList(params) {
params: params
})
}
export function getUserGroupDetail(id) {
return request({
url: `/api/v1/users/groups/${id}/`,
@@ -61,7 +50,6 @@ export function editUserGroup(data) {
data: data
})
}
export function updateUserGroup(id, data) {
return request({
url: '/api/v1/users/groups/' + id + '/',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1 +0,0 @@
<?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="1752631175762" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4421" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M93.866667 234.666667c-34.133333 12.8-51.2 25.6-51.2 34.133333 0 4.266667 51.2 64 115.2 136.533333 64 68.266667 115.2 128 115.2 128s-51.2 59.733333-115.2 128C93.866667 729.6 42.666667 793.6 42.666667 797.866667c4.266667 17.066667 59.733333 42.666667 98.133333 42.666666 64 0 81.066667-12.8 217.6-162.133333 68.266667-76.8 128-140.8 128-140.8 0-4.266667-55.466667-64-123.733333-140.8-85.333333-102.4-132.266667-145.066667-153.6-157.866667-29.866667-12.8-81.066667-17.066667-115.2-4.266666z m725.333333 4.266666c-21.333333 8.533333-68.266667 59.733333-153.6 153.6-68.266667 76.8-123.733333 140.8-123.733333 140.8 0 4.266667 55.466667 68.266667 128 140.8 136.533333 153.6 153.6 162.133333 217.6 162.133334 42.666667 0 98.133333-21.333333 98.133333-42.666667 0-4.266667-51.2-68.266667-115.2-136.533333-64-68.266667-115.2-128-115.2-128s51.2-59.733333 115.2-128c64-68.266667 115.2-128 115.2-136.533334-4.266667-17.066667-55.466667-38.4-98.133333-38.4-34.133333 0-46.933333 4.266667-68.266667 12.8z" fill="#E57000" p-id="4422"></path><path d="M238.933333 136.533333c-42.666667 21.333333-42.666667 25.6-4.266666 68.266667 221.866667 243.2 273.066667 302.933333 277.333333 302.933333 4.266667 0 311.466667-332.8 315.733333-341.333333 0-4.266667-8.533333-12.8-21.333333-21.333333-17.066667-12.8-34.133333-17.066667-68.266667-17.066667-64-4.266667-85.333333 8.533333-162.133333 93.866667-34.133333 38.4-64 72.533333-64 72.533333s-29.866667-29.866667-64-68.266667c-34.133333-38.4-72.533333-76.8-89.6-85.333333-25.6-17.066667-89.6-21.333333-119.466667-4.266667zM354.133333 725.333333c-85.333333 93.866667-153.6 170.666667-153.6 174.933334 0 4.266667 8.533333 12.8 21.333334 21.333333 17.066667 12.8 34.133333 17.066667 68.266666 17.066667 59.733333 4.266667 85.333333-8.533333 162.133334-98.133334 34.133333-38.4 64-68.266667 64-68.266666s29.866667 29.866667 64 68.266666c81.066667 89.6 98.133333 102.4 162.133333 98.133334 34.133333 0 51.2-4.266667 68.266667-17.066667 12.8-8.533333 21.333333-17.066667 21.333333-21.333333-4.266667-8.533333-311.466667-345.6-315.733333-341.333334-8.533333 0-76.8 76.8-162.133334 166.4z" p-id="4423"></path></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 645 B

View File

@@ -1,6 +1,5 @@
import { UpdateToken, UploadSecret } from '@/components/Form/FormFields'
import Select2 from '@/components/Form/FormFields/Select2.vue'
import AssetSelect from '@/components/Apps/AssetSelect/index.vue'
import { Required, RequiredChange } from '@/components/Form/DataForm/rules'
import AutomationParamsForm from '@/views/assets/Platform/AutomationParamsSetting.vue'
@@ -16,11 +15,17 @@ export const accountFieldsMeta = (vm) => {
return {
assets: {
component: AssetSelect,
label: vm.$t('Asset'),
component: Select2,
label: vm.$t('Assets'),
rules: [Required],
el: {
multiple: false
multiple: true,
ajax: {
url: '/api/v1/assets/assets/',
transformOption: (item) => {
return { label: item.name + '(' + item.address + ')', value: item.id }
}
}
},
hidden: () => {
return vm.platform || vm.asset

View File

@@ -11,7 +11,7 @@
<script>
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
export default {
@@ -63,7 +63,7 @@ export default {
encryptedFields: ['secret'],
fields: [
[this.$t('Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']],
[this.$t('Asset'), ['assets']],
[this.$t('Assets'), ['assets']],
[this.$t('Secret'), [
'secret_type', 'password', 'ssh_key', 'token',
'access_key', 'passphrase', 'api_key',

View File

@@ -11,7 +11,7 @@
<script>
import { GenericUpdateFormDialog } from '@/layout/components'
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'AccountBulkUpdateDialog',

View File

@@ -51,8 +51,8 @@
<script>
import { mapGetters } from 'vuex'
import { accountOtherActions, accountQuickFilters, connectivityMeta, isDirectoryServiceAccount } from './const'
import { openTaskPage } from '@/utils/jms/index'
import { accountOtherActions, accountQuickFilters, connectivityMeta } from './const'
import { openTaskPage } from '@/utils/jms'
import {
AccountConnectFormatter,
ActionsFormatter,
@@ -216,7 +216,6 @@ export default {
width: '80px',
formatter: AccountConnectFormatter,
formatterArgs: {
asset: this.asset,
can: ({ row }) => {
return this.currentUserIsSuperAdmin
}
@@ -284,15 +283,10 @@ export default {
formatter: ActionsFormatter,
has: this.showActions,
formatterArgs: {
performDelete: ({ row }) => {
const id = row.id
const url = `/api/v1/accounts/accounts/${id}/`
return this.$axios.delete(url)
},
hasUpdate: false, // can set function(row, value)
hasDelete: true, // can set function(row, value)
hasClone: false,
canDelete: ({ row }) => vm.$hasPerm('accounts.delete_account') && !isDirectoryServiceAccount(row, this),
canDelete: () => vm.$hasPerm('accounts.delete_account'),
moreActionsTitle: this.$t('More'),
extraActions: accountOtherActions(this)
}
@@ -341,7 +335,7 @@ export default {
can: () => {
return vm.$hasPerm('accounts.add_account') && !vm.$store.getters.currentOrgIsRoot
},
callback: async () => {
callback: async() => {
await this.getAssetDetail()
setTimeout(() => {
vm.iAsset = this.asset
@@ -362,7 +356,7 @@ export default {
can: ({ selectedRows }) => {
return selectedRows.length > 0 &&
['clickhouse', 'redis', 'website', 'chatgpt'].indexOf(selectedRows[0].asset.type.value) === -1 &&
!this.$store.getters.currentOrgIsRoot && vm.$hasPerm('accounts.verify_account')
!this.$store.getters.currentOrgIsRoot
},
callback: function({ selectedRows }) {
const ids = selectedRows.map(v => {
@@ -478,7 +472,7 @@ export default {
this.$refs.ListTable.reloadTable()
},
async getAssetDetail() {
const { query: { asset } } = this.$route
const { query: { asset }} = this.$route
if (asset) {
this.iAsset = await this.$axios.get(`/api/v1/assets/assets/${asset}/`)
}

View File

@@ -12,7 +12,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import { openTaskPage } from '@/utils/jms/index'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'RemoveAccount',

View File

@@ -23,7 +23,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
export default {

View File

@@ -61,7 +61,7 @@
import Dialog from '@/components/Dialog/index.vue'
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
import { SecretViewerFormatter } from '@/components/Table/TableFormatters'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'ShowSecretInfo',

View File

@@ -1,5 +1,5 @@
import { ChoicesFormatter } from '@/components/Table/TableFormatters'
import { openTaskPage } from '@/utils/jms/index'
import { openTaskPage } from '@/utils/jms'
export const connectivityMeta = {
formatter: ChoicesFormatter,
@@ -7,23 +7,11 @@ export const connectivityMeta = {
faChoices: {
'-': '',
ok: 'fa-check-circle',
err: 'fa-times-circle',
auth_err: 'fa-times-circle',
rdp_err: 'fa-times-circle',
password_err: 'fa-times-circle',
openssh_key_err: 'fa-times-circle',
ntlm_err: 'fa-times-circle',
create_temp_err: 'fa-times-circle'
err: 'fa-times-circle'
},
classChoices: {
ok: 'text-primary',
err: 'text-danger',
auth_err: 'text-danger',
rdp_err: 'text-danger',
password_err: 'text-danger',
openssh_key_err: 'text-danger',
ntlm_err: 'text-danger',
create_temp_err: 'text-danger'
err: 'text-danger'
},
getText({ cellValue }) {
if (cellValue?.value === '-' || cellValue?.value === 'unknown') {
@@ -36,200 +24,164 @@ export const connectivityMeta = {
width: '130px'
}
export function isDirectoryServiceAccount(account, vm) {
return vm.asset && vm.asset.id !== account.asset.id
}
export const accountOtherActions = vm => {
return [
{
name: 'View',
title: vm.$t('View'),
can: vm.$hasPerm('accounts.view_accountsecret'),
type: 'primary',
order: 1,
callback: ({ row }) => {
// debugger
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
vm.account = row
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
},
{
name: 'Update',
title: vm.$t('Edit'),
can: ({ row }) => {
return (
vm.$hasPerm('accounts.change_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
callback: ({ row }) => {
vm.isUpdateAccount = true
const data = {
...vm.asset,
...row.asset
}
vm.iAsset = data
vm.account = row
vm.addTemplate = false
vm.showAddDialog = false
setTimeout(() => {
vm.showAddDialog = true
})
}
},
{
name: 'UpdateSecret',
title: vm.$t('EditSecret'),
can: ({ row }) => {
return (
vm.$hasPerm('accounts.change_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
callback: ({ row }) => {
const data = {
...vm.asset,
...row.asset
}
vm.account = row
vm.iAsset = data
vm.showUpdateSecretDialog = false
vm.accountCreateUpdateTitle = vm.$t('UpdateAccount')
setTimeout(() => {
vm.showUpdateSecretDialog = true
})
}
},
{
name: 'Clone',
title: vm.$t('Duplicate'),
has: () => {
return !vm.asset
},
can: ({ row }) => {
return (
vm.$hasPerm('accounts.add_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
callback: ({ row }) => {
vm.account = {
name: `${row.name} - ${vm.$t('Duplicate').toLowerCase()}`,
username: `${row.username} - ${vm.$t('Duplicate').toLowerCase()}`,
payload: 'pam_account_clone'
}
vm.iAsset = vm.asset
vm.showAddDialog = false
setTimeout(() => {
vm.showAddDialog = true
})
}
},
{
name: 'Test',
title: vm.$t('VerifySecret'),
divided: true,
can: ({ row }) =>
!vm.$store.getters.currentOrgIsRoot &&
vm.$hasPerm('accounts.verify_account') &&
row.asset['auto_config'].ansible_enabled &&
row.asset['auto_config'].ping_enabled,
callback: ({ row }) => {
vm.$axios
.post(`/api/v1/accounts/accounts/tasks/`, { action: 'verify', accounts: [row.id] })
.then(res => {
openTaskPage(res['task'])
})
}
},
{
name: 'ClearSecret',
title: vm.$t('ClearSecret'),
can: ({ row }) => {
return vm.$hasPerm('accounts.change_account') && !isDirectoryServiceAccount(row, vm)
},
type: 'primary',
callback: ({ row }) => {
vm.$axios
.patch(`/api/v1/accounts/accounts/clear-secret/`, { account_ids: [row.id] })
.then(() => {
vm.$message.success(vm.$tc('ClearSuccessMsg'))
})
}
},
{
name: 'SecretHistory',
title: vm.$t('HistoryPassword'),
can: () => vm.$hasPerm('accounts.view_accountsecret'),
type: 'primary',
callback: ({ row }) => {
vm.account = row
vm.currentAccountColumn = row
vm.showViewSecretDialog = false
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
},
{
name: 'CopyToOther',
title: vm.$t('CopyToAsset'),
type: 'primary',
divided: true,
can: ({ row }) => {
return (
vm.$hasPerm('accounts.add_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
has: () => {
return !vm.asset
},
callback: ({ row }) => {
vm.accountCreateUpdateTitle = vm.$t('CopyToOther')
vm.$route.query.flag = 'copy'
vm.iAsset = vm.asset
vm.account = row
vm.showAddDialog = true
}
},
{
name: 'MoveToOther',
title: vm.$t('MoveToAsset'),
type: 'primary',
can: ({ row }) => {
return (
vm.$hasPerm('accounts.delete_account') &&
!vm.$store.getters.currentOrgIsRoot &&
!isDirectoryServiceAccount(row, vm)
)
},
has: () => {
return !vm.asset
},
callback: ({ row }) => {
vm.accountCreateUpdateTitle = vm.$t('MoveToOther')
vm.$route.query.flag = 'move'
vm.iAsset = vm.asset
vm.account = row
vm.showAddDialog = true
}
export const accountOtherActions = (vm) => [
{
name: 'View',
title: vm.$t('View'),
can: vm.$hasPerm('accounts.view_accountsecret'),
type: 'primary',
order: 1,
callback: ({ row }) => {
// debugger
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
vm.account = row
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
]
}
},
{
name: 'Update',
title: vm.$t('Edit'),
can: vm.$hasPerm('accounts.change_account') && !vm.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
vm.isUpdateAccount = true
const data = {
...vm.asset,
...row.asset
}
vm.iAsset = data
vm.account = row
vm.addTemplate = false
vm.showAddDialog = false
setTimeout(() => {
vm.showAddDialog = true
})
}
},
{
name: 'UpdateSecret',
title: vm.$t('EditSecret'),
can: vm.$hasPerm('accounts.change_account') && !vm.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
const data = {
...vm.asset,
...row.asset
}
vm.account = row
vm.iAsset = data
vm.showUpdateSecretDialog = false
vm.accountCreateUpdateTitle = vm.$t('UpdateAccount')
setTimeout(() => {
vm.showUpdateSecretDialog = true
})
}
},
{
name: 'Clone',
title: vm.$t('Duplicate'),
has: () => {
return !vm.asset
},
can: vm.$hasPerm('accounts.add_account') && !vm.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
vm.account = {
name: `${row.name} - ${vm.$t('Duplicate').toLowerCase()}`,
username: `${row.username} - ${vm.$t('Duplicate').toLowerCase()}`,
payload: 'pam_account_clone'
}
vm.iAsset = vm.asset
export const accountQuickFilters = vm => [
vm.showAddDialog = false
setTimeout(() => {
vm.showAddDialog = true
})
}
},
{
name: 'Test',
title: vm.$t('VerifySecret'),
divided: true,
can: ({ row }) =>
!vm.$store.getters.currentOrgIsRoot &&
vm.$hasPerm('accounts.verify_account') &&
row.asset['auto_config'].ansible_enabled &&
row.asset['auto_config'].ping_enabled,
callback: ({ row }) => {
vm.$axios.post(
`/api/v1/accounts/accounts/tasks/`,
{ action: 'verify', accounts: [row.id] }
).then(res => {
openTaskPage(res['task'])
})
}
},
{
name: 'ClearSecret',
title: vm.$t('ClearSecret'),
can: vm.$hasPerm('accounts.change_account'),
type: 'primary',
callback: ({ row }) => {
vm.$axios.patch(
`/api/v1/accounts/accounts/clear-secret/`,
{ account_ids: [row.id] }
).then(() => {
vm.$message.success(vm.$tc('ClearSuccessMsg'))
})
}
},
{
name: 'SecretHistory',
title: vm.$t('HistoryPassword'),
can: () => vm.$hasPerm('accounts.view_accountsecret'),
type: 'primary',
callback: ({ row }) => {
vm.account = row
vm.currentAccountColumn = row
vm.showViewSecretDialog = false
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
},
{
name: 'CopyToOther',
title: vm.$t('CopyToAsset'),
type: 'primary',
divided: true,
can: () => vm.$hasPerm('accounts.add_account') && !vm.$store.getters.currentOrgIsRoot,
has: () => {
return !vm.asset
},
callback: ({ row }) => {
vm.accountCreateUpdateTitle = vm.$t('CopyToOther')
vm.$route.query.flag = 'copy'
vm.iAsset = vm.asset
vm.account = row
vm.showAddDialog = true
}
},
{
name: 'MoveToOther',
title: vm.$t('MoveToAsset'),
type: 'primary',
can: () => vm.$hasPerm('accounts.add_account') && !vm.$store.getters.currentOrgIsRoot,
has: () => {
return !vm.asset
},
callback: ({ row }) => {
vm.accountCreateUpdateTitle = vm.$t('MoveToOther')
vm.$route.query.flag = 'move'
vm.iAsset = vm.asset
vm.account = row
vm.showAddDialog = true
}
}
]
export const accountQuickFilters = (vm) => [
{
label: vm.$t('Recent (7 days)'),
options: [

View File

@@ -20,7 +20,7 @@
<script>
import TreeTable from '../../Table/TreeTable/index.vue'
import { setRouterQuery, setUrlParam } from '@/utils/common/index'
import { setRouterQuery, setUrlParam } from '@/utils/common'
import $ from '@/utils/jquery-vendor'
export default {
@@ -114,11 +114,6 @@ export default {
treeUrl: `${this.typeUrl}?assets=${showAssets ? '1' : '0'}&count_resource=${this.treeSetting.countResource || 'asset'}`,
callback: {
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode)
},
edit: {
drag: {
isMove: false
}
}
}
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="container">
<div v-if="hasPrompt" class="chat-action">
<div class="chat-action">
<Select2
v-model="select.value"
:disabled="isLoading || isSelectDisabled"
@@ -36,10 +36,6 @@ 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" @insert-code="handleInsertCode" />
<MessageText :message="item.reasoning" />
</p>
</div>
@@ -41,7 +41,8 @@
<span v-if="isServerError" class="error">
{{ isServerError }}
</span>
<MessageText :message="item.result" :is-terminal="isTerminal" @insert-code="handleInsertCode" /></div>
<MessageText :message="item.result" />
</div>
</div>
</div>
<div class="action">
@@ -77,9 +78,9 @@
<script>
import MessageText from './MessageText.vue'
import { mapGetters, mapState } from 'vuex'
import { copy } from '@/utils/common/index'
import { copy } from '@/utils/common'
import { useChat } from '../../useChat.js'
import { reconnect } from '@/utils/request'
import { reconnect } from '@/utils/socket'
const { setLoading, removeLoadingMessageInChat } = useChat()
@@ -92,10 +93,6 @@ export default {
type: Object,
default: () => {
}
},
isTerminal: {
type: Boolean,
default: false
}
},
data() {
@@ -145,9 +142,6 @@ export default {
if (value === 'copy') {
copy(this.item.result.content)
}
},
handleInsertCode(code) {
this.$emit('insert-code', code)
}
}
}

View File

@@ -17,7 +17,7 @@ import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
import { copy } from '@/utils/common/index'
import { copy } from '@/utils/common'
export default {
props: {
@@ -25,10 +25,6 @@ export default {
type: Object,
default: () => {
}
},
isTerminal: {
type: Boolean,
default: false
}
},
data() {
@@ -49,10 +45,10 @@ export default {
this.init()
},
updated() {
this.addEvents()
this.addCopyEvents()
},
destroyed() {
this.removeEvents()
this.removeCopyEvents()
},
methods: {
init() {
@@ -69,68 +65,30 @@ export default {
return vm.highlightBlock(hljs.highlightAuto(code).value, '')
}
})
this.markdown.use(mila, { attrs: { target: '_blank', rel: 'noopener', class: 'link-style' } })
this.markdown.use(mila, { attrs: { target: '_blank', rel: 'noopener', class: 'link-style' }})
this.markdown.use(mdKatex, { blockClass: 'katexmath-block rounded-md', errorColor: ' #cc0000' })
},
highlightBlock(str, lang) {
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>`
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>`
},
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', () => {
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)
}
})
})
},
removeEvents() {
removeCopyEvents() {
if (this.$refs.textRef) {
this.removeBtnClickEvent('.code-block-header__copy')
this.addBtnClickEvents('.code-block-header__insert')
const copyBtn = this.$refs.textRef.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.removeEventListener('click', () => {
})
})
}
}
}
@@ -140,7 +98,6 @@ export default {
<style lang="scss" scoped>
.markdown-body {
font-size: 13px;
max-width: 300px;;
&::v-deep p {
margin-bottom: 0 !important;
@@ -158,46 +115,26 @@ export default {
&::v-deep .code-block-wrapper {
background: #1F2329;
padding: 0;
padding: 2px 6px;
margin: 5px 0;
display: flex;
flex-direction: column;
overflow: hidden;
.code-block-body {
padding: 5px 10px;
padding: 5px 10px 0;
}
;
.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__actions {
display: flex;
gap: 8px;
.code-block-header__copy {
float: right;
cursor: pointer;
.code-block-header__copy {
cursor: pointer;
&:hover {
color: #6e747b;
}
}
.code-block-header__insert {
cursor: pointer;
&:hover {
color: #6e747b;
}
&:hover {
color: #6e747b;
}
}
}
@@ -241,7 +178,6 @@ 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" :is-terminal="isTerminal" @insert-code="insertCode" />
<ChatMessage v-for="(item, index) in activeChat.chats" :key="index" :item="item" />
</div>
<div class="input-box">
<el-button
@@ -28,7 +28,7 @@
size="small"
@click="onStopHandle"
>{{ $tc('Stop') }}</el-button>
<ChatInput ref="chatInput" :expanded="expanded" :has-prompt="!isTerminal" @send="onSendHandle" @select-prompt="onSelectPromptHandle" />
<ChatInput ref="chatInput" :expanded="expanded" @send="onSendHandle" @select-prompt="onSelectPromptHandle" />
</div>
</div>
</template>
@@ -37,7 +37,7 @@
import ChatInput from './ChatInput.vue'
import ChatMessage from './ChatMessage.vue'
import { mapState } from 'vuex'
import { closeWebSocket, createWebSocket, onSend, ws } from '@/utils/request'
import { closeWebSocket, createWebSocket, onSend, ws } from '@/utils/socket'
import { getInputFocus, useChat } from '../../useChat.js'
const {
@@ -68,10 +68,7 @@ export default {
prompt: '',
conversationId: '',
showIntroduction: false,
introduction: [],
terminalContext: null,
isTerminal: false,
sessionChat: {}
introduction: []
}
},
computed: {
@@ -103,9 +100,6 @@ 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'),
@@ -156,32 +150,6 @@ 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 || {}
@@ -236,15 +204,6 @@ 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

@@ -1,6 +1,5 @@
<template>
<DrawerPanel
v-if="visible"
ref="drawer"
:default-show-panel="!!defaultShowPanel"
:expanded="expanded"
@@ -46,12 +45,9 @@
import Sidebar from './components/Sidebar/index.vue'
import Chat from './components/ChitChat/index.vue'
import { getInputFocus } from './useChat.js'
import { ws } from '@/utils/request'
import { ws } from '@/utils/socket'
import DrawerPanel from '@/components/Apps/DrawerPanel/index.vue'
import { ObjectLocalStorage } from '@/utils/common'
import { mapGetters } from 'vuex'
const aiPannelLocalStorage = new ObjectLocalStorage('ai_panel_settings')
export default {
components: {
DrawerPanel,
@@ -76,68 +72,23 @@ export default {
},
data() {
return {
visible: false,
active: 'chat',
robotUrl: require('@/assets/img/robot-assistant.png'),
height: '400px',
expanded: false,
clientOffset: {},
currentTerminalContent: {}
clientOffset: {}
}
},
computed: {
...mapGetters([
'publicSettings'
])
},
watch: {
'publicSettings.CHAT_AI_METHOD': {
handler(newVal) {
this.visible = newVal === 'api'
}
}
},
mounted() {
this.handleStartChat()
this.handlePostMessage()
},
methods: {
handleStartChat() {
if (this.publicSettings.CHAT_AI_METHOD === 'api') {
this.visible = true
const expanded = aiPannelLocalStorage.get('expanded')
this.updateExpandedState(expanded)
this.handlePostMessage()
} else if (this.publicSettings.CHAT_AI_METHOD === 'embed') {
const embedScriptId = 'chat-ai-embed-id'
if (document.getElementById(embedScriptId)) {
return
}
const script = document.createElement('script')
script.id = embedScriptId
script.src = this.publicSettings.CHAT_AI_EMBED_URL
script.async = true
script.onload = () => {
const loadEvent = new Event('load', { bubbles: false, cancelable: false })
window.dispatchEvent(loadEvent)
}
document.body.appendChild(script)
}
},
handlePostMessage() {
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
}
})
},
@@ -145,11 +96,6 @@ 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() {
@@ -161,20 +107,12 @@ export default {
this.$refs.drawer.show = false
},
expandFull() {
this.updateExpandedState(true)
this.save_pannel_settings()
this.height = '100%'
this.expanded = true
},
compress() {
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'
this.height = '400px'
this.expanded = false
},
onNewChat() {
this.active = 'chat'

View File

@@ -1,5 +1,5 @@
import store from '@/store'
import { pageScroll } from '@/utils/common/index'
import { pageScroll } from '@/utils/common'
export const getInputFocus = () => {
const dom = document.querySelector('.chat-input .el-textarea__inner')
@@ -9,11 +9,11 @@ export const getInputFocus = () => {
export function useChat() {
const chatStore = {}
const setLoading = loading => {
const setLoading = (loading) => {
store.commit('chat/setLoading', loading)
}
const onNewChat = name => {
const onNewChat = (name) => {
const data = {
name: name || `new chat`,
id: 1,
@@ -27,7 +27,7 @@ export function useChat() {
store.commit('chat/clearChats')
}
const addMessageToActiveChat = chat => {
const addMessageToActiveChat = (chat) => {
store.commit('chat/addMessageToActiveChat', chat)
}
@@ -35,7 +35,7 @@ export function useChat() {
store.commit('chat/removeLoadingMessageInChat')
}
const addChatMessageById = chat => {
const addChatMessageById = (chat) => {
store.commit('chat/addMessageToActiveChat', chat)
if (chat?.conversation_id) {
store.commit('chat/setActiveChatConversationId', chat.conversation_id)
@@ -54,7 +54,7 @@ export function useChat() {
addChatMessageById(temporaryChat)
}
const newChatAndAddMessageById = chat => {
const newChatAndAddMessageById = (chat) => {
onNewChat(chat.message.content)
addChatMessageById(chat)
}

View File

@@ -34,7 +34,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import { openTaskPage } from '@/utils/jms/index'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'GatewayDialog',

View File

@@ -85,7 +85,6 @@ export default {
hasTree: true,
columnsExtra: ['view_account'],
columnsExclude: ['spec_info'],
columns: ['id', 'name', 'address', 'comment', 'labels', 'connectivity', 'platform', 'view_account', 'actions'],
columnsShow: {
min: ['name', 'address', 'accounts'],
default: ['name', 'address', 'platform', 'view_account', 'actions']

View File

@@ -1,59 +0,0 @@
<template>
<ListTable ref="ListTable" :header-actions="headerActions" :table-config="tableConfig" :create-drawer="createDrawer" />
</template>
<script>
import { DrawerListTable as ListTable } from '@/components'
export default {
name: 'LeakPasswordList',
components: {
ListTable
},
props: {
object: {
type: Object,
required: false,
default: () => ({})
}
},
data() {
return {
createDrawer: () => import('@/components/Apps/LeakPasswords/LeakPasswordsCreateUpdate.vue'),
tableConfig: {
url: '/api/v1/settings/leak-passwords/',
columns: [
'password'
],
columnsMeta: {
actions: {
formatterArgs: {
hasClone: false,
canDelete: this.$hasPerm('settings.change_security'),
canUpdate: this.$hasPerm('settings.change_security')
}
}
}
},
headerActions: {
hasExport: false,
hasImport: false,
hasCreate: true,
hasSearch: true,
hasRefresh: true,
hasBulkDelete: true,
hasBulkUpdate: false,
hasLeftActions: true,
hasRightActions: true,
canCreate: this.$hasPerm('settings.change_security'),
canBulkDelete: this.$hasPerm('settings.change_security')
}
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -1,41 +0,0 @@
<template>
<div v-loading="loading">
<GenericCreateUpdatePage
v-if="!loading"
class="user-create-update"
v-bind="$data"
:title="null"
v-on="$listeners"
/>
</div>
</template>
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
export default {
name: 'LeakPasswordsCreateUpdate',
components: {
GenericCreateUpdatePage
},
data() {
return {
loading: true,
fields: [
[this.$t('Basic'), ['password']]
],
encryptedFields: [],
url: '/api/v1/settings/leak-passwords/'
}
},
async mounted() {
this.loading = false
},
methods: {}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,76 +0,0 @@
<template>
<div>
<div>
<el-button
size="mini"
type="primary"
@click="onOpenDialog"
>
{{ $tc('View') }}
</el-button>
</div>
<Dialog
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="visible"
v-bind="$attrs"
width="40%"
v-on="$listeners"
>
<LeakPasswordList />
</Dialog>
</div>
</template>
<script>
import { Dialog } from '@/components'
import LeakPasswordList from '@/components/Apps/LeakPasswords/LeakPasswordList.vue'
export default {
componentName: 'LeakPasswords',
components: {
LeakPasswordList,
Dialog
},
props: {
value: {
type: Object,
default: () => ({})
},
title: {
type: String,
default: function() {
return this.$t('LeakPasswordList')
}
},
url: {
type: String,
default: `/api/v1/settings/leak-passwords/`
}
},
data() {
return {
visible: false,
form: this.value,
config: {
url: this.url,
hasSaveContinue: false,
hasButtons: true,
fields: [],
fieldsMeta: {}
}
}
},
methods: {
onOpenDialog() {
this.visible = true
}
}
}
</script>
<style scoped>
</style>

View File

@@ -6,7 +6,7 @@
<script>
import { DrawerListTable as ListTable } from '@/components'
import { toM2MJsonParams } from '@/utils/jms/index'
import { toM2MJsonParams } from '@/utils/jms'
import { DetailFormatter } from '@/components/Table/TableFormatters'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
@@ -45,7 +45,7 @@ export default {
formatter: DetailFormatter,
formatterArgs: {
getRoute: ({ row }) => {
return { name: 'AssetDetail', params: { id: row.id } }
return { name: 'AssetDetail', params: { id: row.id }}
}
}
},

View File

@@ -6,7 +6,7 @@
<script>
import { DrawerListTable as ListTable } from '@/components'
import { toM2MJsonParams } from '@/utils/jms/index'
import { toM2MJsonParams } from '@/utils/jms'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
import { DetailFormatter } from '@/components/Table/TableFormatters'

View File

@@ -30,8 +30,8 @@
<script>
import IBox from '@/components/Common/IBox/index.vue'
import DiffDetail from '@/components/Dialog/DiffDetail.vue'
import { openTaskPage } from '@/utils/jms/index'
import { toSafeLocalDateStr } from '@/utils/common/time'
import { openTaskPage } from '@/utils/jms'
import { toSafeLocalDateStr } from '@/utils/time'
import TwoCol from '@/layout/components/Page/TwoColPage.vue'
export default {

View File

@@ -8,7 +8,7 @@
:visible.sync="visible"
class="dialog-content"
v-bind="$attrs"
width="600px"
width="740px"
@confirm="visible = false"
v-on="$listeners"
>
@@ -49,14 +49,24 @@
</el-select>
</el-col>
</el-row>
<el-row v-if="!noCodeMFA.includes(subTypeSelected)" :gutter="24" style="margin: 0 auto;">
<el-col :md="24" :sm="24" style="display: flex; align-items: center; ">
<el-row :gutter="24" style="margin: 0 auto;">
<el-col :md="24" :sm="24" style="display: flex; align-items: center; margin-bottom: 20px;">
<el-input
v-if="subTypeSelected !== 'face'"
v-model="secretValue"
:placeholder="inputPlaceholder"
:show-password="showPassword"
@keyup.enter.native="handleConfirm"
/>
<iframe
v-if="isFaceCaptureVisible && subTypeSelected ==='face' && faceCaptureUrl"
:src="faceCaptureUrl"
allow="camera"
sandbox="allow-scripts allow-same-origin"
style="width: 100%; height: 800px;border: none;"
/>
<span v-if="subTypeSelected === 'sms' || subTypeSelected === 'email'" style="margin: -1px 0 0 20px;">
<el-button
:disabled="smsBtnDisabled"
@@ -70,26 +80,10 @@
</span>
</el-col>
</el-row>
<el-row>
<el-col>
<iframe
v-if="passkeyVisible"
:src="passkeyUrl"
style="display: none"
/>
<iframe
v-if="isFaceCaptureVisible && subTypeSelected ==='face' && faceCaptureUrl"
:src="faceCaptureUrl"
allow="camera"
sandbox="allow-scripts allow-same-origin"
style="width: 100%; height: 600px;border: none;"
/>
</el-col>
</el-row>
<el-row :gutter="24" style="margin: 20px auto 10px;">
<el-row :gutter="24" style="margin: 10px auto;">
<el-col :md="24" :sm="24">
<el-button
v-if="!noCodeMFA.includes(subTypeSelected)"
v-if="subTypeSelected!=='face'"
class="confirm-btn"
size="mini"
type="primary"
@@ -98,8 +92,7 @@
{{ this.$t('Confirm') }}
</el-button>
<el-button
v-if="subTypeSelected === 'face'"
v-show="!isFaceCaptureVisible"
v-if="subTypeSelected==='face'&&!isFaceCaptureVisible"
class="confirm-btn"
size="mini"
type="primary"
@@ -107,16 +100,6 @@
>
{{ this.$tc('VerifyFace') }}
</el-button>
<el-button
v-if="subTypeSelected === 'passkey'"
v-loading="passkeyVisible"
class="confirm-btn"
size="mini"
type="primary"
@click="handlePasskeyVerify"
>
{{ this.$tc('Next') }}
</el-button>
</el-col>
</el-row>
</div>
@@ -124,7 +107,7 @@
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'UserConfirmDialog',
@@ -158,11 +141,7 @@ export default {
processing: false,
isFaceCaptureVisible: false,
faceToken: null,
faceCaptureUrl: null,
noCodeMFA: ['face', 'passkey'],
sendCodeMFA: ['email', 'sms', 'otp'],
passkeyVisible: false,
passkeyUrl: '/api/v1/authentication/passkeys/login/?mfa=1'
faceCaptureUrl: null
}
},
computed: {
@@ -195,7 +174,7 @@ export default {
this.$log.debug('perform confirm action')
const confirmType = response.data?.code
const confirmUrl = '/api/v1/authentication/confirm/'
this.$axios.get(confirmUrl, { params: { confirm_type: confirmType } }).then((data) => {
this.$axios.get(confirmUrl, { params: { confirm_type: confirmType }}).then((data) => {
this.confirmTypeRequired = data.confirm_type
if (this.confirmTypeRequired === 'relogin') {
@@ -245,26 +224,6 @@ export default {
this.$message.error(this.$tc('FailedToSendVerificationCode'))
})
},
handlePasskeyVerify() {
this.passkeyVisible = true
this.checkPasskeyStatus()
},
checkPasskeyStatus() {
const url = '/api/v1/authentication/confirm/check/?confirm_type=mfa'
const t = setInterval(() => {
this.$axios.get(url).then(data => {
this.passkeyVisible = false
this.onSuccess()
})
}, 2000)
setTimeout(() => {
clearInterval(t)
if (this.passkeyVisible) {
this.passkeyVisible = false
this.$message.error(this.$tc('PasskeyTimeout'))
}
}, 20000)
},
startFaceCapture() {
const url = '/api/v1/authentication/face/context/'
this.$axios.post(url).then(data => {
@@ -288,13 +247,6 @@ export default {
handleFaceCapture() {
this.startFaceCapture()
},
onSuccess() {
this.secretValue = ''
this.visible = false
this.$nextTick(() => {
this.callback()
})
},
handleConfirm() {
if (this.confirmTypeRequired === 'relogin') {
return this.logout()
@@ -310,7 +262,11 @@ export default {
}
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(() => {
this.onSuccess()
this.secretValue = ''
this.visible = false
this.$nextTick(() => {
this.callback()
})
}).catch((err) => {
this.$message.error(err.message || this.$tc('ConfirmFailed'))
this.faceCaptureUrl = null

View File

@@ -39,9 +39,6 @@ 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

@@ -1,5 +1,5 @@
<script type="text/jsx">
import { toSafeLocalDateStr } from '@/utils/common/time'
import { toSafeLocalDateStr } from '@/utils/time'
export default {
name: 'ItemValue',

View File

@@ -12,8 +12,8 @@
<script>
import DetailCard from './index.vue'
import { copy } from '@/utils/common/index'
import { toSafeLocalDateStr } from '@/utils/common/time'
import { copy } from '@/utils/common'
import { toSafeLocalDateStr } from '@/utils/time'
import IBox from '@/components/Common/IBox/index.vue'
import LabelsDetailFormatter from '../Formatters/LabelsDetailFormatter.vue'
@@ -129,18 +129,8 @@ export default {
parseArrayValue(value, excludes, label) {
if (Array.isArray(value)) {
const tp = typeof value[0]
let object = {}
if (value.length === 0) {
object = {
key: label,
value: '-'
}
return this.items.push(object)
}
// 如果是空数组,那么循环体将不会执行
for (const [index, item] of value.entries()) {
let object = {}
if (tp === 'object') {
const firstValue = value[0]
if (firstValue.hasOwnProperty('name')) {

View File

@@ -15,7 +15,7 @@
@click="handleClick(action)"
@command="handleDropdownCallback"
>
<span v-if="action.split" :style="{ cursor: action.disabled ? 'not-allowed' : 'pointer' }">
<span v-if="action.split">
{{ action.title }}
</span>
<el-button
@@ -89,7 +89,7 @@
</template>
<script>
import { toSentenceCase } from '@/utils/common/index'
import { toSentenceCase } from '@/utils/common'
import Icon from '@/components/Widgets/Icon/index.vue'
export default {

View File

@@ -1,23 +1,25 @@
<template>
<tr>
<td>{{ getActionTitle(action) }}</td>
<td>
{{ getActionTitle(action) }}
<el-tooltip v-if="action.attrs.showTip" :content="action.attrs.tip" :open-delay="500" effect="dark">
<i class="fa fa-question-circle-o" />
</el-tooltip>
</td>
<td>
<span slot="reference">
<component
:is="iType"
v-model="action.attrs.model"
:title="label"
v-bind="action.attrs"
v-on="callbacks"
>
{{ label }}
</component>
</span>
<el-popover
:content="action.attrs.tip"
:disabled="!action.attrs.showTip"
placement="left-end"
trigger="hover"
>
<span slot="reference">
<component
:is="iType"
v-model="action.attrs.model"
:title="label"
v-bind="action.attrs"
v-on="callbacks"
>
{{ label }}
</component>
</span>
</el-popover>
</td>
</tr>
</template>
@@ -26,7 +28,7 @@
import Switcher from '@/components/Form/FormFields/Switcher.vue'
import Select2 from '@/components/Form/FormFields/Select2.vue'
import UpdateSelect from '@/components/Form/FormFields/UpdateSelect.vue'
import { toSentenceCase } from '@/utils/common/index'
import { toSentenceCase } from '@/utils/common'
class Action {
constructor() {

View File

@@ -5,16 +5,10 @@
:options="options"
:autoresize="true"
theme="light"
:class="{'disabled-when-print': !!dataUrl}"
@finished="genSnapshot"
class="disabled-when-print"
@finished="getDataUrl"
/>
<img
v-if="dataUrl"
:src="dataUrl"
class="enabled-when-print"
style="display: none;width: 100%;"
alt="chart snapshot"
>
<img v-if="dataUrl" :src="dataUrl" class="enabled-when-print" style="display: none;width: 100%;">
</div>
</template>
@@ -116,6 +110,7 @@ export default {
},
axisLabel: {
textStyle: {
// 坐标轴颜色
color: '#8F959E'
}
},
@@ -160,7 +155,7 @@ export default {
type: 'line',
smooth: true,
areaStyle: {
// 区域填充样式
// 区域填充样式
normal: {
color: new echarts.graphic.LinearGradient(
0,
@@ -192,7 +187,7 @@ export default {
type: 'line',
smooth: true,
areaStyle: {
// 区域填充样式
// 区域填充样式
normal: {
color: new echarts.graphic.LinearGradient(
0,
@@ -225,58 +220,23 @@ export default {
},
watch: {
range() {
this.genSnapshot()
},
datesMetrics() {
this.genSnapshot()
},
primaryData() {
this.genSnapshot()
},
secondaryData() {
this.genSnapshot()
this.getMetricData()
}
},
mounted() {
this.genSnapshot()
this._before = () => this.genSnapshot(true)
this._after = () => this.forceResize()
window.addEventListener('beforeprint', this._before)
window.addEventListener('afterprint', this._after)
// 兼容某些浏览器Safari触发 print 媒体切换
this._mql = window.matchMedia && window.matchMedia('print')
if (this._mql) {
const handler = e => (e.matches ? this._before() : this._after())
this._mql.addEventListener?.('change', handler)
this._mql.addListener?.(handler)
this._mql._handler = handler
}
},
beforeDestroy() {
window.removeEventListener('beforeprint', this._before)
window.removeEventListener('afterprint', this._after)
if (this._mql) {
this._mql.removeEventListener?.('change', this._mql._handler)
this._mql.removeListener?.(this._mql._handler)
}
this.getMetricData()
},
methods: {
forceResize() {
const inst = this.$refs.echarts?.echartsInstance
if (inst) inst.resize()
},
async genSnapshot(force = false) {
if (force) this.forceResize()
const inst = this.$refs.echarts?.echartsInstance
if (!inst) return
try {
this.dataUrl = inst.getDataURL({ pixelRatio: 2, backgroundColor: '#ffffff' })
} catch (e) {
this.dataUrl = ''
getDataUrl() {
const instance = this.$refs.echarts.echartsInstance
if (instance) {
this.dataUrl = instance.getDataURL()
}
},
getMetricData() {
this.getDataUrl()
}
}
}
</script>
@@ -288,11 +248,13 @@ export default {
@media print {
.disabled-when-print {
display: none !important;
display: none;
}
.enabled-when-print {
display: block !important;
width: 100% !important;
display: inherit !important;
}
.print-margin {
margin-top: 10px;
}
}
</style>

View File

@@ -9,7 +9,7 @@
class="table"
style="width: 100%"
>
<el-table-column :label="$tc('Ranking')" width="100">
<el-table-column :label="$tc('Ranking')">
<template #header>
<el-tooltip :content="$t('Ranking')" placement="top" :open-delay="500">
<span style="cursor: pointer;">{{ $t('Ranking') }}</span>

View File

@@ -33,7 +33,7 @@ export default {
const formatTitle = (text) => {
if (!text) return ''
const maxLength = 25
const maxLength = 23
const lines = []
for (let i = 0; i < text.length; i += maxLength) {
lines.push(text.slice(i, i + maxLength))
@@ -88,7 +88,6 @@ export default {
{
name: title,
type: 'pie',
minAngle: 5,
radius: ['72%', '90%'],
avoidLabelOverlap: false,
itemStyle: {

View File

@@ -16,13 +16,11 @@
<script>
export default {
props: {
name: {
type: String,
default: 'dashboardDays'
},
days: {
type: [String, Number],
default: null
type: String,
default: () => {
return localStorage.getItem('dashboardDays') || '7'
}
},
options: {
type: Array,
@@ -49,28 +47,8 @@ export default {
iOptions: this.options.length > 0 ? this.options : defaultOptions
}
},
created() {
let days = this.days
if (!days) {
days = this.$route.query.days
}
if (!days) {
days = localStorage.getItem(this.name)
}
if (!days) {
days = '7'
}
if (days && days !== this.select) {
this.select = days
this.$emit('change', days)
}
},
mounted() {
this.$emit('change', this.select)
},
methods: {
onChange(val) {
localStorage.setItem(this.name, val)
this.$emit('change', val)
}
}

View File

@@ -28,7 +28,7 @@
<script>
import i18n from '@/i18n/i18n'
import { copy } from '@/utils/common/index'
import { copy } from '@/utils/common'
import Dialog from '@/components/Dialog/index'
export default {

View File

@@ -30,7 +30,7 @@
</template>
<script>
import { getDrawerWidth } from '@/utils/common/index'
import { getDrawerWidth } from '@/utils/common'
export default {
props: {

View File

@@ -94,7 +94,7 @@ export default {
},
methods: {
async optionUrlMetaAndGenerateColumns() {
let data = { actions: {} }
let data = { actions: {}}
if (this.url) {
data = await this.$store.dispatch('common/getUrlMeta', { url: this.url })
}
@@ -140,11 +140,14 @@ 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

@@ -4,7 +4,7 @@ import NestedField from '@/components/Form/AutoDataForm/components/NestedField.v
import rules from '@/components/Form/DataForm/rules'
import BasicTree from '@/components/Form/FormFields/BasicTree.vue'
import JsonEditor from '@/components/Form/FormFields/JsonEditor.vue'
import { assignIfNot, toSentenceCase } from '@/utils/common/index'
import { assignIfNot, toSentenceCase } from '@/utils/common'
import TagInput from '@/components/Form/FormFields/TagInput.vue'
import i18n from '@/i18n/i18n'
@@ -177,10 +177,7 @@ export class FormFieldGenerator {
const systemLang = document.cookie.django_language
if (helpTextAsPlaceholder !== undefined) {
helpTextAsPlaceholder = !!helpTextAsPlaceholder
} else if (
placeholderType.indexOf(field.type) === -1 &&
placeholderComponent.indexOf(field.component) === -1
) {
} else if (placeholderType.indexOf(field.type) === -1 && placeholderComponent.indexOf(field.component) === -1) {
helpTextAsPlaceholder = false
} else if ((helpTextWordLength <= 5 || helpText.length <= 10) && systemLang === 'en') {
helpTextAsPlaceholder = true

View File

@@ -1,5 +1,5 @@
<template>
<el-form ref="elForm" :model="value" class="el-form-renderer" v-bind="$attrs" @submit.native.prevent>
<el-form ref="elForm" :model="value" class="el-form-renderer" v-bind="$attrs">
<template v-for="item in innerContent">
<slot v-if="!isHidden(item)" :name="`id:${item.id}`" />
<component

View File

@@ -8,8 +8,7 @@
:form="basicForm"
:label-position="iLabelPosition"
class="form-fields"
:label-width="labelWidth"
:style="{ '--label-width': labelWidth }"
label-width="25%"
v-bind="$attrs"
v-on="$listeners"
>
@@ -71,20 +70,8 @@
<script>
import ElFormRender from './components/el-form-renderer'
import { randomString } from '@/utils/common/index'
const scrollToError = (
el,
scrollOption = {
behavior: 'smooth',
block: 'center'
}
) => {
setTimeout(() => {
const isError = el.getElementsByClassName('is-error')
isError[0].scrollIntoView(scrollOption)
}, 0)
}
import { randomString } from '@/utils/string'
import { scrollToError } from '@/utils'
export default {
components: {
@@ -141,10 +128,6 @@ export default {
labelPosition: {
type: String,
default: ''
},
labelWidth: {
type: String,
default: '25%'
}
},
data() {
@@ -280,7 +263,7 @@ export default {
}
.el-form-item__content {
width: calc(100% - var(--label-width));
width: 75%;
line-height: 32px;
// 禁用的输入框
@@ -354,7 +337,7 @@ export default {
::v-deep .form-buttons {
margin-top: 30px;
margin-left: var(--label-width);
margin-left: 25%;
}
}

View File

@@ -10,18 +10,15 @@
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ this.$t('From') }}
<el-input-number v-model="cycle01" :max="23" :min="0" size="mini" />
-
<el-input-number v-model="cycle02" :max="23" :min="0" size="mini" />
{{ this.$t('Hour') }}
<el-input-number v-model="cycle01" :max="60" :min="0" size="mini" /> -
<el-input-number v-model="cycle02" :max="60" :min="0" size="mini" /> {{ this.$t('Hour') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('Every') }}
<el-input-number v-model="average02" :max="23" :min="1" size="mini" />
{{ this.$t('Hour') }} {{ this.$t('ExecuteOnce') }}
<el-input-number v-model="average02" :max="60" :min="1" size="mini" /> {{ this.$t('Hour') }} {{ this.$t('ExecuteOnce') }}
</el-radio>
</el-form-item>
@@ -35,7 +32,7 @@
multiple
style="width:100%"
>
<el-option v-for="item in 24" :key="item" :value="item-1">{{ item - 1 }}</el-option>
<el-option v-for="item in 24" :key="item" :value="item-1">{{ item-1 }}</el-option>
</el-select>
</el-radio>
</el-form-item>

View File

@@ -9,7 +9,7 @@
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('From') }}
<el-input-number v-model="average02" :max="59" :min="1" size="mini" />
<el-input-number v-model="average02" :max="60" :min="1" size="mini" />
{{ this.$t('Min') }} {{ this.$t('ExecuteOnce') }}
</el-radio>
</el-form-item>

View File

@@ -13,7 +13,7 @@
<script>
import parser from 'cron-parser'
import { toSafeLocalDateStr } from '@/utils/common/time'
import { toSafeLocalDateStr } from '@/utils/time'
export default {
name: 'CrontabResult',

View File

@@ -10,7 +10,7 @@
<script>
import BaseFormatter from '@/components/Table/TableFormatters/base.vue'
import { setUrlParam } from '@/utils/common/index'
import { setUrlParam } from '@/utils/common'
export default {
name: 'ValueFormatter',

View File

@@ -42,9 +42,9 @@ import DataTable from '@/components/Table/DataTable/index.vue'
import ValueFormatter from './ValueFormatter.vue'
import AttrFormDialog from './AttrFormDialog.vue'
import AttrMatchResultDialog from './AttrMatchResultDialog.vue'
import { setUrlParam } from '@/utils/common/index'
import { setUrlParam } from '@/utils/common'
import { attrMatchOptions } from '@/components/const'
import { toM2MJsonParams } from '@/utils/jms/index'
import { toM2MJsonParams } from '@/utils/jms'
export default {
name: 'JSONManyToManySelect',
@@ -109,7 +109,7 @@ export default {
columns: [
{ prop: 'name', label: this.$t('AttrName'), formatter: tableFormatter('name') },
{ prop: 'match', label: this.$t('Match'), formatter: tableFormatter('match') },
{ prop: 'value', label: this.$t('AttrValue'), formatter: ValueFormatter, formatterArgs: { attrs: this.attrs } },
{ prop: 'value', label: this.$t('AttrValue'), formatter: ValueFormatter, formatterArgs: { attrs: this.attrs }},
{
prop: 'action',
label: this.$t('Action'),

View File

@@ -5,6 +5,7 @@
slot="prepend"
:placeholder="$tc('Select')"
:value="rawValue.code"
style="width: 105px;"
@change="onChange"
>
<el-option
@@ -46,7 +47,7 @@ export default {
}
},
mounted() {
const defaults = { code: this.getDefaultCode(), phone: '' }
const defaults = { code: localStorage.getItem('prePhoneCode') || '+86', phone: '' }
this.rawValue = this.value || defaults
this.$axios.get('/api/v1/common/countries/').then(res => {
this.countries = res.map(item => {
@@ -56,22 +57,6 @@ export default {
this.$emit('input', this.fullPhone)
},
methods: {
getDefaultCode() {
const mapper = {
'zh': '+86',
'en': '+1',
'ja': '+81',
'ko': '+82',
'fr': '+33',
'de': '+49',
'es': '+34',
'it': '+39',
'ru': '+7',
'ar': '+966'
}
const locale = this.$i18n.locale.split('-')[0]
return localStorage.getItem('prePhoneCode') || mapper[locale] || '+86'
},
onChange(countryCode) {
this.rawValue.code = countryCode
this.onInputChange()
@@ -84,11 +69,7 @@ export default {
}
</script>
<style lang="scss" scoped>
.el-select {
width: 85px;
}
<style scoped>
.country-name {
display: inline-block;
width: 150px;

View File

@@ -53,7 +53,6 @@ export default {
const vm = this
const platform = this.$route.query.platform
return {
platform: '',
loading: true,
form: this.protocol,
platformDetail: platform ? '#/console/assets/platforms/' + platform : '',
@@ -98,29 +97,11 @@ export default {
}
}
},
async mounted() {
try {
const drawActionMeta = await this.$store.dispatch('common/getDrawerActionMeta')
const platform = drawActionMeta.row.platform.id
const name = drawActionMeta.row.platform.name
if (platform) {
this.platformDetail = `/ui/#/settings/platforms?id=${platform}&name=${name}`
} else {
this.platformDetail = ''
}
} catch (e) {
throw new Error(e)
}
},
methods: {
onSubmit(form) {
this.protocol = Object.assign(this.protocol, form)
this.$emit('update:visible', false)
this.$emit('confirm', this.protocol)
},
openInNewTab() {
window.open(this.platformDetail, '_blank')
}
}
}

View File

@@ -78,18 +78,16 @@ export default {
},
data() {
return {
filterTags: this.value,
focus: false,
filterValue: '',
filterTags: this.value,
isCheckShowPassword: this.replaceShowPassword
isCheckShowPassword: this.replaceShowPassword,
component: this.autocomplete ? 'el-autocomplete' : 'el-input'
}
},
computed: {
iPlaceholder() {
return `${this.placeholder} (${this.$t('EnterToContinue')})`
},
component() {
return this.autocomplete !== null ? 'el-autocomplete' : 'el-input'
}
},
watch: {
@@ -115,9 +113,8 @@ export default {
if (!this.filterTags.includes(this.filterValue)) {
this.filterTags.push(this.filterValue)
this.filterValue = ''
this.$emit('change', this.filterTags)
}
this.$emit('change', this.filterTags)
this.$emit('input', this.filterTags)
this.$refs.SearchInput.focus()
},
handleTagClick(v, k) {
@@ -192,7 +189,7 @@ export default {
}
.filter-field ::v-deep .el-input__inner {
height: 27px !important;
height: 28px !important;
}
.show-password {

View File

@@ -24,7 +24,7 @@
<script>
import Select2 from './Select2.vue'
import { hasUUID } from '@/utils/common/index'
import { hasUUID } from '@/utils/common'
export default {
components: {

View File

@@ -20,7 +20,7 @@
</template>
<script>
import { randomString } from '@/utils/common/index'
import { randomString } from '@/utils/string'
export default {
props: {

View File

@@ -170,7 +170,7 @@ export default {
const [start, end] = val.split('~')
const startVal = this.countIndex(start)
const endVal = this.countIndex(end)
for (let i = startVal; i < (endVal === 0 ? 24 : endVal); i++) {
for (let i = startVal; i < (endVal === 0 ? 48 : endVal); i++) {
const curWeek = this.weekTimeData[idNum]
const curChild = curWeek.child[i]
if (curChild) {
@@ -181,8 +181,11 @@ export default {
// 计算索引
countIndex(val) {
const one = val.substr(0, 2)
const index = one.startsWith('0') ? one.substr(1, 2) : one
return Number(index)
const a1 = one.startsWith('0') ? one.substr(1, 2) : one
var reg = RegExp(/30/)
const a2 = val.match(reg) ? 1 : 0
const curIndex = (a1 * 2) + a2
return curIndex
},
formatDate(date, fmt) {
const o = {

View File

@@ -21,11 +21,21 @@
</template>
<script type="text/jsx">
import Sortable from 'sortablejs'
import DataTable from '@/components/Table/DataTable/index.vue'
import { newURL, ObjectLocalStorage, replaceAllUUID } from '@/utils/common/index'
import {
ActionsFormatter,
ArrayFormatter,
ChoicesFormatter,
CopyableFormatter,
DateFormatter,
DetailFormatter,
DisplayFormatter,
ObjectRelatedFormatter
} from '@/components/Table/TableFormatters'
import i18n from '@/i18n/i18n'
import { newURL, ObjectLocalStorage, replaceAllUUID, toSentenceCase } from '@/utils/common'
import ColumnSettingPopover from './components/ColumnSettingPopover.vue'
import { TableColumnsGenerator } from './utils'
import LabelsFormatter from '@/components/Table/TableFormatters/LabelsFormatter.vue'
export default {
name: 'AutoDataTable',
@@ -47,9 +57,9 @@ export default {
return {
loading: true,
method: 'get',
meta: {},
iConfig: {},
autoConfig: {},
iConfig: {},
meta: {},
cleanedColumnsShow: {},
totalColumns: [],
popoverColumns: {
@@ -59,31 +69,41 @@ export default {
defaultCols: []
},
isDeactivated: false,
tableColumnsStorage: this.getTableColumnsStorage(),
sortable: null,
inited: false
objTableColumns: new ObjectLocalStorage('tableColumns')
}
},
computed: {
dynamicActionWidth() {
if (this.$i18n.locale === 'en') {
return '120px'
}
if (this.$i18n.locale === 'pt-br') {
return '160px'
}
return '100px'
}
},
watch: {
config: {
immediate: false,
handler: _.debounce(function(iNew, iOld) {
if (this.isDeactivated || !this.inited) {
if (this.isDeactivated) {
return
}
const changed = this.isConfigChanged(iNew, iOld)
if (!changed) {
return
try {
if (JSON.stringify(iNew) === JSON.stringify(iOld)) {
return
}
} catch (error) {
this.$log.error('JsonStringify Error: ', error)
}
this.optionUrlMetaAndGenCols()
this.$log.debug('AutoDataTable Config change found')
this.$log.debug('AutoDataTable Config change found, ', this.isDeactivated)
}, 200)
}
},
async created() {
await this.optionUrlMetaAndGenCols()
this.loading = false
created() {
this.optionUrlMetaAndGenCols()
},
deactivated() {
this.isDeactivated = true
@@ -92,130 +112,323 @@ export default {
this.isDeactivated = false
},
methods: {
isConfigChanged(iNew, iOld) {
const _iNew = _.cloneDeep(iNew)
const _iOld = _.cloneDeep(iOld)
delete _iNew.columns
delete _iOld.columns
const oldMeta = _iNew.columnsMeta
const newMeta = _iOld.columnsMeta
const metas = [oldMeta, newMeta]
for (const meta of metas) {
if (!meta) {
continue
}
for (const [key, value] of Object.entries(meta)) {
if (!key || !value || typeof value !== 'object') {
continue
}
delete value['formatter']
}
}
try {
if (JSON.stringify(_iNew) === JSON.stringify(_iOld)) {
return false
}
} catch (error) {
this.$log.error('JsonStringify Error: ', error)
}
return true
},
setColumnDraggable() {
const el = this.$el.querySelector('.el-table__header-wrapper thead tr')
if (!el) {
setTimeout(() => this.setColumnDraggable(), 500)
return
}
if (this.sortable) {
this.sortable.destroy()
}
this.sortable = Sortable.create(el, {
animation: 150,
onEnd: (evt) => {
let { oldIndex, newIndex } = evt
if (oldIndex === newIndex) {
return
}
// 检测表格是否有选择列
const hasSelectionColumn = this.$el.querySelector('.el-table-column--selection') !== null
if (hasSelectionColumn) {
// 如果有选择列,调整索引
if (oldIndex > 0) oldIndex -= 1
if (newIndex > 0) newIndex -= 1
}
let columnNames = [...this.cleanedColumnsShow.show]
if (columnNames.includes('actions')) {
columnNames = columnNames.filter(item => item !== 'actions')
columnNames.push('actions')
}
// 边界
if (oldIndex >= 0 && oldIndex < columnNames.length &&
newIndex >= 0 && newIndex < columnNames.length) {
const movedItem = columnNames.splice(oldIndex, 1)[0]
columnNames.splice(newIndex, 0, movedItem)
this.$log.debug('Column moved: ', movedItem, oldIndex, ' => ', newIndex)
// 保存更新的列顺序
this.tableColumnsStorage.set(columnNames)
// 更新内部状态
this.cleanedColumnsShow.show = columnNames
this.popoverColumns.currentCols = columnNames
// 重新应用列顺序
this.filterShowColumns()
this.loading = true
setTimeout(() => {
this.loading = false
// 在DOM完全更新后重新初始化拖拽
this.$nextTick(() => {
setTimeout(() => this.setColumnDraggable(), 200)
})
}, 300)
}
}
})
},
generateTotalColumns() {
const generator = new TableColumnsGenerator(this.config, this.meta, this)
this.totalColumns = generator.generateColumns()
this.config.columns = this.totalColumns
this.iConfig = _.cloneDeep(this.config)
},
async optionUrlMetaAndGenCols() {
if (this.config.url === '') {
return
}
const url = (this.config.url.indexOf('?') === -1)
? `${this.config.url}?display=1`
: `${this.config.url}&display=1`
/**
* 原有代码无法正确的同步 storage 的原因是 currentOrder 总是在 totalColumns 之前进行的
* 这导致在首次加载时currentOrder总是为空数组因为此时cleanedColumnsShow.show还未初始化
*/
try {
const data = await this.$store.dispatch('common/getUrlMeta', { url: url })
? `${this.config.url}?draw=1&display=1`
: `${this.config.url}&draw=1&display=1`
this.$store.dispatch('common/getUrlMeta', { url: url }).then(data => {
const method = this.method.toUpperCase()
this.meta = data.actions && data.actions[method] ? data.actions[method] : {}
this.generateTotalColumns()
this.cleanColumnsShow()
}).then(() => {
// 根据当前列重新生成最终渲染表格
this.filterShowColumns()
}).then(() => {
// 生成给子组件使用的TotalColList
this.generatePopoverColumns()
this.setColumnDraggable()
} catch (error) {
}).catch((error) => {
this.$log.error('Error occur: ', error)
}
}).finally(() => {
this.loading = false
})
},
getTableColumnsStorage() {
let tableName = this.config.name || this.$route.name + '_' + newURL(this.config.url).pathname
tableName = replaceAllUUID(tableName)
return new ObjectLocalStorage('tableColumns', tableName)
generateColumnByName(name, col) {
switch (name) {
case 'id':
if (!col.width) {
col.width = '299px'
}
if (!col.formatter) {
col.formatter = CopyableFormatter
col.iconPosition = 'left'
}
break
case 'name':
col.formatter = DetailFormatter
col.sortable = 'custom'
col.showOverflowTooltip = true
col.minWidth = '150px'
break
case 'actions':
col = {
prop: 'actions',
label: i18n.t('Actions'),
align: 'center',
width: this.dynamicActionWidth,
formatter: ActionsFormatter,
fixed: 'right',
formatterArgs: {}
}
break
case 'is_valid':
col.label = i18n.t('Valid')
col.formatter = ChoicesFormatter
col.formatterArgs = {
textChoices: {
true: i18n.t('Yes'),
false: i18n.t('No')
}
}
col.width = '80px'
break
case 'is_active':
col.formatter = ChoicesFormatter
col.formatterArgs = {
textChoices: {
true: i18n.t('Active'),
false: i18n.t('Inactive')
}
}
col.width = '100px'
break
case 'datetime':
case 'date_start':
col.formatter = DateFormatter
break
case 'labels':
col.formatter = LabelsFormatter
col.width = '200px'
break
case 'comment':
col.showOverflowTooltip = true
}
return col
},
generateColumnByType(type, col, meta) {
switch (type) {
case 'choice':
col.sortable = 'custom'
col.formatter = DisplayFormatter
break
case 'labeled_choice':
col.sortable = 'custom'
col.formatter = ChoicesFormatter
break
case 'boolean':
col.formatter = ChoicesFormatter
// col.width = '80px'
break
case 'datetime':
col.formatter = DateFormatter
col.width = '155px'
break
case 'object_related_field':
col.formatter = ObjectRelatedFormatter
break
case 'm2m_related_field':
col.formatter = ObjectRelatedFormatter
break
case 'list':
col.formatter = ArrayFormatter
break
case 'json':
case 'field':
if (meta.child && meta.child.type === 'nested object') {
col.formatter = ObjectRelatedFormatter
}
break
}
// this.$log.debug('Field: ', type, col.prop, col)
return col
},
addHelpTipIfNeed(col) {
const helpTip = col.helpTip
if (!helpTip) {
return col
}
col.renderHeader = (h, { column, $index }) => {
const binds = {
props: {
placement: 'bottom',
effect: 'dark',
openDelay: 500,
popperClass: 'help-tips'
}
}
return (
<span>{column.label}
<el-tooltip {...binds}>
<div slot='content' v-sanitize={helpTip}/>
<i class='fa fa-question-circle-o help-tip-icon' style='padding-left: 2px'/>
</el-tooltip>
</span>
)
}
return col
},
addFilterIfNeed(col) {
if (col.prop) {
const column = this.meta[col.prop] || {}
if (!column.filter) {
return col
}
if (column.type === 'boolean') {
col.filters = [
{ text: i18n.t('Yes'), value: true },
{ text: i18n.t('No'), value: false }
]
col.sortable = false
col['column-key'] = col.prop
}
if (column.type === 'choice' && column.choices) {
col.filters = column.choices.map(item => {
if (typeof (item.value) === 'boolean') {
if (item.value) {
return { text: item['label'], value: 'True' }
} else {
return { text: item['label'], value: 'False' }
}
}
return { text: item['label'], value: item.value }
})
col.sortable = false
col['column-key'] = col.prop
}
}
return col
},
addOrderingIfNeed(col) {
if (col.prop) {
const column = this.meta[col.prop] || {}
if (column.order) {
col.sortable = 'custom'
col['column-key'] = col.prop
}
}
return col
},
setDefaultFormatterIfNeed(col) {
if (!col.formatter) {
col.formatter = (row, column, cellValue) => {
let value = cellValue
let padding = '0'
const excludes = [undefined, null, '']
if (excludes.indexOf(value) !== -1) {
padding = '6px'
value = '-'
}
return <span style={{ marginLeft: padding }}>{value}</span>
}
}
return col
},
setDefaultWidthIfNeed(col) {
const lang = this.$i18n.locale
let factor = 10
if (lang === 'zh') {
factor = 20
}
let [sortable, filters] = [0, 0]
if (col && col?.sortable === 'custom') {
sortable = 10
}
if (col && col?.filters?.length > 0) {
filters = 12
}
if (col && !col.width && col.label && !col.minWidth) {
col.minWidth = `${col.label.length * factor + sortable + filters + 30}px`
}
return col
},
generateColumn(name) {
const colMeta = this.meta[name] || {}
const customMeta = this.config.columnsMeta ? this.config.columnsMeta[name] : {}
let col = { prop: name, label: colMeta.label, showOverflowTooltip: true }
col = this.generateColumnByType(colMeta.type, col, colMeta)
col = this.generateColumnByName(name, col)
col = this.setDefaultFormatterIfNeed(col)
col = Object.assign(col, customMeta)
col = this.addHelpTipIfNeed(col)
col = this.addFilterIfNeed(col)
col = this.addOrderingIfNeed(col)
col = this.updateLabelIfNeed(col)
col = this.setDefaultWidthIfNeed(col)
return col
},
updateLabelIfNeed(col) {
if (!col.label) {
return col
}
col.label = col.label
.replace(' Amount', '')
.replace(' amount', '')
.replace('数量', '')
if (col.label.startsWith('Is ')) {
col.label = col.label.replace('Is ', '')
}
col.label = toSentenceCase(col.label)
return col
},
generateTotalColumns() {
const config = _.cloneDeep(this.config)
let columns = []
const allColumnNames = Object.entries(this.meta)
.filter(([name, meta]) => !meta['write_only'])
.map(([name, meta]) => name)
.concat(config.columnsExtra || [])
let configColumns = config.columns || allColumnNames
const columnsExclude = config.columnsExclude || []
const columnsAdd = config.columnsAdd || []
configColumns = configColumns.concat(columnsAdd)
configColumns = configColumns.filter(item => !columnsExclude.includes(item))
// 解决后端 API 返回字段中包含 actions 的问题;
const hasColumnActions = configColumns.findIndex(item => item?.prop === 'actions') !== -1
if (!hasColumnActions) {
configColumns = [...configColumns.filter(i => i !== 'actions'), 'actions']
}
for (let col of configColumns) {
if (typeof col === 'object') {
columns.push(col)
} else if (typeof col === 'string') {
col = this.generateColumn(col)
columns.push(col)
}
}
columns = columns.filter(item => {
if (item?.showFullContent) {
item.className = 'show-full-content'
}
let has = item.has
if (has === undefined) {
has = true
} else if (typeof has === 'function') {
has = has()
}
return has
})
columns = this.orderingColumns(columns)
// 第一次初始化时记录 totalColumns
this.totalColumns = columns
config.columns = columns
this.iConfig = config
},
orderingColumns(columns) {
const cols = _.cloneDeep(this.config.columns)
const defaults = _.get(this.config, 'columnsShow.default')
const ordering = (cols || defaults || []).map(item => {
let prop = item
if (typeof item === 'object') {
prop = item.prop
}
return prop
})
return _.sortBy(columns, (item) => {
if (item.prop === 'actions') {
return 1000
}
const i = ordering.indexOf(item.prop)
return i === -1 ? 999 : i
})
},
// 生成给子组件使用的TotalColList
cleanColumnsShow() {
@@ -225,12 +438,16 @@ export default {
if (defaultColumnsNames.length === 0) {
defaultColumnsNames = totalColumnsNames
}
// Clean it
defaultColumnsNames = totalColumnsNames.filter(n => defaultColumnsNames.indexOf(n) > -1)
// 最小列
const minColumnsNames = _.get(this.iConfig, 'columnsShow.min', ['actions', 'id'])
.filter(n => totalColumnsNames.includes(n))
const configShowColumnsNames = this.tableColumnsStorage.get()
let tableName = this.config.name || this.$route.name + '_' + newURL(this.config.url).pathname
tableName = replaceAllUUID(tableName)
const configShowColumnsNames = this.objTableColumns.get(tableName)
let showColumnsNames = configShowColumnsNames || defaultColumnsNames
if (showColumnsNames.length === 0) {
showColumnsNames = totalColumnsNames
@@ -241,6 +458,8 @@ export default {
showColumnsNames.push(v)
}
})
// Clean it
showColumnsNames = totalColumnsNames.filter(n => showColumnsNames.indexOf(n) > -1)
this.cleanedColumnsShow = {
default: defaultColumnsNames,
@@ -252,39 +471,9 @@ export default {
},
filterShowColumns() {
this.cleanColumnsShow()
const showFieldNames = this.cleanedColumnsShow.show
let showFields = this.totalColumns.filter(obj => {
return showFieldNames.indexOf(obj.prop) > -1
this.iConfig.columns = this.totalColumns.filter(obj => {
return this.cleanedColumnsShow.show.indexOf(obj.prop) > -1
})
showFields = this.orderingColumns(showFields)
this.iConfig.columns = showFields
// 确保最新的列配置也应用到config对象上保持同步
this.config.columns = this.iConfig.columns
this.$nextTick(() => {
if (this.$refs.dataTable) {
this.$refs.dataTable.getList()
}
this.inited = true
})
},
orderingColumns(columns) {
const cols = _.cloneDeep(this.config.columns)
const show = this.cleanedColumnsShow.show
const ordering = (show || cols || []).map(item => {
let prop = item
if (typeof item === 'object') {
prop = item.prop
}
return prop
})
const sorted = _.sortBy(columns, (item) => {
const i = ordering.indexOf(item.prop)
item.order = i
return i === -1 ? 999 : i
})
return sorted
},
generatePopoverColumns() {
this.popoverColumns.totalColumnsList = this.totalColumns.filter(obj => {
@@ -304,7 +493,13 @@ export default {
columns = this.cleanedColumnsShow.default
}
this.popoverColumns.currentCols = columns
this.tableColumnsStorage.set(columns)
let tableName = this.config.name || this.$route.name + '_' + newURL(url).pathname
// 替换url中的uuid避免同一个类型接口生成多个keylocalStorage中的数据无法共用.
tableName = replaceAllUUID(tableName)
this.objTableColumns.set(tableName, columns)
this.filterShowColumns()
},
filterChange(filters) {

View File

@@ -1,328 +0,0 @@
import { toSentenceCase } from '@/utils/common/index'
import i18n from '@/i18n/i18n'
import {
ActionsFormatter,
ArrayFormatter,
ChoicesFormatter,
CopyableFormatter,
DateFormatter,
DetailFormatter,
DisplayFormatter,
ObjectRelatedFormatter
} from '@/components/Table/TableFormatters'
import LabelsFormatter from '@/components/Table/TableFormatters/LabelsFormatter.vue'
export class TableColumnsGenerator {
constructor(config, meta, vm) {
this.config = config
this.meta = meta
this.vm = vm
}
// dynamicActionWidth() {
// console.log(i18n.locale)
// if (i18n.locale === 'zh-hans' || i18n.locale === 'zh-hant') {
// return '100px'
// }
// if (i18n.locale === 'ja' || i18n.locale === 'ko') {
// return '120px'
// }
// return '160px'
// }
generateColumns() {
const config = _.cloneDeep(this.config)
let columns = []
const allColumnNames = Object.entries(this.meta)
.filter(([name, meta]) => !meta['write_only'])
.map(([name, meta]) => name)
.concat(config.columnsExtra || [])
let configColumns = config.columns || allColumnNames
const columnsExclude = config.columnsExclude || []
const columnsAdd = config.columnsAdd || []
configColumns = configColumns.concat(columnsAdd)
configColumns = configColumns.filter(item => !columnsExclude.includes(item))
// 解决后端 API 返回字段中包含 actions 的问题;
const hasColumnActions = configColumns.findIndex(item => item?.prop === 'actions') !== -1
if (!hasColumnActions) {
configColumns = [...configColumns.filter(i => i !== 'actions'), 'actions']
}
for (let col of configColumns) {
if (typeof col === 'object') {
columns.push(col)
} else if (typeof col === 'string') {
col = this.generateColumn(col)
columns.push(col)
}
}
columns = columns.filter(item => {
if (item?.showFullContent) {
item.className = 'show-full-content'
}
let has = item.has
if (has === undefined) {
has = true
} else if (typeof has === 'function') {
has = has()
}
return has
})
// columns = this.orderingColumns(columns)
// 第一次初始化时记录 totalColumns
config.columns = columns
return columns
}
updateLabelIfNeed(col) {
if (!col.label) {
return col
}
col.label = col.label
.replace(' Amount', '')
.replace(' amount', '')
.replace('数量', '')
if (col.label.startsWith('Is ')) {
col.label = col.label.replace('Is ', '')
}
col.label = toSentenceCase(col.label)
return col
}
generateColumn(name) {
const colMeta = this.meta[name] || {}
const customMeta = this.config.columnsMeta ? this.config.columnsMeta[name] : {}
let col = { prop: name, label: colMeta.label, showOverflowTooltip: true }
col = this.generateColumnByType(colMeta.type, col, colMeta)
col = this.generateColumnByName(name, col)
col = this.setDefaultFormatterIfNeed(col)
col = Object.assign(col, customMeta)
col = this.addHelpTipIfNeed(col)
col = this.addFilterIfNeed(col)
col = this.addOrderingIfNeed(col)
col = this.updateLabelIfNeed(col)
col = this.setDefaultWidthIfNeed(col)
return col
}
generateColumnByName(name, col) {
switch (name) {
case 'id':
if (!col.width) {
col.width = '299px'
}
if (!col.formatter) {
col.formatter = CopyableFormatter
col.iconPosition = 'left'
}
break
case 'name':
col.formatter = DetailFormatter
col.sortable = 'custom'
col.showOverflowTooltip = true
col.minWidth = '150px'
break
case 'actions':
col = {
prop: 'actions',
label: i18n.t('Actions'),
align: 'center',
width: '120px',
formatter: ActionsFormatter,
fixed: 'right',
formatterArgs: {}
}
break
case 'is_valid':
col.label = i18n.t('Valid')
col.formatter = ChoicesFormatter
col.formatterArgs = {
textChoices: {
true: i18n.t('Yes'),
false: i18n.t('No')
}
}
col.width = '80px'
break
case 'is_active':
col.formatter = ChoicesFormatter
col.formatterArgs = {
textChoices: {
true: i18n.t('Active'),
false: i18n.t('Inactive')
}
}
col.width = '100px'
break
case 'datetime':
case 'date_start':
col.formatter = DateFormatter
break
case 'labels':
col.formatter = LabelsFormatter
col.width = '200px'
break
case 'comment':
col.showOverflowTooltip = true
}
return col
}
generateColumnByType(type, col, meta) {
switch (type) {
case 'choice':
col.sortable = 'custom'
col.formatter = DisplayFormatter
break
case 'labeled_choice':
col.sortable = 'custom'
col.formatter = ChoicesFormatter
break
case 'boolean':
col.formatter = ChoicesFormatter
// col.width = '80px'
break
case 'datetime':
col.formatter = DateFormatter
col.width = '155px'
break
case 'object_related_field':
col.formatter = ObjectRelatedFormatter
break
case 'm2m_related_field':
col.formatter = ObjectRelatedFormatter
break
case 'list':
col.formatter = ArrayFormatter
break
case 'json':
case 'field':
if (meta.child && meta.child.type === 'nested object') {
col.formatter = ObjectRelatedFormatter
}
break
}
// this.$log.debug('Field: ', type, col.prop, col)
return col
}
setDefaultFormatterIfNeed(col) {
const h = this.vm.$createElement
if (!col.formatter) {
col.formatter = (row, column, cellValue) => {
let value = cellValue
let padding = '0'
const excludes = [undefined, null, '']
if (excludes.indexOf(value) !== -1) {
padding = '6px'
value = '-'
}
return h(
'span',
{
style: {
marginLeft: padding
}
},
[value]
)
}
}
return col
}
setDefaultWidthIfNeed(col) {
const lang = i18n.locale
let factor = 10
if (lang === 'zh') {
factor = 20
}
let [sortable, filters] = [0, 0]
if (col && col?.sortable === 'custom') {
sortable = 10
}
if (col && col?.filters?.length > 0) {
filters = 12
}
if (col && !col.width && col.label && !col.minWidth) {
col.minWidth = `${col.label.length * factor + sortable + filters + 30}px`
}
return col
}
addOrderingIfNeed(col) {
if (col.prop) {
const column = this.meta[col.prop] || {}
if (column.order) {
col.sortable = 'custom'
col['column-key'] = col.prop
}
}
return col
}
addHelpTipIfNeed(col) {
const helpTip = col.helpTip
if (!helpTip) {
return col
}
col.renderHeader = (h, { column, $index }) => {
const binds = {
props: {
placement: 'bottom',
effect: 'dark',
openDelay: 500,
popperClass: 'help-tips'
}
}
return (
<span>
{column.label}
<el-tooltip {...binds}>
<div slot='content' v-sanitize={helpTip} />
<i class='fa fa-question-circle-o help-tip-icon' style='padding-left: 2px' />
</el-tooltip>
</span>
)
}
return col
}
addFilterIfNeed(col) {
if (col.prop) {
const column = this.meta[col.prop] || {}
if (!column.filter) {
return col
}
if (column.type === 'boolean') {
col.filters = [{ text: i18n.t('Yes'), value: true }, { text: i18n.t('No'), value: false }]
col.sortable = false
col['column-key'] = col.prop
}
if (column.type === 'choice' && column.choices) {
col.filters = column.choices.map(item => {
if (typeof item.value === 'boolean') {
if (item.value) {
return { text: item['label'], value: 'True' }
} else {
return { text: item['label'], value: 'False' }
}
}
return { text: item['label'], value: item.value }
})
col.sortable = false
col['column-key'] = col.prop
}
}
return col
}
}

View File

@@ -4,7 +4,7 @@
{{ $t('Enterprise') }}
</span>
<el-row class="panel">
<el-col v-if="d.icon" :span="d.icon ? 8 : 0" class="image">
<el-col v-if="d.icon" :span="8" class="image">
<img
v-if="d.icon.startsWith('/') || d.icon.startsWith('data:')"
:alt="d.display_name"
@@ -12,21 +12,21 @@
>
<Icon v-else :icon="d.icon" />
</el-col>
<el-col :span="d.icon ? 16 : 24" class="text-zone">
<el-col :span="16" class="text-zone">
<div class="one-line">
<b>{{ d.display_name }}</b>
</div>
<div class="tag-zone">
<el-tag v-if="d.version" size="mini" style="margin-left: 5px; background-color: #ecf5ff; color: #409eff;">
<el-tag v-if="d.version" size="mini" style="margin-left: 5px">
{{ d.version }}
</el-tag>
<el-tag v-for="tag of d.tags" :key="tag" size="mini">
{{ capitalize(tag) }}
</el-tag>
</div>
<div :title="d.comment " class="comment">
{{ d.comment }}
</div>
<div class="tag-zone">
<el-tag v-for="tag of d.tags" :key="tag" size="mini">
{{ capitalize(tag) }}
</el-tag>
</div>
</el-col>
</el-row>
</div>
@@ -107,20 +107,22 @@ export default {
.comment {
display: -webkit-box;
margin-top: 10px;
height: 120px;
font-size: 12px;
padding: 10px 0;
cursor: pointer;
overflow: hidden;
-webkit-line-clamp: 4;
-webkit-box-flex: 1;
-webkit-box-orient: vertical;
text-align: left;
}
.tag-zone {
display: flex;
height: 30%;
align-items: center;
cursor: pointer;
margin-top: 10px;
margin-left: -5px;
}
}
}

View File

@@ -34,12 +34,6 @@
@currentSizeChange="handleCurrentChange"
@sizeChange="handleSizeChange"
/>
<Drawer
v-if="detailDrawer"
:component="detailDrawer"
:title="detailTitle"
:visible.sync="detailDrawerVisible"
/>
</div>
</template>
@@ -49,14 +43,12 @@ import { Pagination } from '@/components'
import TableAction from '@/components/Table/ListTable/TableAction'
import IBox from '@/components/Common/IBox/index.vue'
import Panel from './Panel'
import Drawer from '@/components/Drawer/index.vue'
const defaultFirstPage = 1
export default {
name: 'CardTable',
components: {
Drawer,
IBox,
Panel,
TableAction,
@@ -87,10 +79,6 @@ export default {
subComponentProps: {
type: Object,
default: () => ({})
},
detailDrawer: {
type: [String, Function],
default: ''
}
},
data() {
@@ -99,9 +87,9 @@ export default {
totalData: [],
page: defaultFirstPage,
extraQuery: {},
paginationSize: 12,
paginationSize: 6,
paginationLayout: 'total, sizes, prev, pager, next',
paginationSizes: [12, 24, 36],
paginationSizes: [6, 18, 27],
loading: true,
axiosConfig: {
raw: 1,
@@ -109,9 +97,7 @@ export default {
display: 1,
draw: 1
}
},
detailDrawerVisible: false,
detailTitle: ''
}
}
},
computed: {
@@ -151,10 +137,9 @@ export default {
return
}
const pageQuery = this.getPageQuery(this.page, this.paginationSize)
const query = Object.assign(this.extraQuery, pageQuery, this.tableConfig.extraQuery)
const query = Object.assign(this.extraQuery, pageQuery)
const queryString = Object.keys(query).map(key => key + '=' + query[key]).join('&')
const connector = this.tableUrl.indexOf('?') === -1 ? '?' : '&'
const url = `${this.tableUrl}${connector}${queryString}`
const url = `${this.tableUrl}?${queryString}`
const resp = await this.$axios.get(url, this.axiosConfig)
const data = resp.data
@@ -192,27 +177,19 @@ export default {
defaultPerformDelete(obj) {
this.$axios.delete(`${this.tableConfig.url}${obj.id}/`)
},
async onView(obj) {
onView(obj) {
if (this.isDisabled(obj)) {
return
}
if (this.detailDrawer) {
await this.$store.dispatch('common/setDrawerActionMeta', {
action: 'detail', row: obj, col: {}, id: obj.id
})
this.detailTitle = `${this.$t('Detail')}: ${obj.name}`
this.detailDrawerVisible = true
} else {
const viewFunc = this.tableConfig.onView || this.defaultPerformView
await viewFunc(obj)
}
const viewFunc = this.tableConfig.onView || this.defaultPerformView
viewFunc(obj)
},
onDelete(obj) {
const msg = `${this.$t('DeleteWarningMsg')} "${obj.name}" ?`
this.$confirm(msg, this.$tc('Info'), {
type: 'warning',
confirmButtonClass: 'el-button--danger',
beforeClose: async (action, instance, done) => {
beforeClose: async(action, instance, done) => {
if (action !== 'confirm') return done()
const deleteFunc = this.tableConfig.onDelete || this.defaultPerformDelete
await deleteFunc(obj)
@@ -248,10 +225,6 @@ export default {
display: flex;
justify-content: left;
flex-wrap: wrap;
.el-card .el-card__body div {
height: inherit;
}
}
.el-col, div {
@@ -261,7 +234,7 @@ export default {
min-width: 330px;
position: relative;
margin-bottom: 20px;
height: 180px;
//height: 230px;
width: 380px;
padding: 15px;

View File

@@ -10,7 +10,7 @@
</template>
<script>
import { newURL, ObjectLocalStorage } from '@/utils/common/index'
import { newURL, ObjectLocalStorage } from '@/utils/common'
import { default as ElDatableTable } from './compenents/el-data-table'
import { mapGetters } from 'vuex'
@@ -65,7 +65,7 @@ export default {
onEdit: (row) => {
const defaultOnEdit = (row) => {
const routeName = userTableActions.editRoute
this.$router.push({ name: routeName, params: { id: row.id } })
this.$router.push({ name: routeName, params: { id: row.id }})
}
let onEdit = userTableActions.onEdit
if (!onEdit) {

View File

@@ -22,9 +22,8 @@
<script>
import ListTable from '../ListTable'
import Drawer from '@/components/Drawer/index.vue'
import { setUrlParam, toLowerCaseExcludeAbbr, toSentenceCase } from '@/utils/common/index'
import { setUrlParam, toLowerCaseExcludeAbbr, toSentenceCase } from '@/utils/common'
import { mapGetters } from 'vuex'
import { resolveRoute } from '@/utils/vue/index'
const drawerType = [String, Function]
@@ -167,9 +166,9 @@ export default {
}
this.drawerComponent = ''
},
getDetailDrawerTitle({ col, row, cellValue, payload = {} }) {
getDetailDrawerTitle({ col, row, cellValue, payload = {}}) {
this.$log.debug('>>> getDetailDrawerTitle: ', col, row, cellValue, payload)
const { detailRoute = {}, formatterArgs = {} } = payload
const { detailRoute = {}, formatterArgs = {}} = payload
const getTitle = formatterArgs.getDrawerTitle
this.$log.debug('>>> getTitle: ', getTitle)
if (getTitle && typeof getTitle === 'function') {
@@ -178,7 +177,7 @@ export default {
if (formatterArgs.title) {
return formatterArgs.title
}
const resolvedRoute = resolveRoute(detailRoute, this.$router)
const resolvedRoute = this.resolveRoute(detailRoute)
let title = cellValue || row.name
if (formatterArgs.getTitle) {
title = formatterArgs.getTitle({ col, row, cellValue })
@@ -197,23 +196,18 @@ export default {
}
let title = this.title
if (!title && this.resource) {
title = this.resource
}
if (!title) {
title = this.$route.meta?.title
title = title.replace('List', '').replace('列表', '')
title = _.trimEnd(title, 's')
}
if (!title) {
title = this.$t('NoTitle')
}
let actionLabel = ''
if (action === 'clone' || action === 'create') {
actionLabel = this.$t('Create')
} else if (action === 'update') {
@@ -221,9 +215,7 @@ export default {
} else if (action === 'detail') {
actionLabel = this.$t('Detail')
}
title = actionLabel + this.$t('WordSep') + toLowerCaseExcludeAbbr(title)
return title
},
getDefaultDrawer(action) {
@@ -233,38 +225,53 @@ export default {
},
getRouteNameComponent(name, action) {
const route = { name: name }
if (action === 'detail' || action === 'update') {
route.params = { id: '1' }
}
const resolved = resolveRoute(route, this.$router)
if (resolved && resolved.components && resolved.components.default) {
return resolved.components.default
const routes = this.$router.resolve(route)
if (!routes) {
return
}
const matched = routes.resolved.matched.filter(item => item.name === name && item.components)
if (matched.length === 0) {
return
}
return ''
if (matched[0] && matched[0].components?.default) {
const component = matched[0].components.default
return component
}
},
resolveRoute(route) {
const routes = this.$router.resolve(route)
if (!routes) {
return
}
const matched = routes.resolved.matched.filter(item => item.name === route.name && item.components)
if (matched.length === 0) {
return
}
if (matched[0] && matched[0].components?.default) {
return matched[0]
}
},
getDetailComponent({ detailRoute }) {
if (!detailRoute) {
return this.detailDrawer
}
const route = resolveRoute(detailRoute, this.$router)
this.$log.debug('>>> getDetailComponent: ', detailRoute)
const route = this.resolveRoute(detailRoute)
let component = null
if (route && route.components && route.components.default) {
if (route) {
component = route.components.default
}
if (!component) {
component = this.detailDrawer
}
return component
},
getDrawerComponent(action, payload) {
this.$log.debug('>>> getDrawerComponent: ', action, payload)
switch (action) {
case 'create':
return this.createDrawer
@@ -279,7 +286,7 @@ export default {
}
},
async showDrawer(action, { row = {}, col = {}, query = {}, cellValue = '', payload = {} } = {}) {
async showDrawer(action, { row = {}, col = {}, query = {}, cellValue = '', payload = {}} = {}) {
try {
// 1. 先重置状态
this.drawerVisible = false
@@ -356,7 +363,7 @@ export default {
await this.$store.dispatch('common/setDrawerActionMeta', {
action: 'detail', row: row, col: col, id: id
})
await this.showDrawer('detail', { row, col, cellValue, payload: { detailRoute, formatterArgs } })
await this.showDrawer('detail', { row, col, cellValue, payload: { detailRoute, formatterArgs }})
},
async onCreate(meta) {
if (!meta) {
@@ -366,14 +373,14 @@ export default {
await this.$store.dispatch('common/setDrawerActionMeta', { action: 'create', ...meta })
await this.showDrawer('create', meta)
},
async onClone({ row, col, query = {} }) {
async onClone({ row, col, query = {}}) {
this.$route.params.id = ''
await this.$store.dispatch('common/setDrawerActionMeta', {
action: 'clone', row: row, col: col, id: row.id
})
await this.showDrawer('clone', { query })
},
async onUpdate({ row, col, query = {} }) {
async onUpdate({ row, col, query = {}}) {
this.$route.params.id = row.id
this.$route.params.action = 'update'
await this.$store.dispatch('common/setDrawerActionMeta', {

View File

@@ -1,7 +1,7 @@
<template>
<div class="item-info">
<el-row>
<el-col v-for="(item, i) of infos" :key="i" :span="12" class="panel-item">
<el-col v-for="item of infos" :key="item.content" :span="12" class="panel-item">
<small class="item-label">{{ item.title }}</small>
<h4 class="item-value">{{ item.content }}</h4>
</el-col>

View File

@@ -1,5 +1,5 @@
<template>
<div class="info-panel" @click="gotoDetail">
<div class="info-panel">
<div class="panel-header">
<div class="panel-title">
<el-avatar :src="imageUrl" shape="square" />
@@ -30,7 +30,7 @@
</el-dropdown>
</div>
</div>
<div class="panel-content">
<div class="panel-content" @click="gotoDetail">
<InfoPanel :infos="getInfos(object)" />
</div>
<el-row class="panel-footer" />
@@ -158,7 +158,6 @@ div.info-panel {
flex-direction: column;
padding: 10px;
gap: 10px;
cursor: pointer;
.panel-header {
padding: 10px 20px;
@@ -166,6 +165,7 @@ div.info-panel {
display: flex;
justify-content: space-between;
gap: 10px;
cursor: default;
.panel-title {
display: flex;
@@ -216,6 +216,7 @@ div.info-panel {
.panel-content {
display: block;
padding: 1px 25px 10px;
cursor: pointer;
.panel-image {
margin: auto 5px;

View File

@@ -49,7 +49,7 @@
import Dialog from '@/components/Dialog/index.vue'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/Table/DataTable/compenents/el-data-table/utils/query'
import { download } from '@/utils/common/index'
import { download } from '@/utils/common'
export default {
name: 'ExportDialog',

View File

@@ -68,7 +68,7 @@
<script>
import Dialog from '@/components/Dialog/index.vue'
import ImportTable from '@/components/Table/ListTable/TableAction/ImportTable.vue'
import { download, getErrorResponseMsg } from '@/utils/common/index'
import { download, getErrorResponseMsg } from '@/utils/common'
import { createSourceIdCache } from '@/api/common'
export default {

View File

@@ -42,10 +42,10 @@
<script>
import DataTable from '@/components/Table/DataTable/index.vue'
import { getUpdateObjURL } from '@/utils/common/index'
import { sleep } from '@/utils/common/time'
import { getUpdateObjURL } from '@/utils/common'
import { sleep } from '@/utils/time'
import { EditableInputFormatter } from '@/components/Table/TableFormatters'
import { encryptPassword } from '@/utils/secure'
import { encryptPassword } from '@/utils/crypto'
import getStatusColumnMeta from '@/components/Table/ListTable/TableAction/const'
export default {

View File

@@ -10,7 +10,7 @@
<script>
import { cleanActions } from './utils'
import { createSourceIdCache } from '@/api/common'
import { getErrorResponseMsg } from '@/utils/common/index'
import { getErrorResponseMsg } from '@/utils/common'
import i18n from '@/i18n/i18n'
import DataActions from '@/components/Common/DataActions/index.vue'
@@ -240,7 +240,7 @@ export default {
type: 'warning',
confirmButtonClass: 'el-button--danger',
showCancelButton: true,
beforeClose: async (action, instance, done) => {
beforeClose: async(action, instance, done) => {
if (action !== 'confirm') return done()
instance.confirmButtonLoading = true
try {

View File

@@ -45,7 +45,7 @@
<script>
import SummaryCard from '@/components/Cards/SummaryCard/index.vue'
import { setUrlParam } from '@/utils/common/index'
import { setUrlParam } from '@/utils/common'
export default {
name: 'QuickFilter',

View File

@@ -17,7 +17,7 @@
import ActionsGroup from '@/components/Common/ActionsGroup/index.vue'
import ImExportDialog from './ImExportDialog.vue'
import { cleanActions } from './utils'
import { assignIfNot } from '@/utils/common/index'
import { assignIfNot } from '@/utils/common'
const defaultTrue = { type: [Boolean, Function, String], default: true }

View File

@@ -49,7 +49,7 @@ import LeftSide from './LeftSide.vue'
import RightSide from './RightSide.vue'
import AutoDataSearch from '@/components/Table/AutoDataSearch/index.vue'
import DatetimeRangePicker from '@/components/Form/FormFields/DatetimeRangePicker.vue'
import { getDaysAgo, getDaysFuture } from '@/utils/common/time'
import { getDaysAgo, getDaysFuture } from '@/utils/time'
import LabelSearch from '@/components/Table/ListTable/TableAction/LabelSearch.vue'
const defaultTrue = { type: Boolean, default: true }

View File

@@ -34,7 +34,7 @@
</template>
<script>
import { getResourceFromApiUrl } from '@/utils/jms/index'
import { getResourceFromApiUrl } from '@/utils/jms'
import deepmerge from 'deepmerge'
import { mapGetters } from 'vuex'
import IBox from '@/components/Common/IBox/index.vue'
@@ -42,8 +42,8 @@ import TableAction from './TableAction/index.vue'
import Emitter from '@/mixins/emitter'
import AutoDataTable from '../AutoDataTable/index.vue'
import QuickFilter from './TableAction/QuickFilter.vue'
import { getDayEnd, getDaysAgo } from '@/utils/common/time'
import { ObjectLocalStorage } from '@/utils/common/index'
import { getDayEnd, getDaysAgo } from '@/utils/time'
import { ObjectLocalStorage } from '@/utils/common'
export default {
name: 'ListTable',
@@ -104,9 +104,7 @@ export default {
tablePath: new URL(this.tableConfig.url || '', 'http://127.0.0.1').pathname,
objStorage: new ObjectLocalStorage('filterExpand'),
iFilterExpand: null,
reloadTable: _.debounce(this._reloadTable, 300),
searchQuery: {},
filterQuery: {}
reloadTable: _.debounce(this._reloadTable, 300)
}
},
computed: {
@@ -300,26 +298,19 @@ export default {
this.$delete(this.extraQuery, key)
}
},
getMergedQuery() {
return { ...this.searchQuery, ...this.filterQuery }
},
search(attrs) {
const init = this.updateInitQuery(attrs)
if (init) {
return
}
this.searchQuery = attrs
const merged = this.getMergedQuery()
this.$log.debug('ListTable: search table', attrs)
this.$emit('TagSearch', attrs)
this.$refs.dataTable?.$refs.dataTable?.search(merged, true)
this.$refs.dataTable?.$refs.dataTable?.search(attrs, true)
},
filter(attrs) {
this.filterQuery = attrs
const merged = this.getMergedQuery()
this.$emit('TagFilter', attrs)
this.$log.debug('ListTable: found filter change', attrs)
this.$refs.dataTable?.$refs.dataTable?.search(merged, true)
this.search(attrs)
},
hasActionPerm(action) {
const permRequired = this.permissions[action]

View File

@@ -14,10 +14,9 @@
plain
size="mini"
type="primary"
:disabled="!hasPerm"
@click="handleBtnConnect"
>
<i :class="iButtonIcon" :style="{ color: hasPerm ? '' : '#fff' }" />
<i :class="iButtonIcon" />
</el-button>
<el-dropdown-menu v-if="!isClick" slot="dropdown">
@@ -54,16 +53,13 @@ export default {
default() {
return {
can: () => true,
getConnectUrl: (row, protocol, asset) => {
const assetId = asset ? asset.id : row.asset.id
getConnectUrl: (row, protocol) => {
return `/luna/admin-connect/?
asset=${assetId}
asset=${row.asset.id}
&account=${row.id}
&protocol=${protocol}
&org_id=${this.$store.getters.currentOrg.id}
`.replace(/\s+/g, '')
},
asset: null,
assetUrl: '/api/v1/assets/assets/{id}/',
buttonIcon: 'fa fa-desktop'
}
@@ -89,7 +85,7 @@ export default {
},
methods: {
handleProtocolConnect(protocol) {
const url = this.formatterArgs.getConnectUrl(this.row, protocol, this.formatterArgs.asset)
const url = this.formatterArgs.getConnectUrl(this.row, protocol)
window.open(url, '_blank')
},
visibleChange(visible) {
@@ -115,8 +111,7 @@ export default {
try {
const url = this.formatterArgs.assetUrl.replace('{id}', assetId)
const res = await this.$axios.get(url)
this.protocols = res.protocols.filter(protocol => (protocol.name !== 'winrm')) || []
this.protocols = res.protocols || []
} catch (e) {
throw new Error(`Error getting protocols: ${e}`)
}

View File

@@ -1,9 +1,8 @@
<template>
<el-popover
:title="title"
placement="left"
placement="left-start"
trigger="click"
width="300"
@show="getAsyncItems"
>
<div v-if="!loading" class="detail-content">

View File

@@ -24,17 +24,16 @@ 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 })
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
return (cellValue && typeof cellValue === 'object') ? cellValue.label : this.textChoices[key] || cellValue
},
getIcon({ row, cellValue }) {
const key = this.getKey({ row, cellValue })

View File

@@ -1,6 +1,6 @@
<script>
import BaseFormatter from './base.vue'
import { copy } from '@/utils/common/index'
import { copy } from '@/utils/common'
export default {
name: 'CopyableFormatter',

View File

@@ -4,7 +4,7 @@
<script>
import BaseFormatter from './base.vue'
import { toSafeLocalDateStr } from '@/utils/common/time'
import { toSafeLocalDateStr } from '@/utils/time'
export default {
name: 'DateFormatter',

View File

@@ -6,18 +6,14 @@
<i class="fa fa-check" />
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="item of iActions" :key="item.name" :command="item.name" :disabled="item.disabled">
<el-dropdown-item v-for="item of iActions" :key="item.name" :command="item.name">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-tooltip :content="$tc('Ignore')" :open-delay="400">
<el-button :disabled="!this.$hasPerm('accounts.change_accountrisk')" class="ignore action" size="mini">
<svg-icon
icon-class="ignore"
:style="!this.$hasPerm('accounts.change_accountrisk') ? 'pointer-events: none;' : ''"
@click="handleRisk('ignore')"
/>
<el-button class="ignore action" size="mini">
<svg-icon icon-class="ignore" @click="handleRisk('ignore')" />
</el-button>
</el-tooltip>
</span>
@@ -127,20 +123,17 @@ export default {
{
name: 'delete_remote',
label: this.$t('DeleteRemoteAccount'),
has: this.row.remote_present,
disabled: this.$store.getters.currentOrgIsRoot || !this.$hasPerm('accounts.remove_account') || !this.$hasPerm('accounts.change_accountrisk')
has: this.row.remote_present
},
{
name: 'add_account',
label: this.$t('AddAccount'),
has: !this.row.present,
disabled: this.$store.getters.currentOrgIsRoot || !this.$hasPerm('accounts.add_account') || !this.$hasPerm('accounts.change_accountrisk')
has: !this.row.present
},
{
name: 'change_password_add',
label: this.$t('AddAccountAfterChangingPassword'),
has: !this.row.present,
disabled: this.$store.getters.currentOrgIsRoot || !this.$hasPerm('accounts.add_pushaccountexecution') || !this.$hasPerm('accounts.change_accountrisk')
has: !this.row.present
}
]
return actions.filter(action => {

View File

@@ -47,10 +47,12 @@ export default {
}
},
data() {
const valueIsString = typeof this.cellValue === 'string'
const jsonValue = this.cellValue ? JSON.stringify(this.cellValue) : ''
return {
inEditMode: false,
value: '',
valueIsString: '',
value: valueIsString ? this.cellValue || '' : jsonValue,
valueIsString: valueIsString,
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
@@ -65,16 +67,6 @@ export default {
return this.getCellValue(this.cellValue)
}
},
watch: {
cellValue: {
immediate: true,
handler(newVal) {
const valueIsString = typeof newVal === 'string'
this.value = valueIsString ? newVal || '' : (newVal ? JSON.stringify(newVal) : '')
this.valueIsString = valueIsString
}
}
},
methods: {
editCell() {
if (this.formatterArgs.canEdit) {

View File

@@ -50,75 +50,23 @@ export default {
return cellValue
},
viewText() {
const cpuCount = Number(this.items['cpu_count'] || 0)
const cpuCores = Number(this.items['cpu_cores'] || 0)
const cpuVcpus = Number(this.items['cpu_vcpus'] || 0)
const memory = Number(this.items['memory'] || 0)
const diskTotal = Number(this.items['disk_total'] || 0)
const rawCpuModel = this.items['cpu_model']
const rawGpuModel = this.items['gpu_model']
let summary = ''
const cpuCount = this.items['cpu_count']
const cpuCores = this.items['cpu_cores']
const cpuVcpus = this.items['cpu_vcpus']
const memory = this.items['memory']
const diskTotal = this.items['disk_total']
if (cpuCount) {
const coreCount = cpuVcpus || (cpuCores * cpuCount)
summary = `${coreCount} Core`
let text = `${cpuVcpus || cpuCores * cpuCount} Core`
if (memory) {
summary += ` ${Math.round(memory)}G`
text += ` ${memory}G`
}
if (diskTotal) {
summary += ` ${Math.round(diskTotal)}G`
text += ` ${diskTotal}G`
}
const cpuModel = this.formatCpuModel(rawCpuModel)
if (cpuModel) {
summary += ` (${cpuModel})`
}
if (rawGpuModel) {
const gpu = this.formatGpuModel(rawGpuModel)
summary += ` ${gpu}`
}
return summary
return text
}
return this.items?.distribution || '-'
}
},
methods: {
formatCpuModel(raw) {
if (!raw) return ''
const match = raw.match(/^([^\s]+)\s*(x\d+)?$/)
let base = match?.[1] || raw
const suffix = match?.[2] || ''
base = base
.replace(/GenuineIntel/i, '')
.replace(/\b\d+(st|nd|rd|th)? Gen\b/i, '')
.replace(/\(R\)/g, '')
.replace(/\(TM\)/g, '')
.replace(/\s+/g, ' ')
.trim()
return `${base}${suffix}`
},
formatGpuModel(raw) {
if (!raw) return ''
const match = raw.match(/(.+?),\s*(\d+),\s*([\d.]+)(?:\s*x(\d+))?/)
if (match) {
const [, model, vramMb, driverVersion, countStr] = match
const count = countStr ? parseInt(countStr) : 1
const vramGb = Math.round(parseInt(vramMb) / 1024)
const label = `${model} (${vramGb}GB, Driver ${driverVersion})`
return count > 1 ? `${label} x${count}` : label
}
return raw // fallback
}
}
}
</script>

View File

@@ -9,7 +9,7 @@
<script>
import BaseFormatter from './base.vue'
import { loadPlatformIcon } from '@/utils/jms/index'
import { loadPlatformIcon } from '@/utils/jms'
export default {
name: 'PlatformFormatter',

View File

@@ -36,7 +36,7 @@
</template>
<script>
import { copy, downloadText } from '@/utils/common/index'
import { copy, downloadText } from '@/utils/common'
import BaseFormatter from '@/components/Table/TableFormatters/base.vue'
export default {

View File

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

Some files were not shown because too many files have changed in this diff Show More