Compare commits
11 Commits
pr@v3@perf
...
v2.20.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43badfa0b3 | ||
|
|
d3cfcbf71f | ||
|
|
89141636a2 | ||
|
|
085789f6c5 | ||
|
|
0f47f99786 | ||
|
|
a549f3f81f | ||
|
|
3b54ad8b00 | ||
|
|
e5e976e007 | ||
|
|
ad4ddcd1c0 | ||
|
|
944aba8b26 | ||
|
|
36d8e8cca6 |
@@ -4,17 +4,11 @@ root = true
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_size = 2
|
||||
|
||||
[*.py]
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
@@ -22,5 +22,4 @@ VUE_APP_LOGOUT_PATH = '/core/auth/logout/'
|
||||
# Dev server for core proxy
|
||||
VUE_APP_CORE_HOST = 'http://localhost:8080'
|
||||
VUE_APP_CORE_WS = 'ws://localhost:8080'
|
||||
VUE_APP_KAEL_HOST = 'http://localhost:8083'
|
||||
VUE_APP_ENV = 'development'
|
||||
|
||||
32
.github/workflows/jms-build-test.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: "Run Build Test"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- pr@*
|
||||
- repr@*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
|
||||
- uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: jumpserver/lina:test
|
||||
file: Dockerfile
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- uses: LouisBrunner/checks-action@v1.5.0
|
||||
if: always()
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: Check Build
|
||||
conclusion: ${{ job.status }}
|
||||
@@ -10,4 +10,3 @@ jobs:
|
||||
- uses: jumpserver/action-generic-handler@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
||||
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
|
||||
|
||||
37
.github/workflows/release-drafter.yml
vendored
@@ -19,7 +19,9 @@ jobs:
|
||||
id: get_version
|
||||
run: |
|
||||
TAG=$(basename ${GITHUB_REF})
|
||||
VERSION=${TAG/v/}
|
||||
echo "::set-output name=TAG::$TAG"
|
||||
echo "::set-output name=VERSION::$VERSION"
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: release-drafter/release-drafter@v5
|
||||
@@ -29,29 +31,16 @@ jobs:
|
||||
config-name: release-config.yml
|
||||
version: ${{ steps.get_version.outputs.TAG }}
|
||||
tag: ${{ steps.get_version.outputs.TAG }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.20'
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Build web
|
||||
run: |
|
||||
sed -i "s@version-dev@${{ steps.get_version.outputs.TAG }}@g" src/layout/components/NavHeader/About.vue
|
||||
yarn build
|
||||
- name: Create Upload Assets
|
||||
run: |
|
||||
rm -rf build/*
|
||||
mv lina lina-${{ steps.get_version.outputs.TAG }}
|
||||
tar -czf lina-${{ steps.get_version.outputs.TAG }}.tar.gz lina-${{ steps.get_version.outputs.TAG }}
|
||||
echo $(md5sum lina-${{ steps.get_version.outputs.TAG }}.tar.gz | awk '{print $1}') > build/lina-${{ steps.get_version.outputs.TAG }}.tar.gz.md5
|
||||
mv lina-${{ steps.get_version.outputs.TAG }}.tar.gz build/
|
||||
- name: Release Upload Assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
build/lina-${{ steps.get_version.outputs.TAG }}.tar.gz
|
||||
build/lina-${{ steps.get_version.outputs.TAG }}.tar.gz.md5
|
||||
|
||||
build-and-release:
|
||||
needs: create-realese
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build it and upload
|
||||
uses: jumpserver/action-build-upload-assets@node10
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-realese.outputs.upload_url }}
|
||||
|
||||
1
.gitignore
vendored
@@ -16,4 +16,3 @@ tests/**/coverage/
|
||||
*.njsproj
|
||||
*.sln
|
||||
.env.development
|
||||
.python-version
|
||||
|
||||
48
Dockerfile
@@ -1,39 +1,23 @@
|
||||
FROM node:16.20-bullseye-slim as stage-build
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
python3"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=lina \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ${DEPENDENCIES} \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG NPM_REGISTRY="https://registry.npmmirror.com"
|
||||
RUN set -ex \
|
||||
&& npm config set registry ${NPM_REGISTRY} \
|
||||
&& yarn config set registry ${NPM_REGISTRY}
|
||||
FROM node:10 as stage-build
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
ARG NPM_REGISTRY="https://registry.npm.taobao.org"
|
||||
ENV NPM_REGISTY=$NPM_REGISTRY
|
||||
ARG SASS_BINARY_SITE="https://npm.taobao.org/mirrors/node-sass"
|
||||
ENV SASS_BINARY_SITE=$SASS_BINARY_SITE
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
ADD package.json yarn.lock /data
|
||||
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked,id=lina \
|
||||
yarn install
|
||||
RUN npm config set sass_binary_site=${SASS_BINARY_SITE}
|
||||
RUN npm config set registry ${NPM_REGISTRY}
|
||||
RUN yarn config set registry ${NPM_REGISTRY}
|
||||
COPY package.json yarn.lock /data/
|
||||
RUN yarn install
|
||||
RUN npm rebuild node-sass
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
ADD . /data
|
||||
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked,id=lina \
|
||||
sed -i "s@version-dev@${VERSION}@g" src/layout/components/NavHeader/About.vue \
|
||||
&& yarn build
|
||||
RUN cd utils && bash -xieu build.sh build
|
||||
|
||||
FROM nginx:1.24-bullseye
|
||||
COPY --from=stage-build /data/lina /opt/lina
|
||||
FROM nginx:alpine
|
||||
COPY --from=stage-build /data/release/lina /opt/lina
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
@@ -19,7 +19,7 @@ VUE_APP_CORE_HOST = 'JUMPSERVER_APIHOST'
|
||||
$ yarn serve
|
||||
|
||||
4. 构建
|
||||
$ yarn build:prod
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
## 生产中部署
|
||||
|
||||
@@ -24,9 +24,9 @@ if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
|
||||
)
|
||||
|
||||
app.listen(port, function () {
|
||||
// debug(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
|
||||
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
|
||||
if (report) {
|
||||
// debug(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
|
||||
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
@@ -56,7 +56,7 @@ const responseFake = (url, type, respond) => {
|
||||
url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
|
||||
type: type || 'get',
|
||||
response(req, res) {
|
||||
// debug('request invoke:' + req.path)
|
||||
console.log('request invoke:' + req.path)
|
||||
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,9 @@ module.exports = app => {
|
||||
mockRoutesLength = mockRoutes.mockRoutesLength
|
||||
mockStartIndex = mockRoutes.mockStartIndex
|
||||
|
||||
// debug(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
|
||||
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
|
||||
} catch (error) {
|
||||
// debug(chalk.redBright(error))
|
||||
console.log(chalk.redBright(error))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
const tokens = {
|
||||
admin: {
|
||||
token: 'admin-token'
|
||||
@@ -45,6 +46,7 @@ export default [
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// get user info
|
||||
{
|
||||
url: '/vue-admin-template/user/info\.*',
|
||||
|
||||
39
package.json
@@ -7,45 +7,35 @@
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve",
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "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"
|
||||
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"@ztree/ztree_v3": "3.5.44",
|
||||
"axios": "0.28.0",
|
||||
"axios": "0.21.1",
|
||||
"axios-retry": "^3.1.9",
|
||||
"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",
|
||||
"echarts": "^4.7.0",
|
||||
"element-ui": "2.13.2",
|
||||
"eslint-plugin-html": "^6.0.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"install": "^0.13.0",
|
||||
"jquery": "^3.6.1",
|
||||
"jquery": "^3.5.0",
|
||||
"js-cookie": "2.2.0",
|
||||
"jsencrypt": "^3.2.1",
|
||||
"krry-transfer": "^1.7.3",
|
||||
"less": "^3.10.3",
|
||||
"less-loader": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.frompairs": "^4.0.1",
|
||||
"lodash.get": "^4.4.2",
|
||||
@@ -57,34 +47,30 @@
|
||||
"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",
|
||||
"moment": "^2.29.1",
|
||||
"moment-parseformat": "^3.0.0",
|
||||
"normalize.css": "7.0.0",
|
||||
"npm": "^7.8.0",
|
||||
"nprogress": "0.2.0",
|
||||
"path-to-regexp": "2.4.0",
|
||||
"vue": "2.6.10",
|
||||
"vue-codemirror": "4.0.6",
|
||||
"vue-codemirror-lite": "^1.0.4",
|
||||
"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",
|
||||
"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/core": "7.0.0",
|
||||
"@babel/register": "7.0.0",
|
||||
"@vue/cli-plugin-babel": "3.6.0",
|
||||
"@vue/cli-plugin-eslint": "^3.9.1",
|
||||
@@ -98,24 +84,21 @@
|
||||
"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": "^1.26.10",
|
||||
"sass-loader": "^7.1.0",
|
||||
"script-ext-html-webpack-plugin": "2.1.3",
|
||||
"script-loader": "0.7.2",
|
||||
"serve-static": "^1.13.2",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"svg-sprite-loader": "4.1.3",
|
||||
"svgo": "1.2.2",
|
||||
"vue-i18n-extract": "^1.1.1",
|
||||
|
||||
@@ -9,7 +9,23 @@
|
||||
<meta http-equiv="Cache" content="no-cache">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title><%= webpackConfig.name %></title>
|
||||
<link rel="stylesheet" href="<%= BASE_URL %>theme/element-ui.css">
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
width:14px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
border-radius:10px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 8px;
|
||||
box-shadow: 8px 10px 20px #C6C6C6 inset;
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
box-shadow: 8px 10px 20px #878787 inset;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
@@ -17,18 +33,9 @@
|
||||
</noscript>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
if (location.pathname === '/') {
|
||||
location.pathname = '/ui/'
|
||||
}
|
||||
const pathname = window.location.pathname
|
||||
if (pathname.startsWith('/core')) {
|
||||
return
|
||||
}
|
||||
if(pathname.indexOf('/ui') === -1) {
|
||||
window.location.href = window.location.origin + '/ui/#' + pathname
|
||||
}
|
||||
if (pathname.startsWith('/ui/#/chat')) {
|
||||
window.location.href = window.location.origin + pathname
|
||||
const baseUrl = "/ui/"
|
||||
if (location.pathname === '/' && baseUrl !== '/') {
|
||||
location.pathname = baseUrl
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# 主题颜色
|
||||
```
|
||||
alpha-1: "rgba(64, 158, 255, 0.1)"
|
||||
alpha-2: "rgba(64, 158, 255, 0.2)"
|
||||
alpha-3: "rgba(64, 158, 255, 0.3)"
|
||||
alpha-4: "rgba(64, 158, 255, 0.4)"
|
||||
alpha-5: "rgba(64, 158, 255, 0.5)"
|
||||
alpha-6: "rgba(64, 158, 255, 0.6)"
|
||||
alpha-7: "rgba(64, 158, 255, 0.7)"
|
||||
alpha-8: "rgba(64, 158, 255, 0.8)"
|
||||
alpha-9: "rgba(64, 158, 255, 0.9)"
|
||||
light-1: "#53a8ff"
|
||||
light-2: "#66b1ff"
|
||||
light-3: "#79bbff"
|
||||
light-4: "#8cc5ff"
|
||||
light-5: "#a0cfff"
|
||||
light-6: "#b3d8ff"
|
||||
light-7: "#c6e2ff"
|
||||
light-8: "#d9ecff"
|
||||
light-9: "#ecf5ff"
|
||||
primary: "#409EFF"
|
||||
```
|
||||
primary是初始主题颜色,其他颜色均属于primary的系列颜色
|
||||
14
src/App.vue
@@ -1,21 +1,11 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view v-if="isRouterAlive" />
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
computed: {
|
||||
...mapState({
|
||||
isRouterAlive: state => state.common.isRouterAlive
|
||||
})
|
||||
}
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
@@ -40,12 +40,3 @@ export function getCommandFilterList(data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getCategoryTypes() {
|
||||
return request({
|
||||
url: '/api/v1/assets/categories/?limit=1000',
|
||||
method: 'get'
|
||||
}).then(res => {
|
||||
return res.results
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function createSourceIdCache(ids) {
|
||||
ids = ids.map(item => {
|
||||
if (typeof item === 'object' && item.id) {
|
||||
return item.id
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
})
|
||||
return request({
|
||||
url: '/api/v1/common/resources/cache/',
|
||||
method: 'post',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function updateInterface(formData) {
|
||||
export function postInterface(formData) {
|
||||
return request({
|
||||
url: '/api/v1/xpack/interface/setting/',
|
||||
url: '/api/v1/xpack/interface/setting',
|
||||
method: 'put',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
@@ -12,15 +12,15 @@ export function updateInterface(formData) {
|
||||
}
|
||||
export function getInterfaceInfo() {
|
||||
return request({
|
||||
url: '/api/v1/xpack/interface/setting/',
|
||||
url: '/api/v1/xpack/interface/setting',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function restoreInterface() {
|
||||
return request({
|
||||
url: '/api/v1/xpack/interface/setting/restore/',
|
||||
method: 'put'
|
||||
url: '/api/v1/xpack/interface/restore',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,10 +34,3 @@ export function importLicense(formData) {
|
||||
data: formData
|
||||
})
|
||||
}
|
||||
|
||||
export function previewThemes() {
|
||||
return request({
|
||||
url: `/api/v1/xpack/interface/setting/themes/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getTaskDetail(id) {
|
||||
return request({
|
||||
url: `/api/v1/ops/tasks/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getAdhocDetail(id) {
|
||||
return request({
|
||||
url: `/api/v1/ops/adhoc/${id}/`,
|
||||
@@ -13,61 +20,3 @@ export function getHistoryExecutionDetail(id) {
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getTaskDetail(id) {
|
||||
return request({
|
||||
url: `/api/v1/ops/job-execution/task-detail/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getJob(id) {
|
||||
return request({
|
||||
url: `/api/v1/ops/jobs/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function uploadPlaybook(form) {
|
||||
return request({
|
||||
url: '/api/v1/ops/playbooks/',
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
data: form
|
||||
})
|
||||
}
|
||||
|
||||
export function renameFile(playbookId, node) {
|
||||
return request({
|
||||
url: `/api/v1/ops/playbook/${playbookId}/file/`,
|
||||
method: 'patch',
|
||||
data: node
|
||||
})
|
||||
}
|
||||
|
||||
export function createJob(form) {
|
||||
return request({
|
||||
url: '/api/v1/ops/jobs/',
|
||||
method: 'post',
|
||||
data: form
|
||||
})
|
||||
}
|
||||
|
||||
export function StopJob(form) {
|
||||
return request({
|
||||
url: '/api/v1/ops/job-executions/stop/',
|
||||
method: 'post',
|
||||
data: form
|
||||
})
|
||||
}
|
||||
|
||||
export function JobUploadFile(form) {
|
||||
return request({
|
||||
url: '/api/v1/ops/jobs/upload/',
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 60 * 60 * 1000,
|
||||
data: form
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,3 @@ export function getCurrentOrg() {
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
getCurrentOrg,
|
||||
getOrgDetail
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getAssetPermissionDetail(id) {
|
||||
return request({
|
||||
url: `/api/v1/perms/asset-permissions/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getRemoteAppPermissionDetail(id) {
|
||||
return request({
|
||||
url: `/api/v1/perms/remote-app-permissions/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getDatabaseAppPermissionDetail(id) {
|
||||
return request({
|
||||
url: `/api/v1/perms/database-app-permissions/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getUserAssetGrantedSystemUsers(userId, assetId) {
|
||||
return request({
|
||||
url: `/api/v1/perms/users/${userId}/assets/${assetId}/system-users/?cache_policy=1`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getMyAssetGrantedSystemUsers(userId, assetId) {
|
||||
return request({
|
||||
url: `/api/v1/perms/users/assets/${assetId}/system-users/?cache_policy=1`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getUserGroupAssetGrantedSystemUsers(gId, assetId) {
|
||||
return request({
|
||||
url: `/api/v1/perms/user-groups/${gId}/assets/${assetId}/system-users/?cache_policy=1`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,11 +8,24 @@ export function terminateSession(data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function toggleLockSession(data) {
|
||||
export function getSessionDetail(id) {
|
||||
return request({
|
||||
url: '/api/v1/terminal/tasks/toggle-lock-session/',
|
||||
method: 'post',
|
||||
data: data
|
||||
url: `/api/v1/terminal/sessions/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getSessionCommands(id) {
|
||||
return request({
|
||||
url: `/api/v1/terminal/commands/?session_id=${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getTerminalDetail(id) {
|
||||
return request({
|
||||
url: `/api/v1/terminal/terminals/${id}/`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -25,28 +25,12 @@ export function importLicense(formData) {
|
||||
data: formData
|
||||
})
|
||||
}
|
||||
export function testLdapSetting(data, refresh = true) {
|
||||
let url = '/api/v1/settings/ldap/testing/config/'
|
||||
if (refresh) {
|
||||
url = url + '?refresh=1'
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
disableFlashErrorMsg: true,
|
||||
url: url,
|
||||
method: 'post',
|
||||
data: data
|
||||
}).then(res => {
|
||||
if (res.status !== 'running') {
|
||||
resolve(res)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
resolve(testLdapSetting(data, false))
|
||||
}, 1000)
|
||||
}
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
export function testLdapSetting(data) {
|
||||
return request({
|
||||
disableFlashErrorMsg: true,
|
||||
url: '/api/v1/settings/ldap/testing/config/',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,21 +68,15 @@ export function importLdapUser(data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getPublicSettings(isOpen) {
|
||||
let url
|
||||
if (isOpen) {
|
||||
url = '/api/v1/settings/public/open/'
|
||||
} else {
|
||||
url = '/api/v1/settings/public/'
|
||||
}
|
||||
export function getPublicSettings() {
|
||||
return request({
|
||||
url: url,
|
||||
url: '/api/v1/settings/public/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
export function getLogo() {
|
||||
return request({
|
||||
url: '/api/v1/xpack/interface/setting/',
|
||||
url: '/api/v1/xpack/interface/setting',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export function getProfile(token) {
|
||||
return request({
|
||||
url: '/api/v1/users/profile/',
|
||||
method: 'get'
|
||||
// params: { token }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,8 +69,3 @@ export function logout() {
|
||||
export function refreshSessionIdAge() {
|
||||
return getProfile()
|
||||
}
|
||||
|
||||
export default {
|
||||
getProfile,
|
||||
getUserList
|
||||
}
|
||||
|
||||
BIN
src/assets/img/admin.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="transparent" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot "><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>
|
||||
|
Before Width: | Height: | Size: 406 B |
BIN
src/assets/img/header-profile.png
Normal file → Executable file
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
85
src/components/AccountListTable/ShowSecretInfo.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div>
|
||||
<MFAVerifyDialog
|
||||
@MFAVerifyDone="getAuthInfo"
|
||||
@MFAVerifyCancel="exit"
|
||||
/>
|
||||
<Dialog
|
||||
:title="dialogTitle"
|
||||
:show-confirm="false"
|
||||
:show-cancel="false"
|
||||
:destroy-on-close="true"
|
||||
:width="'50'"
|
||||
:visible.sync="showAuthInfo"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div>
|
||||
<el-form label-position="right" label-width="80px" :model="authInfo">
|
||||
<el-form-item :label="this.$t('assets.Hostname')">
|
||||
<el-input v-model="account.hostname" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Username')">
|
||||
<el-input v-model="account['username']" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Password')">
|
||||
<el-input v-model="authInfo.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('users.SSHKey')">
|
||||
<el-input v-model="authInfo['private_key']" class="item-textarea" type="textarea" show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
|
||||
export default {
|
||||
name: 'ShowSecretInfo',
|
||||
components: {
|
||||
Dialog,
|
||||
MFAVerifyDialog
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogTitle: this.$t('common.ViewSecret'),
|
||||
authInfo: {},
|
||||
showAuthInfo: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getAuthInfo()
|
||||
},
|
||||
methods: {
|
||||
getAuthInfo() {
|
||||
const url = `/api/v1/assets/account-secrets/${this.account.id}/`
|
||||
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
|
||||
this.authInfo = resp
|
||||
this.showAuthInfo = true
|
||||
})
|
||||
},
|
||||
exit() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.item-textarea >>> .el-textarea__inner {
|
||||
height: 110px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +1,27 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:title="$tc('assets.UpdateAssetUserToken')"
|
||||
:visible.sync="visible"
|
||||
width="50"
|
||||
@cancel="handleCancel()"
|
||||
:title="this.$t('assets.UpdateAssetUserToken')"
|
||||
:visible.sync="visible"
|
||||
:destroy-on-close="true"
|
||||
@confirm="handleConfirm()"
|
||||
@cancel="handleCancel()"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-form label-position="right" label-width="90px">
|
||||
<el-form-item :label="$tc('assets.Name')">
|
||||
<el-input v-model="account['asset_name']" readonly />
|
||||
<el-form-item :label="this.$t('assets.Hostname')">
|
||||
<el-input v-model="account.hostname" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('assets.Username')">
|
||||
<el-form-item :label="this.$t('assets.Username')">
|
||||
<el-input v-model="account['username']" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('assets.Password')">
|
||||
<el-form-item :label="this.$t('assets.Password')">
|
||||
<UpdateToken v-model="authInfo.password" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('assets.SSHSecretKey')">
|
||||
<el-form-item :label="this.$t('assets.SSHSecretKey')">
|
||||
<UploadKey @input="getFile" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('assets.Passphrase')">
|
||||
<el-form-item :label="this.$t('assets.Passphrase')">
|
||||
<UpdateToken v-model="authInfo.passphrase" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -29,10 +29,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { UpdateToken, UploadKey } from '@/components/Form/FormFields'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
|
||||
import Dialog from '@/components/Dialog'
|
||||
import { UpdateToken, UploadKey } from '@/components/FormFields'
|
||||
export default {
|
||||
name: 'UpdateSecretInfo',
|
||||
components: {
|
||||
@@ -52,7 +50,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
secretInfo: {
|
||||
authInfo: {
|
||||
password: '',
|
||||
private_key: '',
|
||||
passphrase: ''
|
||||
@@ -62,15 +60,15 @@ export default {
|
||||
methods: {
|
||||
handleConfirm() {
|
||||
const data = {}
|
||||
if (this.secretInfo.password !== '') {
|
||||
data.password = encryptPassword(this.secretInfo.password)
|
||||
if (this.authInfo.password !== '') {
|
||||
data.password = this.authInfo.password
|
||||
}
|
||||
if (this.secretInfo.private_key !== '') {
|
||||
data.private_key = encryptPassword(this.secretInfo.private_key)
|
||||
if (this.secretInfo.passphrase) data.passphrase = this.secretInfo.passphrase
|
||||
if (this.authInfo.private_key !== '') {
|
||||
data.private_key = this.authInfo.private_key
|
||||
if (this.authInfo.passphrase) data.passphrase = this.authInfo.passphrase
|
||||
}
|
||||
this.$axios.patch(
|
||||
`/api/v1/accounts/accounts/${this.account.id}/`,
|
||||
`/api/v1/assets/accounts/${this.account.id}/`,
|
||||
data,
|
||||
{ disableFlashErrorMsg: true }
|
||||
).then(res => {
|
||||
@@ -88,7 +86,7 @@ export default {
|
||||
this.$emit('update:visible', false)
|
||||
},
|
||||
getFile(file) {
|
||||
this.secretInfo.private_key = file
|
||||
this.authInfo.private_key = file
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/components/AccountListTable/const.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
import ChoicesFormatter from '@/components/TableFormatters/ChoicesFormatter'
|
||||
import i18n from '@/i18n/i18n'
|
||||
|
||||
export const connectivityMeta = {
|
||||
label: i18n.t('assets.Reachable'),
|
||||
formatter: ChoicesFormatter,
|
||||
formatterArgs: {
|
||||
iconChoices: {
|
||||
ok: 'fa-check',
|
||||
failed: 'fa-times',
|
||||
unknown: 'fa-circle-o'
|
||||
},
|
||||
classChoices: {
|
||||
ok: 'text-primary',
|
||||
failed: 'text-danger',
|
||||
unknown: 'text-warning'
|
||||
},
|
||||
hasTips: true,
|
||||
getTips: ({ row, cellValue }) => {
|
||||
const mapper = {
|
||||
'ok': i18n.t('assets.Reachable'),
|
||||
'failed': i18n.t('assets.Unreachable'),
|
||||
'unknown': i18n.t('assets.Unknown')
|
||||
}
|
||||
let tips = mapper[cellValue]
|
||||
if (row['date_verified']) {
|
||||
const datetime = toSafeLocalDateStr(row['date_verified'])
|
||||
tips += '<br> ' + datetime
|
||||
}
|
||||
return tips
|
||||
}
|
||||
},
|
||||
width: '90px',
|
||||
align: 'center'
|
||||
}
|
||||
199
src/components/AccountListTable/index.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div>
|
||||
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
|
||||
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
|
||||
<UpdateSecretInfo v-if="showUpdateSecretDialog" :visible.sync="showUpdateSecretDialog" :account="account" @updateAuthDone="onUpdateAuthDone" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/ListTable/index'
|
||||
import { ActionsFormatter, DetailFormatter, DisplayFormatter } from '@/components/TableFormatters'
|
||||
import ShowSecretInfo from './ShowSecretInfo'
|
||||
import UpdateSecretInfo from './UpdateSecretInfo'
|
||||
import { connectivityMeta } from './const'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'AccountListTable',
|
||||
components: {
|
||||
ListTable,
|
||||
UpdateSecretInfo,
|
||||
ShowSecretInfo
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
exportUrl: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.url.replace('/assets/accounts/', '/assets/account-secrets/')
|
||||
}
|
||||
},
|
||||
hasLeftActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
otherActions: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
hasClone: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
return {
|
||||
showViewSecretDialog: false,
|
||||
showUpdateSecretDialog: false,
|
||||
account: {},
|
||||
tableConfig: {
|
||||
url: this.url,
|
||||
permissions: {
|
||||
app: 'assets',
|
||||
resource: 'authbook'
|
||||
},
|
||||
columns: [
|
||||
'hostname', 'ip', 'username', 'version', 'connectivity',
|
||||
'systemuser', 'date_created', 'date_updated', 'actions'
|
||||
],
|
||||
columnsShow: {
|
||||
min: ['username', 'actions'],
|
||||
default: ['hostname', 'ip', 'username', 'version', 'actions']
|
||||
},
|
||||
columnsMeta: {
|
||||
hostname: {
|
||||
prop: 'hostname',
|
||||
label: this.$t('assets.Hostname'),
|
||||
showOverflowTooltip: true,
|
||||
formatter: DetailFormatter,
|
||||
formatterArgs: {
|
||||
can: this.$hasPerm('assets.view_asset'),
|
||||
getRoute({ row }) {
|
||||
return {
|
||||
name: 'AssetDetail',
|
||||
params: { id: row.asset }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ip: {
|
||||
width: '120px'
|
||||
},
|
||||
username: {
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
systemuser: {
|
||||
formatter: DisplayFormatter
|
||||
},
|
||||
version: {
|
||||
width: '70px'
|
||||
},
|
||||
connectivity: connectivityMeta,
|
||||
actions: {
|
||||
formatter: ActionsFormatter,
|
||||
formatterArgs: {
|
||||
hasUpdate: false, // can set function(row, value)
|
||||
hasDelete: false, // can set function(row, value)
|
||||
hasClone: this.hasClone,
|
||||
moreActionsTitle: this.$t('common.More'),
|
||||
extraActions: [
|
||||
{
|
||||
name: 'View',
|
||||
title: this.$t('common.View'),
|
||||
can: this.$hasPerm('assets.view_assetaccountsecret'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
vm.account = row
|
||||
vm.showViewSecretDialog = true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
title: this.$t('common.Delete'),
|
||||
can: this.$hasPerm('assets.delete_authbook'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
this.$axios.delete(`/api/v1/assets/accounts/${row.id}/`).then(() => {
|
||||
this.$message.success(this.$tc('common.deleteSuccessMsg'))
|
||||
this.$refs.ListTable.reloadTable()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
title: this.$t('common.Test'),
|
||||
can: this.$hasPerm('assets.test_authbook'),
|
||||
callback: ({ row }) => {
|
||||
this.$axios.post(
|
||||
`/api/v1/assets/accounts/${row.id}/verify/`,
|
||||
{ action: 'test' }
|
||||
).then(res => {
|
||||
openTaskPage(res['task'])
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
title: this.$t('common.Update'),
|
||||
can: this.$hasPerm('assets.change_assetaccountsecret') && !this.$store.getters.currentOrgIsRoot,
|
||||
callback: ({ row }) => {
|
||||
vm.account = row
|
||||
vm.showUpdateSecretDialog = false
|
||||
setTimeout(() => {
|
||||
vm.showUpdateSecretDialog = true
|
||||
console.log('Show update1: ', vm.showUpdateSecretDialog)
|
||||
})
|
||||
console.log('Show update2: ', vm.showUpdateSecretDialog)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: this.hasLeftActions,
|
||||
hasMoreActions: false,
|
||||
hasImport: false,
|
||||
hasExport: this.$hasPerm('assets.view_assetaccountsecret'),
|
||||
exportOptions: {
|
||||
url: this.exportUrl,
|
||||
mfaVerifyRequired: true
|
||||
},
|
||||
searchConfig: {
|
||||
exclude: ['systemuser', 'asset']
|
||||
},
|
||||
hasSearch: true
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
url(iNew) {
|
||||
this.$set(this.tableConfig, 'url', iNew)
|
||||
this.$set(this.headerActions.exportOptions, 'url', iNew.replace('/accounts/', '/account-secrets/'))
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.otherActions) {
|
||||
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
|
||||
for (const item of this.otherActions) {
|
||||
actionColumn.formatterArgs.extraActions.push(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onUpdateAuthDone(account) {
|
||||
Object.assign(this.account, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
|
||||
</style>
|
||||
@@ -18,16 +18,16 @@ export default {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
moreActionBtn: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
moreActionsTitle: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('common.MoreActions')
|
||||
}
|
||||
},
|
||||
moreActionsType: {
|
||||
type: String,
|
||||
default: 'default'
|
||||
},
|
||||
moreActionsPlacement: {
|
||||
type: String,
|
||||
default: 'bottom'
|
||||
@@ -43,21 +43,14 @@ export default {
|
||||
return actions
|
||||
},
|
||||
iMoreAction() {
|
||||
const defaultBtn = {
|
||||
return {
|
||||
name: 'moreActions',
|
||||
title: this.$t('common.MoreActions'),
|
||||
type: 'primary',
|
||||
plain: true
|
||||
}
|
||||
const btn = {
|
||||
...defaultBtn,
|
||||
...this.moreActionBtn,
|
||||
title: this.iMoreActionsTitle,
|
||||
dropdown: this.moreActions || []
|
||||
}
|
||||
if (this.moreActionsTitle) {
|
||||
btn.title = this.moreActionsTitle
|
||||
}
|
||||
return btn
|
||||
},
|
||||
iMoreActionsTitle() {
|
||||
return this.moreActionsTitle || this.$t('common.MoreActions')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,23 +7,21 @@
|
||||
:title="title"
|
||||
@close="onClose"
|
||||
>
|
||||
<MarkDown class="markdown" :value="announcement.content" />
|
||||
<span class="announcement-main"> {{ announcement.content }}</span>
|
||||
<span v-if="announcement.link">
|
||||
<el-link :href="announcement.link" target="_blank" type="info" class="link-more">
|
||||
<el-link :href="announcement.link" target="_blank" class="link-more">
|
||||
{{ $t('common.ViewMore') }}
|
||||
</el-link>
|
||||
<i class="fa fa-external-link icon" />
|
||||
<i class="fa fa-share-square-o" />
|
||||
</span>
|
||||
</el-alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import MarkDown from '@/components/Widgets/MarkDown'
|
||||
|
||||
export default {
|
||||
name: 'Announcement',
|
||||
components: { MarkDown },
|
||||
data() {
|
||||
return {
|
||||
viewedKey: 'AnnouncementViewed'
|
||||
@@ -56,30 +54,17 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.announcement >>> .el-alert__content {
|
||||
width: 100%;
|
||||
}
|
||||
.announcement-main {
|
||||
word-wrap:break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.link-more {
|
||||
font-size: 10px;
|
||||
margin-left: 10px;
|
||||
border-bottom: solid 1px;
|
||||
}
|
||||
.icon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
>>> .markdown-body {
|
||||
background-color: transparent !important;
|
||||
a {
|
||||
color: var(--color-info) !important;
|
||||
}
|
||||
h1, h2, h3, h4, h5 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
80
src/components/AppAccountListTable/ShowSecretInfo.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<MFAVerifyDialog
|
||||
@MFAVerifyDone="getAuthInfo"
|
||||
@MFAVerifyCancel="exit"
|
||||
/>
|
||||
<Dialog
|
||||
:title="dialogTitle"
|
||||
:show-confirm="false"
|
||||
:show-cancel="false"
|
||||
:destroy-on-close="true"
|
||||
:width="'50'"
|
||||
:visible.sync="showAuthInfo"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div>
|
||||
<el-form label-position="right" label-width="80px" :model="authInfo">
|
||||
<el-form-item :label="this.$t('applications.appName')">
|
||||
<el-input v-model="account['app_display']" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Username')">
|
||||
<el-input v-model="account['username']" readonly />
|
||||
</el-form-item>
|
||||
<el-form-item :label="this.$t('assets.Password')">
|
||||
<el-input v-model="authInfo.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog'
|
||||
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
|
||||
export default {
|
||||
name: 'ShowSecretInfo',
|
||||
components: {
|
||||
Dialog,
|
||||
MFAVerifyDialog
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogTitle: this.$t('common.ViewSecret'),
|
||||
authInfo: {},
|
||||
showAuthInfo: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getAuthInfo()
|
||||
},
|
||||
methods: {
|
||||
getAuthInfo() {
|
||||
const url = `/api/v1/applications/account-secrets/${this.account.id}/`
|
||||
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
|
||||
this.authInfo = resp
|
||||
this.showAuthInfo = true
|
||||
})
|
||||
},
|
||||
exit() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
31
src/components/AppAccountListTable/const.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ChoicesFormatter } from '@/components/TableFormatters'
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
import i18n from '@/i18n/i18n'
|
||||
|
||||
export const connectivityMeta = {
|
||||
label: i18n.t('assets.Reachable'),
|
||||
formatter: ChoicesFormatter,
|
||||
formatterArgs: {
|
||||
iconChoices: {
|
||||
ok: 'fa-check text-primary',
|
||||
failed: 'fa-times text-danger',
|
||||
unknown: 'fa-circle text-warning'
|
||||
},
|
||||
hasTips: true,
|
||||
getTips: ({ row, cellValue }) => {
|
||||
const mapper = {
|
||||
'ok': i18n.t('assets.Reachable'),
|
||||
'failed': i18n.t('assets.Unreachable'),
|
||||
'unknown': i18n.t('assets.Unknown')
|
||||
}
|
||||
let tips = mapper[cellValue]
|
||||
if (row['date_verified']) {
|
||||
const datetime = toSafeLocalDateStr(row['date_verified'])
|
||||
tips += '<br> ' + datetime
|
||||
}
|
||||
return tips
|
||||
}
|
||||
},
|
||||
width: '90px',
|
||||
align: 'center'
|
||||
}
|
||||
169
src/components/AppAccountListTable/index.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div>
|
||||
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
|
||||
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/ListTable/index'
|
||||
import { ActionsFormatter, DetailFormatter } from '@/components/TableFormatters'
|
||||
import ShowSecretInfo from './ShowSecretInfo'
|
||||
|
||||
export default {
|
||||
name: 'Detail',
|
||||
components: {
|
||||
ListTable,
|
||||
ShowSecretInfo
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
exportUrl: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.url.replace('/applications/accounts/', '/applications/account-secrets/')
|
||||
}
|
||||
},
|
||||
hasLeftActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
otherActions: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
hasClone: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showViewSecretDialog: false,
|
||||
showUpdateSecretDialog: false,
|
||||
account: {},
|
||||
tableConfig: {
|
||||
url: this.url,
|
||||
columns: [
|
||||
'app_display', 'username', 'category_display',
|
||||
'type_display', 'systemuser', 'actions'
|
||||
],
|
||||
columnsMeta: {
|
||||
app_display: {
|
||||
showOverflowTooltip: true,
|
||||
formatter: DetailFormatter,
|
||||
formatterArgs: {
|
||||
getRoute({ row }) {
|
||||
switch (row['category']) {
|
||||
case 'remote_app':
|
||||
return {
|
||||
name: 'RemoteAppDetail',
|
||||
params: { id: row.app }
|
||||
}
|
||||
case 'db':
|
||||
return {
|
||||
name: 'DatabaseAppDetail',
|
||||
params: { id: row.app }
|
||||
}
|
||||
default:
|
||||
return {
|
||||
name: 'KubernetesAppDetail',
|
||||
params: { id: row.app }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
username: {
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
systemuser: {
|
||||
showOverflowTooltip: true,
|
||||
formatter: DetailFormatter,
|
||||
formatterArgs: {
|
||||
can: this.$hasPerm('assets.view_systemuser'),
|
||||
getTitle({ row }) {
|
||||
return row.systemuser_display
|
||||
},
|
||||
getRoute({ row }) {
|
||||
return {
|
||||
name: 'SystemUserDetail',
|
||||
params: { id: row.systemuser }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
formatter: ActionsFormatter,
|
||||
formatterArgs: {
|
||||
hasUpdate: false, // can set function(row, value)
|
||||
hasDelete: false, // can set function(row, value)
|
||||
hasClone: this.hasClone,
|
||||
moreActionsTitle: this.$t('common.More'),
|
||||
extraActions: [
|
||||
{
|
||||
name: 'View',
|
||||
title: this.$t('common.View'),
|
||||
type: 'primary',
|
||||
can: this.$hasPerm('applications.view_applicationaccountsecret'),
|
||||
callback: function({ row }) {
|
||||
this.account = row
|
||||
this.showViewSecretDialog = true
|
||||
}.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
title: this.$t('common.Update'),
|
||||
can: !this.$store.getters.currentOrgIsRoot,
|
||||
callback: function({ row }) {
|
||||
this.$message.success(this.$tc('applications.updateAccountMsg'))
|
||||
}.bind(this)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: this.hasLeftActions,
|
||||
hasMoreActions: false,
|
||||
hasImport: false,
|
||||
hasExport: this.$hasPerm('applications.view_applicationaccountsecret'),
|
||||
exportOptions: {
|
||||
url: this.exportUrl,
|
||||
mfaVerifyRequired: true
|
||||
},
|
||||
searchConfig: {
|
||||
exclude: ['systemuser', 'app']
|
||||
},
|
||||
hasSearch: true
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
url(iNew) {
|
||||
this.$set(this.tableConfig, 'url', iNew)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.otherActions) {
|
||||
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
|
||||
for (const item of this.otherActions) {
|
||||
actionColumn.formatterArgs.extraActions.push(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onUpdateAuthDone(account) {
|
||||
Object.assign(this.account, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,191 +0,0 @@
|
||||
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'
|
||||
|
||||
export const accountFieldsMeta = (vm) => {
|
||||
const defaultPrivilegedAccounts = ['root', 'administrator']
|
||||
return {
|
||||
assets: {
|
||||
rules: [Required],
|
||||
component: AssetSelect,
|
||||
label: vm.$t('assets.Asset'),
|
||||
el: {
|
||||
multiple: false
|
||||
},
|
||||
hidden: () => {
|
||||
return vm.platform || vm.asset
|
||||
}
|
||||
},
|
||||
template: {
|
||||
component: Select2,
|
||||
rules: [Required],
|
||||
el: {
|
||||
multiple: false,
|
||||
ajax: {
|
||||
url: '/api/v1/accounts/account-templates/',
|
||||
transformOption: (item) => {
|
||||
return { label: item.name, value: item.id }
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return vm.platform || vm.asset || !vm.addTemplate
|
||||
}
|
||||
},
|
||||
on_invalid: {
|
||||
rules: [Required],
|
||||
label: vm.$t('accounts.AccountPolicy'),
|
||||
helpText: vm.$t('accounts.BulkCreateStrategy'),
|
||||
hidden: () => {
|
||||
return vm.platform || vm.asset
|
||||
}
|
||||
},
|
||||
name: {
|
||||
label: vm.$t('common.Name'),
|
||||
rules: [RequiredChange],
|
||||
on: {
|
||||
input: ([value], updateForm) => {
|
||||
if (!vm.usernameChanged) {
|
||||
if (!vm.account?.name) {
|
||||
updateForm({ username: value })
|
||||
}
|
||||
const maybePrivileged = defaultPrivilegedAccounts.includes(value)
|
||||
if (maybePrivileged) {
|
||||
updateForm({ privileged: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
},
|
||||
username: {
|
||||
el: {
|
||||
disabled: !!vm.account?.name
|
||||
},
|
||||
on: {
|
||||
input: ([value], updateForm) => {
|
||||
vm.usernameChanged = true
|
||||
},
|
||||
change: ([value], updateForm) => {
|
||||
const maybePrivileged = defaultPrivilegedAccounts.includes(value)
|
||||
if (maybePrivileged) {
|
||||
updateForm({ privileged: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
},
|
||||
privileged: {
|
||||
label: vm.$t('assets.Privileged'),
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
},
|
||||
su_from: {
|
||||
component: Select2,
|
||||
hidden: (formValue) => {
|
||||
return !vm.asset?.id || !vm.iPlatform.su_enabled
|
||||
},
|
||||
el: {
|
||||
multiple: false,
|
||||
clearable: true,
|
||||
ajax: {
|
||||
url: `/api/v1/accounts/accounts/su-from-accounts/?account=${vm.account?.id || ''}&asset=${vm.asset?.id || ''}`,
|
||||
transformOption: (item) => {
|
||||
return { label: `${item.name}(${item.username})`, value: item.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
su_from_username: {
|
||||
label: vm.$t('assets.UserSwitchFrom'),
|
||||
hidden: (formValue) => {
|
||||
return vm.platform || vm.asset || vm.addTemplate
|
||||
}
|
||||
},
|
||||
password: {
|
||||
label: vm.$t('assets.Password'),
|
||||
component: UpdateToken,
|
||||
hidden: (formValue) => {
|
||||
return formValue.secret_type !== 'password' || vm.addTemplate
|
||||
}
|
||||
},
|
||||
ssh_key: {
|
||||
label: vm.$t('assets.PrivateKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || vm.addTemplate
|
||||
},
|
||||
passphrase: {
|
||||
label: vm.$t('assets.Passphrase'),
|
||||
component: UpdateToken,
|
||||
hidden: (formValue) => formValue.secret_type !== 'ssh_key' || vm.addTemplate
|
||||
},
|
||||
token: {
|
||||
label: vm.$t('assets.Token'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'token' || vm.addTemplate
|
||||
},
|
||||
access_key: {
|
||||
id: 'access_key',
|
||||
label: vm.$t('assets.AccessKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'access_key' || vm.addTemplate
|
||||
},
|
||||
api_key: {
|
||||
id: 'api_key',
|
||||
label: vm.$t('assets.ApiKey'),
|
||||
component: UploadSecret,
|
||||
hidden: (formValue) => formValue.secret_type !== 'api_key' || vm.addTemplate
|
||||
},
|
||||
secret_type: {
|
||||
type: 'radio-group',
|
||||
options: [],
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
},
|
||||
push_now: {
|
||||
helpText: vm.$t('accounts.AccountPush.WindowsPushHelpText'),
|
||||
hidden: (formValue) => {
|
||||
const automation = vm.iPlatform.automation || {}
|
||||
return !automation.push_account_enabled ||
|
||||
!automation.ansible_enabled ||
|
||||
!vm.$hasPerm('accounts.push_account') ||
|
||||
(formValue.secret_type === 'ssh_key' && vm.iPlatform.type.value === 'windows') ||
|
||||
vm.addTemplate
|
||||
}
|
||||
},
|
||||
params: {
|
||||
label: vm.$t('assets.PushParams'),
|
||||
component: AutomationParamsForm,
|
||||
el: {},
|
||||
hidden: (formValue) => {
|
||||
const automation = vm.iPlatform.automation || {}
|
||||
vm.fieldsMeta.params.el.method = vm.iPlatform.automation.push_account_method
|
||||
vm.fieldsMeta.params.el.pushAccountParams = vm.iPlatform.automation.push_account_params
|
||||
return !formValue.push_now ||
|
||||
!automation.push_account_enabled ||
|
||||
!automation.ansible_enabled ||
|
||||
(formValue.secret_type === 'ssh_key' &&
|
||||
vm.iPlatform.type.value === 'windows') ||
|
||||
!vm.$hasPerm('accounts.push_account') ||
|
||||
vm.addTemplate
|
||||
}
|
||||
},
|
||||
is_active: {
|
||||
label: vm.$t('common.IsActive')
|
||||
},
|
||||
comment: {
|
||||
label: vm.$t('common.Comment'),
|
||||
hidden: () => {
|
||||
return vm.addTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
<template>
|
||||
<AutoDataForm
|
||||
v-if="!loading"
|
||||
ref="AutoDataForm"
|
||||
v-bind="$data"
|
||||
@submit="confirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
|
||||
export default {
|
||||
name: 'AccountCreateForm',
|
||||
components: {
|
||||
AutoDataForm
|
||||
},
|
||||
props: {
|
||||
asset: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
platform: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
account: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 默认组件密码加密
|
||||
encryptPassword: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
addTemplate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
usernameChanged: false,
|
||||
iPlatform: {
|
||||
automation: {},
|
||||
su_enabled: false,
|
||||
protocols: [
|
||||
{
|
||||
name: 'ssh',
|
||||
secret_types: ['password', 'ssh_key', 'token', 'access_key', 'api_key']
|
||||
}
|
||||
]
|
||||
},
|
||||
url: '/api/v1/accounts/accounts/',
|
||||
form: Object.assign({ 'on_invalid': 'error' }, this.account || {}),
|
||||
encryptedFields: ['secret'],
|
||||
fields: [
|
||||
[this.$t('assets.Asset'), ['assets']],
|
||||
[this.$t('accounts.AccountTemplate'), ['template']],
|
||||
[this.$t('common.Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username']],
|
||||
[this.$t('assets.Secret'), [
|
||||
'secret_type', 'password', 'ssh_key', 'token',
|
||||
'access_key', 'passphrase', 'api_key'
|
||||
]],
|
||||
[this.$t('common.Other'), ['push_now', 'params', 'on_invalid', 'is_active', 'comment']]
|
||||
],
|
||||
fieldsMeta: accountFieldsMeta(this),
|
||||
hasSaveContinue: false
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
await this.getPlatform()
|
||||
this.setSecretTypeOptions()
|
||||
this.getDefaultAssets()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getDefaultAssets() {
|
||||
const assetId = this.$route.query.asset_id
|
||||
if (assetId && !this.form.name) {
|
||||
this.form.assets = [assetId]
|
||||
}
|
||||
},
|
||||
async getPlatform() {
|
||||
if (this.platform) {
|
||||
this.iPlatform = this.platform
|
||||
}
|
||||
if (!this.asset || !this.asset.platform) {
|
||||
return
|
||||
}
|
||||
const platformId = this.asset.platform.id
|
||||
this.iPlatform = await this.$axios.get(`/api/v1/assets/platforms/${platformId}/`)
|
||||
},
|
||||
setSecretTypeOptions() {
|
||||
const choices = [
|
||||
{
|
||||
label: this.$t('assets.Password'),
|
||||
value: 'password'
|
||||
},
|
||||
{
|
||||
label: this.$t('assets.SSHKey'),
|
||||
value: 'ssh_key'
|
||||
},
|
||||
{
|
||||
label: this.$t('assets.Token'),
|
||||
value: 'token'
|
||||
},
|
||||
{
|
||||
label: this.$t('assets.AccessKey'),
|
||||
value: 'access_key'
|
||||
},
|
||||
{
|
||||
label: this.$t('assets.ApiKey'),
|
||||
value: 'api_key'
|
||||
}
|
||||
]
|
||||
const secretTypes = []
|
||||
this.iPlatform.protocols?.forEach(p => {
|
||||
secretTypes.push(...p['secret_types'])
|
||||
})
|
||||
if (!this.form?.secret_type) {
|
||||
this.form.secret_type = secretTypes[0]
|
||||
}
|
||||
this.fieldsMeta.secret_type.options = choices.filter(item => {
|
||||
return secretTypes.indexOf(item.value) > -1
|
||||
})
|
||||
},
|
||||
confirm(form) {
|
||||
const secretType = form.secret_type || 'password'
|
||||
form.secret = form[secretType]
|
||||
form.secret = this.encryptPassword ? encryptPassword(form.secret) : form.secret
|
||||
|
||||
// 如果不删除会明文显示
|
||||
delete form[secretType]
|
||||
|
||||
if (!form.secret) {
|
||||
delete form['secret']
|
||||
}
|
||||
if (this.account?.name) {
|
||||
this.$emit('edit', form)
|
||||
} else {
|
||||
this.$emit('add', form)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<GenericUpdateFormDialog
|
||||
v-if="visible"
|
||||
:form-setting="formSetting"
|
||||
:selected-rows="selectedRows"
|
||||
:visible="visible"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GenericUpdateFormDialog } from '@/layout/components'
|
||||
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
|
||||
export default {
|
||||
name: 'AccountBulkUpdateDialog',
|
||||
components: {
|
||||
GenericUpdateFormDialog
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedRows: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formSetting: {
|
||||
url: '/api/v1/accounts/accounts/',
|
||||
hasSaveContinue: false,
|
||||
fields: [],
|
||||
fieldsMeta: accountFieldsMeta(this),
|
||||
cleanOtherFormValue: (formValue) => {
|
||||
for (const value of formValue) {
|
||||
Object.keys(value).forEach((item, index, arr) => {
|
||||
if (['ssh_key', 'token', 'access_key', 'api_key', 'password'].includes(item)) {
|
||||
value['secret'] = encryptPassword(value[item])
|
||||
delete value[item]
|
||||
}
|
||||
})
|
||||
}
|
||||
return formValue
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.filterFieldsMeta()
|
||||
},
|
||||
methods: {
|
||||
filterFieldsMeta() {
|
||||
let fields = ['privileged']
|
||||
const fieldsMeta = {}
|
||||
const secretFields = ['password', 'ssh_key', 'passphrase', 'token', 'access_key', 'api_key']
|
||||
const secret_type = this.selectedRows[0].secret_type?.value || 'password'
|
||||
for (const field of secretFields) {
|
||||
if (secret_type === 'ssh_key' && field === 'passphrase') {
|
||||
fields.push('passphrase')
|
||||
this.formSetting.fieldsMeta['passphrase'].hidden = () => false
|
||||
continue
|
||||
}
|
||||
if (secret_type === field) {
|
||||
fields.push(field)
|
||||
this.formSetting.fieldsMeta[field].hidden = () => false
|
||||
continue
|
||||
}
|
||||
delete this.formSetting.fieldsMeta[field]
|
||||
}
|
||||
fields = fields.concat(['is_active', 'comment'])
|
||||
for (const field of fields) {
|
||||
fieldsMeta[field] = this.formSetting.fieldsMeta[field]
|
||||
}
|
||||
this.formSetting.fields = fields
|
||||
this.formSetting.fieldsMeta = fieldsMeta
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,177 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-if="iVisible"
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="title"
|
||||
:visible.sync="iVisible"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<AccountCreateUpdateForm
|
||||
v-if="!loading"
|
||||
ref="form"
|
||||
:account="account"
|
||||
:add-template="addTemplate"
|
||||
:asset="asset"
|
||||
@add="addAccount"
|
||||
@edit="editAccount"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import AccountCreateUpdateForm from '@/components/Apps/AccountCreateUpdateForm/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'CreateAccountDialog',
|
||||
components: {
|
||||
Dialog,
|
||||
AccountCreateUpdateForm
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
addTemplate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
asset: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
account: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('assets.AddAccount')
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
platform: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
protocols() {
|
||||
return this.asset ? this.asset.protocol : []
|
||||
},
|
||||
iVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addAccount(form) {
|
||||
const formValue = Object.assign({}, form)
|
||||
let data, url, iVisible
|
||||
if (this.asset) {
|
||||
data = {
|
||||
asset: this.asset.id,
|
||||
...formValue
|
||||
}
|
||||
iVisible = false
|
||||
url = `/api/v1/accounts/accounts/`
|
||||
} else {
|
||||
iVisible = true
|
||||
data = formValue
|
||||
url = `/api/v1/accounts/accounts/bulk/`
|
||||
if (data.assets.length === 0) {
|
||||
this.$message.error(this.$tc('assets.PleaseSelectAsset'))
|
||||
return
|
||||
}
|
||||
}
|
||||
this.$axios.post(url, data, {
|
||||
disableFlashErrorMsg: iVisible
|
||||
}).then((data) => {
|
||||
this.handleResult(data, null)
|
||||
this.iVisible = iVisible
|
||||
if (!iVisible) {
|
||||
this.$emit('add', true)
|
||||
}
|
||||
}).catch(error => {
|
||||
this.iVisible = true
|
||||
this.handleResult(null, error)
|
||||
})
|
||||
},
|
||||
editAccount(form) {
|
||||
const data = { ...form }
|
||||
this.$axios.patch(`/api/v1/accounts/accounts/${this.account.id}/`, data).then(() => {
|
||||
this.iVisible = false
|
||||
this.$emit('add', true)
|
||||
this.$message.success(this.$tc('common.updateSuccessMsg'))
|
||||
}).catch(error => this.setFieldError(error))
|
||||
},
|
||||
handleResult(resp, error) {
|
||||
let bulkCreate = !this.asset
|
||||
if (error && !Array.isArray(error?.response?.data)) {
|
||||
bulkCreate = false
|
||||
}
|
||||
if (resp && !Array.isArray(resp)) {
|
||||
bulkCreate = false
|
||||
}
|
||||
if (!bulkCreate) {
|
||||
if (!error) {
|
||||
this.$message.success(this.$tc('common.createSuccessMsg'))
|
||||
} else {
|
||||
this.setFieldError(error)
|
||||
}
|
||||
} else {
|
||||
let result
|
||||
if (error) {
|
||||
result = error.response.data
|
||||
} else {
|
||||
result = resp
|
||||
}
|
||||
this.$emit('bulk-create-done', result)
|
||||
}
|
||||
},
|
||||
setFieldError(error) {
|
||||
const response = error.response
|
||||
const data = response.data
|
||||
const refsAutoDataForm = this.$refs.form.$refs.AutoDataForm
|
||||
if (response.status === 400) {
|
||||
for (const key of Object.keys(data)) {
|
||||
let err = ''
|
||||
let current = key
|
||||
let errorTips = data[current]
|
||||
if (errorTips instanceof Array) {
|
||||
errorTips = _.filter(errorTips, (item) => Object.keys(item).length > 0)
|
||||
for (const i of errorTips) {
|
||||
if (i instanceof Object) {
|
||||
err += i?.port?.join(',')
|
||||
} else {
|
||||
err += errorTips
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = errorTips
|
||||
}
|
||||
if (current === 'secret') {
|
||||
current = refsAutoDataForm.form.secret_type?.value || key
|
||||
}
|
||||
refsAutoDataForm.setFieldError(current, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,502 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<ListTable ref="ListTable" :header-actions="headerActions" :table-config="tableConfig" />
|
||||
<ViewSecret
|
||||
v-if="showViewSecretDialog"
|
||||
:account="account"
|
||||
:url="secretUrl"
|
||||
:visible.sync="showViewSecretDialog"
|
||||
/>
|
||||
<UpdateSecretInfo
|
||||
v-if="showUpdateSecretDialog"
|
||||
:account="account"
|
||||
:visible.sync="showUpdateSecretDialog"
|
||||
@updateAuthDone="onUpdateAuthDone"
|
||||
/>
|
||||
<AccountCreateUpdate
|
||||
v-if="showAddDialog"
|
||||
:account="account"
|
||||
:asset="iAsset"
|
||||
:title="accountCreateUpdateTitle"
|
||||
:visible.sync="showAddDialog"
|
||||
@add="addAccountSuccess"
|
||||
@bulk-create-done="showBulkCreateResult($event)"
|
||||
/>
|
||||
<AccountCreateUpdate
|
||||
v-if="showAddTemplateDialog"
|
||||
:account="account"
|
||||
:add-template="true"
|
||||
:asset="iAsset"
|
||||
:title="accountCreateUpdateTitle"
|
||||
:visible.sync="showAddTemplateDialog"
|
||||
@add="addAccountSuccess"
|
||||
@bulk-create-done="showBulkCreateResult($event)"
|
||||
/>
|
||||
<ResultDialog
|
||||
v-if="showResultDialog"
|
||||
:result="createAccountResults"
|
||||
:visible.sync="showResultDialog"
|
||||
/>
|
||||
<AccountBulkUpdateDialog
|
||||
v-if="updateSelectedDialogSetting.visible"
|
||||
:visible.sync="updateSelectedDialogSetting.visible"
|
||||
v-bind="updateSelectedDialogSetting"
|
||||
@update="handleAccountBulkUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/Table/ListTable/index.vue'
|
||||
import { ActionsFormatter } from '@/components/Table/TableFormatters'
|
||||
import ViewSecret from './ViewSecret.vue'
|
||||
import UpdateSecretInfo from './UpdateSecretInfo.vue'
|
||||
import AccountCreateUpdate from './AccountCreateUpdate.vue'
|
||||
import { connectivityMeta } from './const'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
import ResultDialog from './BulkCreateResultDialog.vue'
|
||||
import AccountBulkUpdateDialog from '@/components/Apps/AccountListTable/AccountBulkUpdateDialog.vue'
|
||||
|
||||
export default {
|
||||
name: 'AccountListTable',
|
||||
components: {
|
||||
AccountBulkUpdateDialog,
|
||||
ResultDialog,
|
||||
ListTable,
|
||||
UpdateSecretInfo,
|
||||
ViewSecret,
|
||||
AccountCreateUpdate
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
exportUrl: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.url.replace('/accounts/accounts/', '/accounts/account-secrets/')
|
||||
}
|
||||
},
|
||||
hasLeftActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
otherActions: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
hasClone: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
asset: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
hasExport: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasImport: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hasDeleteAction: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
columnsMeta: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
}
|
||||
},
|
||||
columnsDefault: {
|
||||
type: Array,
|
||||
default: () => ([
|
||||
'name', 'username', 'asset', 'privileged',
|
||||
'secret_type', 'is_active', 'date_updated'
|
||||
])
|
||||
},
|
||||
headerExtraActions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
extraQuery: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
return {
|
||||
showViewSecretDialog: false,
|
||||
showUpdateSecretDialog: false,
|
||||
showResultDialog: false,
|
||||
showAddDialog: false,
|
||||
showAddTemplateDialog: false,
|
||||
createAccountResults: [],
|
||||
accountCreateUpdateTitle: this.$t('assets.AddAccount'),
|
||||
iAsset: this.asset,
|
||||
account: {},
|
||||
secretUrl: '',
|
||||
tableConfig: {
|
||||
url: this.url,
|
||||
permissions: {
|
||||
app: 'assets',
|
||||
resource: 'account'
|
||||
},
|
||||
extraQuery: this.extraQuery,
|
||||
columnsExclude: ['spec_info'],
|
||||
columnsShow: {
|
||||
min: ['name', 'username', 'actions'],
|
||||
default: this.columnsDefault
|
||||
},
|
||||
columnsMeta: {
|
||||
name: {
|
||||
formatter: function(row) {
|
||||
const to = {
|
||||
name: 'AssetAccountDetail',
|
||||
params: { id: row.id }
|
||||
}
|
||||
if (vm.$hasPerm('accounts.view_account')) {
|
||||
return <router-link to={to}>{row.name}</router-link>
|
||||
} else {
|
||||
return <span>{row.name}</span>
|
||||
}
|
||||
}
|
||||
},
|
||||
asset: {
|
||||
label: this.$t('assets.Asset'),
|
||||
formatter: function(row) {
|
||||
const to = {
|
||||
name: 'AssetDetail',
|
||||
params: { id: row.asset.id }
|
||||
}
|
||||
if (vm.$hasPerm('assets.view_asset')) {
|
||||
return <router-link to={to}>{row.asset.name}</router-link>
|
||||
} else {
|
||||
return <span>{row.asset.name}</span>
|
||||
}
|
||||
}
|
||||
},
|
||||
secret_type: {
|
||||
width: '100px',
|
||||
formatter: function(row) {
|
||||
return row.secret_type.label
|
||||
}
|
||||
},
|
||||
source: {
|
||||
formatter: function(row) {
|
||||
return row.source.label
|
||||
}
|
||||
},
|
||||
has_secret: {
|
||||
width: '100px',
|
||||
formatterArgs: {
|
||||
showFalse: false
|
||||
}
|
||||
},
|
||||
privileged: {
|
||||
label: this.$t('assets.Privileged'),
|
||||
width: '120px',
|
||||
formatterArgs: {
|
||||
showText: false,
|
||||
showFalse: false
|
||||
}
|
||||
},
|
||||
connectivity: connectivityMeta,
|
||||
actions: {
|
||||
formatter: ActionsFormatter,
|
||||
formatterArgs: {
|
||||
hasUpdate: false, // can set function(row, value)
|
||||
hasDelete: false, // can set function(row, value)
|
||||
hasClone: this.hasClone,
|
||||
moreActionsTitle: this.$t('common.More'),
|
||||
extraActions: [
|
||||
{
|
||||
name: 'View',
|
||||
title: this.$t('common.View'),
|
||||
can: this.$hasPerm('accounts.view_accountsecret'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
// debugger
|
||||
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
|
||||
vm.account = row
|
||||
vm.showViewSecretDialog = false
|
||||
setTimeout(() => {
|
||||
vm.showViewSecretDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ClearSecret',
|
||||
title: this.$t('common.ClearSecret'),
|
||||
can: this.$hasPerm('accounts.change_account'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
this.$axios.patch(
|
||||
`/api/v1/accounts/accounts/clear-secret/`,
|
||||
{ account_ids: [row.id] }
|
||||
).then(() => {
|
||||
this.$message.success(this.$tc('common.ClearSuccessMsg'))
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
title: this.$t('accounts.Test'),
|
||||
can: ({ row }) =>
|
||||
!this.$store.getters.currentOrgIsRoot &&
|
||||
this.$hasPerm('accounts.change_account') &&
|
||||
row.asset['auto_config'].ansible_enabled &&
|
||||
row.asset['auto_config'].ping_enabled,
|
||||
callback: ({ row }) => {
|
||||
this.$axios.post(
|
||||
`/api/v1/accounts/accounts/tasks/`,
|
||||
{ action: 'verify', accounts: [row.id] }
|
||||
).then(res => {
|
||||
openTaskPage(res['task'])
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
title: this.$t('common.Update'),
|
||||
can: this.$hasPerm('accounts.change_account') && !this.$store.getters.currentOrgIsRoot,
|
||||
callback: ({ row }) => {
|
||||
const data = {
|
||||
...this.asset,
|
||||
...row.asset
|
||||
}
|
||||
vm.account = row
|
||||
vm.iAsset = data
|
||||
vm.showAddDialog = false
|
||||
vm.accountCreateUpdateTitle = this.$t('assets.UpdateAccount')
|
||||
setTimeout(() => {
|
||||
vm.showAddDialog = true
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
...this.columnsMeta
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLabelSearch: true,
|
||||
hasLeftActions: this.hasLeftActions,
|
||||
hasMoreActions: true,
|
||||
hasCreate: false,
|
||||
hasImport: this.hasImport,
|
||||
hasExport: this.hasExport && this.$hasPerm('accounts.view_accountsecret'),
|
||||
handleImportClick: ({ selectedRows }) => {
|
||||
this.$eventBus.$emit('showImportDialog', {
|
||||
selectedRows,
|
||||
url: '/api/v1/accounts/accounts/',
|
||||
name: this?.name
|
||||
})
|
||||
},
|
||||
exportOptions: {
|
||||
url: this.exportUrl,
|
||||
mfaVerifyRequired: true,
|
||||
tips: this.$t('accounts.AccountExportTips')
|
||||
},
|
||||
importOptions: {
|
||||
canImportCreate: this.$hasPerm('accounts.add_account'),
|
||||
canImportUpdate: this.$hasPerm('accounts.change_account')
|
||||
},
|
||||
extraActions: [
|
||||
{
|
||||
name: 'add',
|
||||
title: this.$t('common.Add'),
|
||||
type: 'primary',
|
||||
can: () => {
|
||||
return vm.$hasPerm('accounts.add_account') && !this.$store.getters.currentOrgIsRoot
|
||||
},
|
||||
callback: async() => {
|
||||
await this.getAssetDetail()
|
||||
setTimeout(() => {
|
||||
vm.iAsset = this.asset
|
||||
vm.account = {}
|
||||
vm.accountCreateUpdateTitle = this.$t('assets.AddAccount')
|
||||
vm.showAddDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'add-template',
|
||||
title: this.$t('common.TemplateAdd'),
|
||||
has: !(this.platform || this.asset),
|
||||
can: () => {
|
||||
return vm.$hasPerm('accounts.add_account') && !this.$store.getters.currentOrgIsRoot
|
||||
},
|
||||
callback: async() => {
|
||||
await this.getAssetDetail()
|
||||
setTimeout(() => {
|
||||
vm.iAsset = this.asset
|
||||
vm.account = {}
|
||||
vm.accountCreateUpdateTitle = this.$t('assets.AddAccount')
|
||||
vm.showAddTemplateDialog = true
|
||||
})
|
||||
}
|
||||
},
|
||||
...this.headerExtraActions
|
||||
],
|
||||
extraMoreActions: [
|
||||
{
|
||||
name: 'BulkVerify',
|
||||
title: this.$t('accounts.BulkVerify'),
|
||||
type: 'primary',
|
||||
fa: 'fa-link',
|
||||
can: ({ selectedRows }) => {
|
||||
return selectedRows.length > 0 &&
|
||||
['clickhouse', 'redis', 'website', 'chatgpt'].indexOf(selectedRows[0].asset.type.value) === -1 &&
|
||||
!this.$store.getters.currentOrgIsRoot
|
||||
},
|
||||
callback: function({ selectedRows }) {
|
||||
const ids = selectedRows.map(v => {
|
||||
return v.id
|
||||
})
|
||||
this.$axios.post(
|
||||
'/api/v1/accounts/accounts/tasks/',
|
||||
{ action: 'verify', accounts: ids }).then(res => {
|
||||
openTaskPage(res['task'])
|
||||
}).catch(err => {
|
||||
this.$message.error(this.$tc('common.bulkVerifyErrorMsg' + ' ' + err))
|
||||
})
|
||||
}.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'ClearSecrets',
|
||||
title: this.$t('common.ClearSecret'),
|
||||
type: 'primary',
|
||||
fa: 'clean',
|
||||
can: ({ selectedRows }) => {
|
||||
return selectedRows.length > 0 && vm.$hasPerm('accounts.change_account')
|
||||
},
|
||||
callback: function({ selectedRows }) {
|
||||
const ids = selectedRows.map(v => {
|
||||
return v.id
|
||||
})
|
||||
this.$axios.patch(
|
||||
'/api/v1/accounts/accounts/clear-secret/',
|
||||
{ account_ids: ids }).then(() => {
|
||||
this.$message.success(this.$tc('common.ClearSuccessMsg'))
|
||||
}).catch(err => {
|
||||
this.$message.error(this.$tc('common.bulkClearErrorMsg' + ' ' + err))
|
||||
})
|
||||
}.bind(this)
|
||||
},
|
||||
{
|
||||
name: 'actionUpdateSelected',
|
||||
title: this.$t('accounts.AccountBatchUpdate'),
|
||||
fa: 'batch-update',
|
||||
can: ({ selectedRows }) => {
|
||||
return selectedRows.length > 0 &&
|
||||
!this.$store.getters.currentOrgIsRoot &&
|
||||
vm.$hasPerm('accounts.change_account') &&
|
||||
selectedRows.every(i => i.secret_type.value === selectedRows[0].secret_type.value)
|
||||
},
|
||||
callback: ({ selectedRows }) => {
|
||||
vm.updateSelectedDialogSetting.selectedRows = selectedRows
|
||||
vm.updateSelectedDialogSetting.visible = true
|
||||
}
|
||||
}
|
||||
],
|
||||
canBulkDelete: vm.$hasPerm('accounts.delete_account'),
|
||||
searchConfig: {
|
||||
getUrlQuery: false,
|
||||
exclude: ['asset']
|
||||
},
|
||||
hasSearch: true
|
||||
},
|
||||
updateSelectedDialogSetting: {
|
||||
visible: false,
|
||||
selectedRows: []
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
url(iNew) {
|
||||
this.$set(this.tableConfig, 'url', iNew)
|
||||
this.$set(this.headerActions.exportOptions, 'url', iNew.replace(/(.*)accounts/, '$1account-secrets'))
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.columns.length > 0) {
|
||||
this.tableConfig.columns = this.columns
|
||||
}
|
||||
if (this.otherActions) {
|
||||
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
|
||||
for (const item of this.otherActions) {
|
||||
actionColumn.formatterArgs.extraActions.push(item)
|
||||
}
|
||||
}
|
||||
if (this.hasDeleteAction) {
|
||||
this.tableConfig.columnsMeta.actions.formatterArgs.extraActions.push(
|
||||
{
|
||||
name: 'Delete',
|
||||
title: this.$t('common.Delete'),
|
||||
can: this.$hasPerm('accounts.delete_account'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
const msg = this.$t('accounts.AccountDeleteConfirmMsg')
|
||||
this.$confirm(msg, this.$tc('common.Info'), {
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
beforeClose: async(action, instance, done) => {
|
||||
if (action !== 'confirm') return done()
|
||||
this.$axios.delete(`/api/v1/accounts/accounts/${row.id}/`).then(() => {
|
||||
done()
|
||||
this.$refs.ListTable.reloadTable()
|
||||
this.$message.success(this.$tc('common.deleteSuccessMsg'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onUpdateAuthDone(account) {
|
||||
Object.assign(this.account, account)
|
||||
},
|
||||
addAccountSuccess() {
|
||||
this.$refs.ListTable.reloadTable()
|
||||
},
|
||||
async getAssetDetail() {
|
||||
const { query: { asset }} = this.$route
|
||||
if (asset) {
|
||||
this.iAsset = await this.$axios.get(`/api/v1/assets/assets/${asset}/`)
|
||||
}
|
||||
},
|
||||
refresh() {
|
||||
this.$refs.ListTable.reloadTable()
|
||||
},
|
||||
showBulkCreateResult(results) {
|
||||
this.showResultDialog = false
|
||||
this.createAccountResults = results
|
||||
setTimeout(() => {
|
||||
this.showResultDialog = true
|
||||
}, 100)
|
||||
},
|
||||
handleAccountBulkUpdate() {
|
||||
this.updateSelectedDialogSetting.visible = false
|
||||
this.$refs.ListTable.reloadTable()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.cell a {
|
||||
color: var(--color-info);
|
||||
}
|
||||
</style>
|
||||
@@ -1,120 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:show-cancel="false"
|
||||
:title="title"
|
||||
v-bind="$attrs"
|
||||
@confirm="closeDialog"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-alert style="margin-bottom: 10px" type="success">
|
||||
<span v-for="item of summary" :key="item.key"><b>{{ item.label }}</b>: {{ item.value }} </span>
|
||||
</el-alert>
|
||||
<DataTable :config="config" />
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import DataTable from '@/components/Table/DataTable/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'ResultDialog',
|
||||
components: {
|
||||
DataTable,
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
result: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const errorProp = this.$t('common.Error')
|
||||
const stateMap = {
|
||||
'created': this.$tc('common.Created'),
|
||||
'updated': this.$tc('common.Updated'),
|
||||
'skipped': this.$tc('common.Skipped')
|
||||
}
|
||||
const stateClsMap = {
|
||||
'created': 'color-primary',
|
||||
'updated': 'color-success',
|
||||
'skipped': 'color-default'
|
||||
}
|
||||
return {
|
||||
title: this.$t('accounts.AddAccountResult'),
|
||||
config: {
|
||||
columns: [
|
||||
{
|
||||
prop: 'asset',
|
||||
label: this.$t('assets.Asset')
|
||||
},
|
||||
{
|
||||
prop: 'state',
|
||||
label: this.$t('common.Status'),
|
||||
width: '200px',
|
||||
formatter: (row) => {
|
||||
if (row.error) {
|
||||
return <span class='color-error'>{ errorProp }: { row.error }</span>
|
||||
} else if (row.state) {
|
||||
const colorCls = stateClsMap[row.state]
|
||||
const state = stateMap[row.state]
|
||||
return <span class={ colorCls }>{ state }</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
totalData: this.result
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
summary() {
|
||||
const labels = {
|
||||
total: this.$tc('common.Total'),
|
||||
created: this.$tc('common.Created'),
|
||||
updated: this.$tc('common.Updated'),
|
||||
skipped: this.$tc('common.Skipped'),
|
||||
error: this.$tc('common.Error')
|
||||
}
|
||||
const grouped = _.groupBy(this.result, 'state')
|
||||
const groupedLength = _.mapValues(grouped, 'length')
|
||||
groupedLength['total'] = this.result.length
|
||||
return _.map(groupedLength, (value, key) => {
|
||||
return {
|
||||
label: labels[key],
|
||||
value: value,
|
||||
key: key
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.color-error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.color-primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.color-success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.color-default {
|
||||
}
|
||||
|
||||
::v-deep .el-data-table .el-table .el-table__row > td > div > span {
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<GenericListTableDialog :visible.sync="iVisible" v-bind="config" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GenericListTableDialog } from '@/layout/components'
|
||||
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GenericListTableDialog
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config: {
|
||||
title: this.$t('accounts.HistoryPassword'),
|
||||
visible: false,
|
||||
width: '60%',
|
||||
tableConfig: {
|
||||
id: 'history_date',
|
||||
url: `/api/v1/accounts/account-secrets/${this.account.id}/histories/`,
|
||||
columns: ['secret', 'version', 'history_date'],
|
||||
columnsMeta: {
|
||||
secret: {
|
||||
label: this.$t('assets.Password'),
|
||||
formatter: ShowKeyCopyFormatter,
|
||||
formatterArgs: {
|
||||
hasDownload: false,
|
||||
name: this.account.name
|
||||
}
|
||||
},
|
||||
history_date: {
|
||||
label: this.$t('accounts.HistoryDate')
|
||||
},
|
||||
secret_type: {
|
||||
width: '200px'
|
||||
},
|
||||
version: {
|
||||
width: '100px'
|
||||
},
|
||||
actions: {
|
||||
has: false
|
||||
}
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasSearch: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,112 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:visible.sync="show"
|
||||
:width="'50'"
|
||||
v-bind="$attrs"
|
||||
@confirm="accountConfirmHandle"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'RemoveAccount',
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
accounts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
mfaDialogVisible: true
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
mounted() {
|
||||
const url = `/api/v1/accounts/accounts/tasks/`
|
||||
this.$axios.post(
|
||||
url, { disableFlashErrorMsg: true, action: 'remove' }
|
||||
).then(resp => {
|
||||
this.$axios.post(
|
||||
`/api/v1/accounts/accounts/tasks/`,
|
||||
{
|
||||
action: 'remove',
|
||||
gather_accounts: this.accounts.map(account => account.id)
|
||||
}
|
||||
).then(res => {
|
||||
openTaskPage(res['task'])
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
accountConfirmHandle() {
|
||||
this.show = false
|
||||
this.mfaDialogVisible = false
|
||||
},
|
||||
exit() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item-textarea > > > .el-textarea__inner {
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
> > > .el-form-item__label {
|
||||
padding-right: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
> > > .el-form-item__content {
|
||||
line-height: 30px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.title {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,214 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:title="title"
|
||||
:visible.sync="showSecret"
|
||||
:width="'50'"
|
||||
v-bind="$attrs"
|
||||
@confirm="accountConfirmHandle"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-form :model="secretInfo" class="password-form" label-position="right" label-width="100px">
|
||||
<el-form-item :label="$tc('assets.Name')">
|
||||
<span>{{ account['name'] }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('assets.Username')">
|
||||
<span>{{ account['username'] }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="secretTypeLabel">
|
||||
<ShowKeyCopyFormatter
|
||||
:cell-value="secretInfo.secret"
|
||||
:col="{ formatterArgs: {
|
||||
name: account['name'],
|
||||
secretType: secretType || ''
|
||||
}}"
|
||||
@input="onShowKeyCopyFormatterChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="secretType === 'ssh_key'" :label="$tc('assets.sshKeyFingerprint')">
|
||||
<span>{{ sshKeyFingerprint }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('common.DateCreated')">
|
||||
<span>{{ account['date_created'] | date }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('common.DateUpdated')">
|
||||
<span>{{ account['date_updated'] | date }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showPasswordRecord" v-perms="'accounts.view_accountsecret'" :label="$tc('accounts.PasswordRecord')">
|
||||
<el-link
|
||||
:underline="false"
|
||||
type="success"
|
||||
@click="showHistoryDialog"
|
||||
>
|
||||
<span style="padding-right: 30px">
|
||||
{{ versions }}
|
||||
</span>
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</Dialog>
|
||||
<PasswordHistoryDialog
|
||||
v-if="showPasswordHistoryDialog"
|
||||
:account="account"
|
||||
:visible.sync="showPasswordHistoryDialog"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
|
||||
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
|
||||
export default {
|
||||
name: 'ShowSecretInfo',
|
||||
components: {
|
||||
Dialog,
|
||||
PasswordHistoryDialog,
|
||||
ShowKeyCopyFormatter
|
||||
},
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'account'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$tc('assets.AccountDetail')
|
||||
}
|
||||
},
|
||||
showPasswordRecord: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modifiedSecret: '',
|
||||
secretInfo: {},
|
||||
versions: '-',
|
||||
showSecret: false,
|
||||
mfaDialogVisible: true,
|
||||
sshKeyFingerprint: '-',
|
||||
historyCount: 0,
|
||||
showPasswordHistoryDialog: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
secretTypeLabel() {
|
||||
return this.account['secret_type'].label || 'Password'
|
||||
},
|
||||
secretType() {
|
||||
return this.account['secret_type'].value
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.showPasswordRecord) {
|
||||
const url = `/api/v1/accounts/account-secrets/${this.account.id}/histories/?limit=1`
|
||||
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
|
||||
this.versions = resp.count
|
||||
this.showSecretDialog()
|
||||
})
|
||||
} else {
|
||||
this.showSecretDialog()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
accountConfirmHandle() {
|
||||
this.modifiedSecret && this.onChangeSecretSubmit()
|
||||
this.showSecret = false
|
||||
this.mfaDialogVisible = false
|
||||
},
|
||||
onChangeSecretSubmit() {
|
||||
const params = {
|
||||
name: this.secretInfo.name,
|
||||
secret: encryptPassword(this.modifiedSecret)
|
||||
}
|
||||
const url = this.type === 'account' ? `/api/v1/accounts/accounts` : `/api/v1/accounts/account-templates`
|
||||
this.$axios.patch(`${url}/${this.account.id}/`, params).then(() => {
|
||||
this.$message.success(this.$tc('common.updateSuccessMsg'))
|
||||
})
|
||||
},
|
||||
showSecretDialog() {
|
||||
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then((res) => {
|
||||
this.secretInfo = res
|
||||
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
|
||||
this.showSecret = true
|
||||
})
|
||||
},
|
||||
exit() {
|
||||
this.$emit('update:visible', false)
|
||||
},
|
||||
showHistoryDialog() {
|
||||
this.showPasswordHistoryDialog = true
|
||||
},
|
||||
onShowKeyCopyFormatterChange(value) {
|
||||
if (value === this.secretInfo.secret) return
|
||||
this.modifiedSecret = value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item-textarea >>> .el-textarea__inner {
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
>>> .el-form-item__label {
|
||||
padding-right: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
>>> .el-form-item__content {
|
||||
line-height: 30px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.title {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
import i18n from '@/i18n/i18n'
|
||||
import { ChoicesFormatter } from '@/components/Table/TableFormatters'
|
||||
|
||||
export const connectivityMeta = {
|
||||
label: i18n.t('assets.Connectivity'),
|
||||
formatter: ChoicesFormatter,
|
||||
formatterArgs: {
|
||||
faChoices: {
|
||||
'-': '',
|
||||
ok: 'fa-check-circle',
|
||||
err: 'fa-times-circle'
|
||||
},
|
||||
classChoices: {
|
||||
ok: 'text-primary',
|
||||
err: 'text-danger'
|
||||
},
|
||||
getText({ cellValue }) {
|
||||
if (cellValue?.value === '-' || cellValue?.value === 'unknown') {
|
||||
return '-'
|
||||
} else {
|
||||
return cellValue?.label
|
||||
}
|
||||
}
|
||||
},
|
||||
width: '100px'
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:close-on-click-modal="false"
|
||||
:loading-status="!isLoaded"
|
||||
:title="$tc('assets.Assets')"
|
||||
custom-class="asset-select-dialog"
|
||||
top="2vh"
|
||||
v-bind="$attrs"
|
||||
width="1000px"
|
||||
@cancel="handleCancel"
|
||||
@close="handleClose"
|
||||
@confirm="handleConfirm"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<AssetTreeTable
|
||||
ref="ListPage"
|
||||
:header-actions="headerActions"
|
||||
:node-url="baseNodeUrl"
|
||||
:table-config="tableConfig"
|
||||
:tree-url="`${baseNodeUrl}children/tree/`"
|
||||
:url="baseUrl"
|
||||
:tree-setting="treeSetting"
|
||||
class="tree-table"
|
||||
v-bind="$attrs"
|
||||
@loaded="handleTableLoaded"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AssetTreeTable from '@/components/Apps/AssetTreeTable/index.vue'
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
|
||||
export default {
|
||||
componentName: 'AssetSelectDialog',
|
||||
components: { AssetTreeTable, Dialog },
|
||||
props: {
|
||||
baseUrl: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/assets/'
|
||||
},
|
||||
baseNodeUrl: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/nodes/'
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
canSelect: {
|
||||
type: Function,
|
||||
default(row, index) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: [Boolean, Function],
|
||||
default: false
|
||||
},
|
||||
treeSetting: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
return {
|
||||
isLoaded: false,
|
||||
dialogVisible: false,
|
||||
rowSelected: _.cloneDeep(this.value) || [],
|
||||
rowsAdd: [],
|
||||
tableConfig: {
|
||||
url: this.baseUrl,
|
||||
hasTree: true,
|
||||
canSelect: this.canSelect,
|
||||
columns: [
|
||||
{
|
||||
prop: 'name',
|
||||
label: this.$t('assets.Name'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
prop: 'address',
|
||||
label: this.$t('assets.ipDomain'),
|
||||
sortable: 'custom'
|
||||
},
|
||||
{
|
||||
prop: 'platform',
|
||||
label: this.$t('assets.Platform'),
|
||||
sortable: true,
|
||||
formatter: function(row) {
|
||||
return row.platform.name
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'actions',
|
||||
has: false
|
||||
}
|
||||
],
|
||||
listeners: {
|
||||
'toggle-row-selection': (isSelected, row) => {
|
||||
if (isSelected) {
|
||||
vm.addRowToSelect(row)
|
||||
} else {
|
||||
vm.removeRowFromSelect(row)
|
||||
}
|
||||
}
|
||||
},
|
||||
theRowDefaultIsSelected: (row) => {
|
||||
return this.value.indexOf(row.id) > -1
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasRightActions: false,
|
||||
hasLabelSearch: true,
|
||||
searchConfig: {
|
||||
getUrlQuery: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$eventBus.$emit('treeComponentKey')
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$emit('confirm', this.rowSelected, this.rowsAdd)
|
||||
if (this.rowSelected.length > 0) {
|
||||
this.handleClose()
|
||||
}
|
||||
},
|
||||
handleCancel() {
|
||||
this.$emit('cancel')
|
||||
this.handleClose()
|
||||
},
|
||||
addRowToSelect(row) {
|
||||
const selectValueIndex = this.rowSelected.indexOf(row.id)
|
||||
if (selectValueIndex === -1) {
|
||||
this.rowSelected.push(row.id)
|
||||
this.rowsAdd.push(row)
|
||||
}
|
||||
},
|
||||
removeRowFromSelect(row) {
|
||||
const selectValueIndex = this.rowSelected.indexOf(row.id)
|
||||
if (selectValueIndex > -1) {
|
||||
this.rowSelected.splice(selectValueIndex, 1)
|
||||
}
|
||||
},
|
||||
handleTableLoaded() {
|
||||
this.isLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page ::v-deep .page-heading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog__wrapper ::v-deep .el-dialog__body {
|
||||
padding: 0 0 0 3px;
|
||||
|
||||
.tree-table {
|
||||
.search {
|
||||
}
|
||||
|
||||
.left {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.right {
|
||||
min-height: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mini {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.transition-box {
|
||||
padding: 10px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page ::v-deep .treebox .ztree {
|
||||
|
||||
}
|
||||
|
||||
.asset-select-dialog ::v-deep .el-icon-circle-check {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,158 +0,0 @@
|
||||
<template>
|
||||
<div class="asset-select-formatter">
|
||||
<Select2
|
||||
ref="select2"
|
||||
v-model="select2Config.value"
|
||||
v-bind="select2Config"
|
||||
@input="onInputChange"
|
||||
v-on="$listeners"
|
||||
@focus.stop="handleFocus"
|
||||
/>
|
||||
<AssetSelectDialog
|
||||
v-if="dialogVisible"
|
||||
ref="dialog"
|
||||
:base-node-url="baseNodeUrl"
|
||||
:base-url="baseUrl"
|
||||
:tree-setting="treeSetting"
|
||||
:tree-url-query="treeUrlQuery"
|
||||
:value="value"
|
||||
:visible.sync="dialogVisible"
|
||||
v-bind="$attrs"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleConfirm"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Select2 from '@/components/Form/FormFields/Select2.vue'
|
||||
import AssetSelectDialog from './dialog.vue'
|
||||
|
||||
export default {
|
||||
componentName: 'AssetSelect',
|
||||
components: { AssetSelectDialog, Select2 },
|
||||
props: {
|
||||
baseUrl: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/assets/'
|
||||
},
|
||||
baseNodeUrl: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/nodes/'
|
||||
},
|
||||
treeUrlQuery: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
treeSetting: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const iValue = []
|
||||
for (const item of this.value) {
|
||||
if (typeof item === 'object') {
|
||||
iValue.push(item.id)
|
||||
} else {
|
||||
iValue.push(item)
|
||||
}
|
||||
}
|
||||
return {
|
||||
dialogVisible: false,
|
||||
initialValue: _.cloneDeep(iValue),
|
||||
select2Config: {
|
||||
value: iValue,
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
ajax: {
|
||||
url: this.baseUrl,
|
||||
transformOption: (item) => {
|
||||
return { label: item.name + '(' + item.address + ')', value: item.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFocus() {
|
||||
this.$refs.select2.selectRef.blur()
|
||||
this.dialogVisible = true
|
||||
},
|
||||
handleConfirm(valueSelected, rowsAdd) {
|
||||
if (valueSelected === undefined) {
|
||||
return
|
||||
}
|
||||
this.$refs.select2.iValue = valueSelected
|
||||
this.addRowsToSelect(rowsAdd)
|
||||
this.onInputChange(valueSelected)
|
||||
this.dialogVisible = false
|
||||
},
|
||||
handleCancel() {
|
||||
this.dialogVisible = false
|
||||
},
|
||||
onInputChange(val) {
|
||||
this.$emit('change', val)
|
||||
},
|
||||
addToSelect(options, row) {
|
||||
const selectOptionsHas = options.find(item => item.value === row.id)
|
||||
// 如果select2的options中没有,那么可能无法显示正常的值
|
||||
if (selectOptionsHas === undefined) {
|
||||
const option = {
|
||||
label: `${row.name}(${row.address})`,
|
||||
value: row.id
|
||||
}
|
||||
options.push(option)
|
||||
}
|
||||
},
|
||||
addRowsToSelect(rows) {
|
||||
const outSelectOptions = this.$refs.select2.options
|
||||
for (const row of rows) {
|
||||
this.addToSelect(outSelectOptions, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page ::v-deep .page-heading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog__wrapper ::v-deep .el-dialog__body {
|
||||
padding: 0 0 0 3px;
|
||||
|
||||
.tree-table {
|
||||
.left {
|
||||
padding: 5px;
|
||||
|
||||
.ztree {
|
||||
min-height: 500px;
|
||||
height: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mini {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.transition-box {
|
||||
padding: 10px 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page ::v-deep .treebox {
|
||||
height: inherit !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<TreeTable
|
||||
ref="TreeList"
|
||||
:active-menu.sync="treeTableConfig.activeMenu"
|
||||
:table-config="tableConfig"
|
||||
:tree-tab-config="treeTableConfig"
|
||||
component="TabTree"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template #table>
|
||||
<slot name="table" />
|
||||
</template>
|
||||
<div slot="rMenu" slot-scope="{data}">
|
||||
<slot :data="data" name="rMenu" />
|
||||
</div>
|
||||
</TreeTable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TreeTable from '../../Table/TreeTable/index.vue'
|
||||
import { setRouterQuery, setUrlParam } from '@/utils/common'
|
||||
import $ from '@/utils/jquery-vendor'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeTable
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/assets/'
|
||||
},
|
||||
typeUrl: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/nodes/category/tree/'
|
||||
},
|
||||
nodeUrl: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/nodes/'
|
||||
},
|
||||
treeUrl: {
|
||||
type: String,
|
||||
default: '/api/v1/assets/nodes/children/tree/'
|
||||
},
|
||||
treeUrlQuery: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
treeSetting: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
tableConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
showAssets: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const showAssets = this.treeSetting?.showAssets || this.showAssets
|
||||
const treeUrlQuery = this.setTreeUrlQuery()
|
||||
const assetTreeUrl = `${this.treeUrl}?assets=${showAssets ? '1' : '0'}&${treeUrlQuery}`
|
||||
const vm = this
|
||||
|
||||
return {
|
||||
treeTabConfig: {
|
||||
activeMenu: 'CustomTree',
|
||||
submenu: [
|
||||
{
|
||||
title: this.$t('assets.AssetTree'),
|
||||
name: 'CustomTree',
|
||||
treeSetting: {
|
||||
showAssets,
|
||||
showMenu: false,
|
||||
showRefresh: true,
|
||||
showCreate: true,
|
||||
showUpdate: true,
|
||||
showDelete: true,
|
||||
hasRightMenu: true,
|
||||
showSearch: true,
|
||||
url: this.url,
|
||||
nodeUrl: this.nodeUrl,
|
||||
treeUrl: assetTreeUrl,
|
||||
callback: {
|
||||
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode),
|
||||
beforeRefresh: () => {
|
||||
const query = { ...this.$route.query, node_id: '', asset_id: '' }
|
||||
setTimeout(() => {
|
||||
vm.$router.replace({ query: query })
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
...this.treeSetting
|
||||
}
|
||||
},
|
||||
{
|
||||
title: this.$t('assets.BuiltinTree'),
|
||||
name: 'BuiltinTree',
|
||||
treeSetting: {
|
||||
showRefresh: true,
|
||||
showAssets: false,
|
||||
showSearch: false,
|
||||
customTreeHeaderName: this.$t('assets.BuiltinTree'),
|
||||
url: this.typeUrl,
|
||||
nodeUrl: this.treeSetting?.nodeUrl || this.nodeUrl,
|
||||
treeUrl: `${this.typeUrl}?assets=${showAssets ? '1' : '0'}&count_resource=${this.treeSetting.countResource || 'asset'}`,
|
||||
callback: {
|
||||
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
treeTableConfig() {
|
||||
if (this.treeSetting.notShowBuiltinTree) {
|
||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||
this.treeTabConfig.submenu.splice(1, 1)
|
||||
}
|
||||
return this.treeTabConfig
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.decorateRMenu()
|
||||
const treeSetting = this.treeTabConfig.submenu[0].treeSetting
|
||||
treeSetting.hasRightMenu = !this.currentOrgIsRoot
|
||||
treeSetting.showCreate = this.$hasPerm('assets.add_node')
|
||||
treeSetting.showUpdate = this.$hasPerm('assets.change_node')
|
||||
treeSetting.showDelete = this.$hasPerm('assets.delete_node')
|
||||
},
|
||||
methods: {
|
||||
setTreeUrlQuery() {
|
||||
let str = ''
|
||||
for (const key in this.treeUrlQuery) {
|
||||
str += `${key}=${this.treeUrlQuery[key]}&`
|
||||
}
|
||||
str = str.substr(0, str.length - 1)
|
||||
|
||||
return str
|
||||
},
|
||||
decorateRMenu() {
|
||||
const show_current_asset = this.$cookie.get('show_current_asset') || '0'
|
||||
if (show_current_asset === '1') {
|
||||
$('#m_show_asset_all_children_node').css('color', '#606266')
|
||||
$('#m_show_asset_only_current_node').css('color', 'green')
|
||||
} else {
|
||||
$('#m_show_asset_all_children_node').css('color', 'green')
|
||||
$('#m_show_asset_only_current_node').css('color', '#606266')
|
||||
}
|
||||
},
|
||||
getAssetsUrl(treeNode) {
|
||||
let url = this.treeSetting?.url || this.url
|
||||
if (treeNode.meta.type === 'node') {
|
||||
const nodeId = treeNode.meta.data.id
|
||||
url = setUrlParam(url, 'node_id', nodeId)
|
||||
url = setUrlParam(url, 'asset_id', '')
|
||||
} else if (treeNode.meta.type === 'asset') {
|
||||
const assetId = treeNode.meta.data?.id || treeNode.id
|
||||
url = setUrlParam(url, 'node_id', '')
|
||||
url = setUrlParam(url, 'asset_id', assetId)
|
||||
} else if (treeNode.meta.type === 'category') {
|
||||
url = setUrlParam(url, 'category', treeNode.meta.category)
|
||||
} else if (treeNode.meta.type === 'type') {
|
||||
url = setUrlParam(url, 'category', treeNode.meta.category)
|
||||
url = setUrlParam(url, 'type', treeNode.meta._type)
|
||||
} else if (treeNode.meta.type === 'platform') {
|
||||
url = setUrlParam(url, 'platform', treeNode.id)
|
||||
}
|
||||
const query = this.setTreeUrlQuery()
|
||||
url = query ? `${url}&${query}` : url
|
||||
this.$set(this.tableConfig, 'url', url)
|
||||
setRouterQuery(this, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,203 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<el-button
|
||||
:disabled="isDisabled"
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="onOpenDialog"
|
||||
>
|
||||
{{ $tc('common.Setting') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<Dialog
|
||||
v-if="visible"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
v-bind="$attrs"
|
||||
width="60%"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<AutoDataForm
|
||||
ref="autoDataForm"
|
||||
:form="form"
|
||||
class="data-form"
|
||||
v-bind="config"
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '../../Dialog'
|
||||
import AutoDataForm from '../../Form/AutoDataForm'
|
||||
|
||||
export default {
|
||||
componentName: 'AutomationParams',
|
||||
components: {
|
||||
Dialog,
|
||||
AutoDataForm
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('assets.PushParams')
|
||||
}
|
||||
},
|
||||
assets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
nodes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
platforms: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
method: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: `/api/v1/assets/platform-automation-methods/`
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
return {
|
||||
remoteMeta: {},
|
||||
visible: false,
|
||||
isDisabled: true,
|
||||
form: this.value,
|
||||
config: {
|
||||
url: this.url,
|
||||
hasSaveContinue: false,
|
||||
hasButtons: true,
|
||||
method: 'get',
|
||||
fields: [],
|
||||
fieldsMeta: {}
|
||||
},
|
||||
onFieldChangeHandler: _.debounce(vm.handleFieldChange, 1000)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
refForm() {
|
||||
return this.$refs.autoDataForm
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
nodes: {
|
||||
handler() {
|
||||
this.onFieldChangeHandler()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
assets: {
|
||||
handler() {
|
||||
this.onFieldChangeHandler()
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
platforms: {
|
||||
handler(newVal) {
|
||||
this.onFieldChangeHandler()
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getUrlMeta()
|
||||
},
|
||||
methods: {
|
||||
async getUrlMeta() {
|
||||
const data = await this.$store.dispatch('common/getUrlMeta', { url: this.url })
|
||||
this.remoteMeta = data.actions[this.config.method.toUpperCase()] || {}
|
||||
},
|
||||
async getFilterPlatforms() {
|
||||
return await this.$axios.post(
|
||||
'/api/v1/assets/platforms/filter-nodes-assets/',
|
||||
{
|
||||
'node_ids': this.nodes,
|
||||
'asset_ids': this.assets,
|
||||
'platform_ids': this.platforms.map(i => i.id || i.pk || i)
|
||||
}
|
||||
)
|
||||
},
|
||||
async handleFieldChange() {
|
||||
const platforms = await this.getFilterPlatforms()
|
||||
let pushAccountMethods = platforms.map(i => i.automation[this.method])
|
||||
pushAccountMethods = _.uniq(pushAccountMethods)
|
||||
// 检测是否有可设置的推送方式
|
||||
const hasCanSettingPushMethods = _.intersection(pushAccountMethods, Object.keys(this.remoteMeta))
|
||||
this.setFormConfig(hasCanSettingPushMethods)
|
||||
this.isDisabled = hasCanSettingPushMethods.length <= 0
|
||||
},
|
||||
setFormConfig(methods) {
|
||||
const newForm = {}
|
||||
const fields = []
|
||||
const fieldsMeta = {}
|
||||
this.config.fields = []
|
||||
// Todo: 未来改成后端处理,生成 serializer, 这里就不用判断类型了
|
||||
const typeMapper = {
|
||||
'string': 'input',
|
||||
'boolean': 'switch'
|
||||
}
|
||||
|
||||
for (const method of methods) {
|
||||
const filterField = this.remoteMeta[method] || {}
|
||||
// 修改资产、节点时不点击设置按钮也需要获取form表单值暴露出去
|
||||
if (this.form.hasOwnProperty(method)) {
|
||||
newForm[method] = this.form[method]
|
||||
}
|
||||
fields.push([filterField.label, [method]])
|
||||
fieldsMeta[method] = {
|
||||
fields: [],
|
||||
fieldsMeta: {}
|
||||
}
|
||||
if (Object.keys(filterField?.children || {}).length > 0) {
|
||||
for (const [k, v] of Object.entries(filterField.children)) {
|
||||
const item = {
|
||||
...v,
|
||||
type: typeMapper[v.type] || 'input'
|
||||
}
|
||||
delete item.default
|
||||
fieldsMeta[method].fields.push(k)
|
||||
fieldsMeta[method].fieldsMeta[k] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.form = newForm
|
||||
this.config.fields = fields
|
||||
this.config.fieldsMeta = fieldsMeta
|
||||
},
|
||||
onOpenDialog() {
|
||||
this.visible = true
|
||||
},
|
||||
onSubmit(form) {
|
||||
this.form = form
|
||||
this.$emit('input', form)
|
||||
setTimeout(() => {
|
||||
this.visible = false
|
||||
}, 100)
|
||||
this.$log.debug('Auto push form:', form)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/Table/ListTable/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'BlockedIPList',
|
||||
components: {
|
||||
ListTable
|
||||
},
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const vm = this
|
||||
return {
|
||||
tableConfig: {
|
||||
url: '/api/v1/settings/security/block-ip/',
|
||||
columns: [
|
||||
'ip', 'actions'
|
||||
],
|
||||
columnsMeta: {
|
||||
ip: {
|
||||
label: this.$t('assets.ip')
|
||||
},
|
||||
actions: {
|
||||
formatterArgs: {
|
||||
hasDelete: false,
|
||||
hasUpdate: false,
|
||||
hasClone: false,
|
||||
extraActions: [
|
||||
{
|
||||
name: 'UnlockIP',
|
||||
title: this.$t('setting.Unblock'),
|
||||
can: this.$hasPerm('settings.change_security'),
|
||||
type: 'primary',
|
||||
callback: ({ row }) => {
|
||||
this.$axios.post(
|
||||
'/api/v1/settings/security/unlock-ip/',
|
||||
{ ips: [row.ip] }
|
||||
).then(() => {
|
||||
vm.$message.success(this.$tc('common.UnlockSuccessMsg'))
|
||||
vm.$refs.ListTable.reloadTable()
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasExport: false,
|
||||
hasImport: false,
|
||||
hasCreate: false,
|
||||
hasSearch: false,
|
||||
hasRefresh: true,
|
||||
hasBulkDelete: false,
|
||||
hasBulkUpdate: false,
|
||||
hasLeftActions: true,
|
||||
hasRightActions: true,
|
||||
extraMoreActions: [
|
||||
{
|
||||
name: 'UnlockSelected',
|
||||
title: this.$t('setting.BulkUnblock'),
|
||||
type: 'primary',
|
||||
can: ({ selectedRows }) => {
|
||||
return selectedRows.length > 0
|
||||
},
|
||||
callback: function({ selectedRows }) {
|
||||
vm.$axios.post(
|
||||
'/api/v1/settings/security/unlock-ip/',
|
||||
{
|
||||
ips: selectedRows.map(v => { return v.ip })
|
||||
}
|
||||
).then(res => {
|
||||
vm.$message.success(vm.$tc('common.UnlockSuccessMsg'))
|
||||
vm.$refs.ListTable.reloadTable()
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
@click="onOpenDialog"
|
||||
>
|
||||
{{ $tc('common.View') }}
|
||||
<span>({{ $tc('setting.LockedIP', ipCounts ) }})</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<Dialog
|
||||
:visible.sync="visible"
|
||||
:title="title"
|
||||
width="40%"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:destroy-on-close="true"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<BlockedIPList />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dialog } from '@/components'
|
||||
import BlockedIPList from '@/components/Apps/BlockedIPs/BlockedIPList'
|
||||
|
||||
export default {
|
||||
componentName: 'BlockedIPs',
|
||||
components: {
|
||||
BlockedIPList,
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('setting.BlockedIPS')
|
||||
}
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: `/api/v1/assets/platform-automation-methods/`
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
remoteMeta: {},
|
||||
visible: false,
|
||||
form: this.value,
|
||||
ipCounts: 0,
|
||||
config: {
|
||||
url: this.url,
|
||||
hasSaveContinue: false,
|
||||
hasButtons: true,
|
||||
fields: [],
|
||||
fieldsMeta: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getLockedIp()
|
||||
},
|
||||
methods: {
|
||||
getLockedIp() {
|
||||
this.$axios.get('/api/v1/settings/security/block-ip/').then(res => {
|
||||
this.ipCounts = res.count
|
||||
})
|
||||
},
|
||||
onOpenDialog() {
|
||||
this.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,136 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Dialog
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:title="title"
|
||||
:visible.sync="showSecret"
|
||||
:width="'50'"
|
||||
v-bind="$attrs"
|
||||
@confirm="accountConfirmHandle"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<el-form :model="secretInfo" class="password-form" label-position="right" label-width="100px">
|
||||
<el-form-item :label="$tc('accounts.AccountChangeSecret.OldSecret')">
|
||||
<ShowKeyCopyFormatter
|
||||
:cell-value="secretInfo.old_secret"
|
||||
:col="{ formatterArgs: {
|
||||
name: 'old_secret'
|
||||
}}"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$tc('accounts.AccountChangeSecret.NewSecret')">
|
||||
<ShowKeyCopyFormatter
|
||||
:cell-value="secretInfo.new_secret"
|
||||
:col="{ formatterArgs: {
|
||||
name: 'new_secret'
|
||||
}}"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { ShowKeyCopyFormatter } from '@/components/Table/TableFormatters'
|
||||
|
||||
export default {
|
||||
name: 'RecordViewSecret',
|
||||
components: {
|
||||
Dialog,
|
||||
ShowKeyCopyFormatter
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$tc('common.ViewSecret')
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
secretInfo: {},
|
||||
showSecret: false,
|
||||
mfaDialogVisible: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
mounted() {
|
||||
this.showSecretDialog()
|
||||
},
|
||||
methods: {
|
||||
accountConfirmHandle() {
|
||||
this.showSecret = false
|
||||
this.mfaDialogVisible = false
|
||||
},
|
||||
showSecretDialog() {
|
||||
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then((res) => {
|
||||
this.secretInfo = res
|
||||
this.showSecret = true
|
||||
})
|
||||
},
|
||||
exit() {
|
||||
this.$emit('update:visible', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item-textarea >>> .el-textarea__inner {
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
padding: 5px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
>>> .el-form-item__label {
|
||||
padding-right: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
>>> .el-form-item__content {
|
||||
line-height: 30px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.title {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,163 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="chat-action">
|
||||
<Select2
|
||||
v-model="select.value"
|
||||
:disabled="isLoading || isSelectDisabled"
|
||||
v-bind="select"
|
||||
@change="onSelectChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="chat-input">
|
||||
<el-input
|
||||
v-model="inputValue"
|
||||
:disabled="isLoading"
|
||||
:placeholder="$tc('common.InputMessage')"
|
||||
type="textarea"
|
||||
@compositionend="isIM = false"
|
||||
@compositionstart="isIM = true"
|
||||
@keypress.native="onKeyEnter"
|
||||
/>
|
||||
<div class="input-action">
|
||||
<span class="right">
|
||||
<i :class="{'active': inputValue }" class="fa fa-send" @click="onSendHandle" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Select2 from '../../../../Form/FormFields/Select2.vue'
|
||||
import { useChat } from '../../useChat.js'
|
||||
|
||||
const { setLoading } = useChat()
|
||||
|
||||
export default {
|
||||
components: { Select2 },
|
||||
props: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isIM: false,
|
||||
inputValue: '',
|
||||
select: {
|
||||
url: '/api/v1/settings/chatai-prompts/',
|
||||
value: '',
|
||||
multiple: false,
|
||||
placeholder: this.$t('common.Prompt'),
|
||||
ajax: {
|
||||
transformOption: (item) => {
|
||||
return { label: item.name, value: item.content }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isLoading: state => state.chat.loading
|
||||
}),
|
||||
isSelectDisabled() {
|
||||
return !!this.select.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onKeyEnter(event) {
|
||||
if (event.key === 'Enter') {
|
||||
if ((!this.isIM && !event.shiftKey) || (this.isIM && event.ctrlKey)) {
|
||||
event.preventDefault()
|
||||
this.onSendHandle()
|
||||
}
|
||||
}
|
||||
},
|
||||
onSendHandle() {
|
||||
if (!this.inputValue) return
|
||||
|
||||
setLoading(true)
|
||||
this.$emit('send', this.inputValue)
|
||||
this.inputValue = ''
|
||||
},
|
||||
onSelectChange(value) {
|
||||
this.$emit('select-prompt', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
.chat-action {
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
&>>> .el-select {
|
||||
width: 50%;
|
||||
.el-input__inner {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border-radius: 14px;
|
||||
border-color: transparent;
|
||||
background-color: #f7f7f8;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
&:hover {
|
||||
background-color: #ededed;
|
||||
}
|
||||
}
|
||||
.el-input__icon {
|
||||
line-height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 12px;
|
||||
&:has(.el-textarea__inner:focus) {
|
||||
border: 1px solid var(--color-primary);
|
||||
}
|
||||
&>>> .el-textarea {
|
||||
height: 100%;
|
||||
.el-textarea__inner {
|
||||
height: 100%;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
resize: none;
|
||||
&::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-textarea.is-disabled + .input-action {
|
||||
background-color: #F5F7FA;
|
||||
cursor: no-drop;
|
||||
i {
|
||||
cursor: no-drop;
|
||||
}
|
||||
}
|
||||
.input-action {
|
||||
overflow: hidden;
|
||||
padding: 0 16px 15px;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
.right {
|
||||
float: right;
|
||||
.active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div :class="{'user-role': isUserRole}" class="chat-item">
|
||||
<div class="avatar">
|
||||
<el-avatar :src="isUserRole ? userUrl : chatUrl" class="header-avatar" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="operational">
|
||||
<span class="date">
|
||||
{{ $moment(item.message.create_time).format('YYYY-MM-DD HH:mm:ss') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="message">
|
||||
<div class="message-content">
|
||||
<span v-if="isSystemError" class="error">
|
||||
{{ item.message.content }}
|
||||
</span>
|
||||
<span v-else class="chat-text">
|
||||
<MessageText :message="item.message" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="action">
|
||||
<el-tooltip
|
||||
v-if="isSystemError && isLoading"
|
||||
:content="$tc('common.Reconnect')"
|
||||
effect="dark"
|
||||
placement="top"
|
||||
>
|
||||
<svg-icon icon-class="refresh" @click="onRefresh" />
|
||||
</el-tooltip>
|
||||
<el-dropdown v-else size="small" @command="handleCommand">
|
||||
<span class="el-dropdown-link">
|
||||
<i class="fa fa-ellipsis-v" />
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-for="i in dropdownOptions" :key="i.action" :command="i.action">
|
||||
{{ i.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MessageText from './MessageText.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import { copy } from '@/utils/common'
|
||||
import { useChat } from '../../useChat.js'
|
||||
import { reconnect } from '@/utils/socket'
|
||||
|
||||
const { setLoading, removeLoadingMessageInChat } = useChat()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MessageText
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chatUrl: require('@/assets/img/chat.png'),
|
||||
userUrl: '/api/v1/settings/logo/',
|
||||
dropdownOptions: [
|
||||
{
|
||||
action: 'copy',
|
||||
label: this.$t('common.Copy')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isLoading: state => state.chat.loading
|
||||
}),
|
||||
isUserRole() {
|
||||
return this.item.message?.role === 'user'
|
||||
},
|
||||
isSystemError() {
|
||||
return this.item.type === 'error' && this.item.message?.role === 'assistant'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onRefresh() {
|
||||
reconnect()
|
||||
removeLoadingMessageInChat()
|
||||
setLoading(false)
|
||||
},
|
||||
handleCommand(value) {
|
||||
if (value === 'copy') {
|
||||
copy(this.item.message.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-item {
|
||||
display: flex;
|
||||
padding: 16px 14px 0;
|
||||
&:last-child {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.avatar {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-top: 2px;
|
||||
.header-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
&>>> img {
|
||||
background-color: #e5e5e7;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin-left: 6px;
|
||||
overflow: hidden;
|
||||
.operational {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
.copy {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.message {
|
||||
display: -webkit-box;
|
||||
.message-content {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border-radius: 2px 12px 12px;
|
||||
background-color: #f0f1f5;
|
||||
}
|
||||
.action {
|
||||
.svg-icon {
|
||||
transform: translateY(50%);
|
||||
margin-left: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.el-dropdown {
|
||||
height: 32px;
|
||||
line-height: 37px;
|
||||
font-size: 13px;
|
||||
.el-dropdown-link {
|
||||
i {
|
||||
padding: 4px 5px;
|
||||
font-size: 15px;
|
||||
color: #8d9091;
|
||||
&:hover {
|
||||
color: #7b8085
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.user-role {
|
||||
flex-direction: row-reverse;
|
||||
.content {
|
||||
margin-right: 10px;
|
||||
.operational {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.message {
|
||||
flex-direction: row-reverse;
|
||||
.message-content {
|
||||
background-color: var(--menu-hover);
|
||||
border-radius: 12px 2px 12px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,178 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="textRef" class="leading-relaxed break-words">
|
||||
<span v-if="message.content === 'loading'" class="loading-box">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
<div v-else class="inline-block markdown-body" v-html="text" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MarkdownIt from 'markdown-it'
|
||||
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'
|
||||
|
||||
/* eslint-disable vue/no-v-html */
|
||||
export default {
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
markdown: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
text() {
|
||||
const value = this.message?.content || ''
|
||||
if (value && this.markdown) {
|
||||
return this.markdown?.render(value)
|
||||
}
|
||||
return this.$xss.process(value)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
updated() {
|
||||
this.addCopyEvents()
|
||||
},
|
||||
destroyed() {
|
||||
this.removeCopyEvents()
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
const vm = this
|
||||
this.markdown = new MarkdownIt({
|
||||
html: false,
|
||||
linkify: true,
|
||||
highlight(code, language) {
|
||||
const validLang = !!(language && hljs.getLanguage(language))
|
||||
if (validLang) {
|
||||
const lang = language || ''
|
||||
return vm.highlightBlock(hljs.highlight(lang, code, true).value, lang)
|
||||
}
|
||||
return vm.highlightBlock(hljs.highlightAuto(code).value, '')
|
||||
}
|
||||
})
|
||||
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) {
|
||||
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>`
|
||||
},
|
||||
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)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
removeCopyEvents() {
|
||||
if (this.$refs.textRef) {
|
||||
const copyBtn = this.$refs.textRef.querySelectorAll('.code-block-header__copy')
|
||||
copyBtn.forEach((btn) => {
|
||||
btn.removeEventListener('click', () => {})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.markdown-body {
|
||||
font-size: 13px;
|
||||
&>>> p {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
background: inherit;
|
||||
&>>> pre {
|
||||
padding: 0 0 6px 0;
|
||||
.hljs.code-block-body {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
&>>> .code-block-wrapper {
|
||||
background: #1F2329;
|
||||
padding: 2px 6px;
|
||||
margin: 5px 0;
|
||||
|
||||
.code-block-body {
|
||||
padding: 5px 10px 0;
|
||||
};
|
||||
.code-block-header {
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
background: #353946;
|
||||
color: #c2d1e1;
|
||||
|
||||
.code-block-header__copy {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #6e747b;
|
||||
}
|
||||
}
|
||||
}
|
||||
.hljs.code-block-body.javascript {
|
||||
.hljs-comment {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
>>> .link-style {
|
||||
color: #487bf4;
|
||||
&:hover {
|
||||
color: #275ee3;
|
||||
}
|
||||
}
|
||||
.loading-box{
|
||||
margin-left: 6px;
|
||||
}
|
||||
.loading-box span{
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-right: 5px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
background: #676A6c;
|
||||
animation: load 1.2s ease infinite;
|
||||
}
|
||||
.loading-box span:last-child{
|
||||
margin-right: 0;
|
||||
}
|
||||
@keyframes load{
|
||||
0%{
|
||||
opacity: 1;
|
||||
}
|
||||
100%{
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.loading-box span:nth-child(1){
|
||||
animation-delay: 0.23s;
|
||||
}
|
||||
.loading-box span:nth-child(2){
|
||||
animation-delay: 0.36s;
|
||||
}
|
||||
.loading-box span:nth-child(3){
|
||||
animation-delay: 0.49s;
|
||||
}
|
||||
</style>
|
||||
@@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<div class="chat-content">
|
||||
<div id="scrollRef" class="chat-list">
|
||||
<div v-if="showIntroduction" class="introduction">
|
||||
<div v-for="(item, index) in introduction" :key="index" class="introduction-item" @click="sendIntroduction(item)">
|
||||
<div class="head">
|
||||
<i v-if="item.icon" :class="item.icon" />
|
||||
<span class="title">{{ item.title }}</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChatMessage v-for="(item, index) in activeChat.chats" :key="index" :item="item" />
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<el-button
|
||||
v-if="isLoading && socket && socket.readyState === 1"
|
||||
class="stop"
|
||||
icon="fa fa-stop-circle-o"
|
||||
round
|
||||
size="small"
|
||||
@click="onStopHandle"
|
||||
>{{ $tc('common.Stop') }}</el-button>
|
||||
<ChatInput ref="chatInput" @send="onSendHandle" @select-prompt="onSelectPromptHandle" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChatInput from './ChatInput.vue'
|
||||
import ChatMessage from './ChatMessage.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import { closeWebSocket, createWebSocket, onSend, ws } from '@/utils/socket'
|
||||
import { getInputFocus, useChat } from '../../useChat.js'
|
||||
|
||||
const {
|
||||
setLoading,
|
||||
clearChats,
|
||||
addChatMessageById,
|
||||
addMessageToActiveChat,
|
||||
newChatAndAddMessageById,
|
||||
removeLoadingMessageInChat,
|
||||
updateChaMessageContentById,
|
||||
addTemporaryLoadingToChat
|
||||
} = useChat()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatInput,
|
||||
ChatMessage
|
||||
},
|
||||
props: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {},
|
||||
prompt: '',
|
||||
currentConversationId: '',
|
||||
showIntroduction: false,
|
||||
introduction: [
|
||||
{
|
||||
title: this.$t('common.introduction.ConceptTitle'),
|
||||
content: this.$t('common.introduction.ConceptContent')
|
||||
},
|
||||
{
|
||||
title: this.$t('common.introduction.IdeaTitle'),
|
||||
content: this.$t('common.introduction.IdeaContent')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isLoading: state => state.chat.loading,
|
||||
activeChat: state => state.chat.activeChat
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
closeWebSocket()
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.initWebSocket()
|
||||
this.initChatMessage()
|
||||
},
|
||||
initWebSocket() {
|
||||
const { NODE_ENV, VUE_APP_KAEL_HOST } = process.env || {}
|
||||
const api = '/kael/chat/system/'
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const path = `${protocol}://${window.location.host}${api}`
|
||||
const index = VUE_APP_KAEL_HOST?.indexOf('://')
|
||||
const localPath = protocol + VUE_APP_KAEL_HOST?.substring(index, VUE_APP_KAEL_HOST?.length) + api
|
||||
const url = NODE_ENV === 'development' ? localPath : path
|
||||
createWebSocket(url, this.onWebSocketMessage)
|
||||
},
|
||||
initChatMessage() {
|
||||
this.prompt = ''
|
||||
this.showIntroduction = true
|
||||
this.currentConversationId = ''
|
||||
this.$refs.chatInput.select.value = ''
|
||||
const chat = {
|
||||
message: {
|
||||
content: this.$t('common.ChatHello'),
|
||||
role: 'assistant',
|
||||
create_time: new Date()
|
||||
}
|
||||
}
|
||||
newChatAndAddMessageById(chat)
|
||||
setLoading(false)
|
||||
},
|
||||
onWebSocketMessage(data) {
|
||||
if (data.type === 'message') {
|
||||
this.onChatMessage(data)
|
||||
}
|
||||
if (data.type === 'error') {
|
||||
this.onSystemMessage(data)
|
||||
}
|
||||
},
|
||||
onChatMessage(data) {
|
||||
if (data.conversation_id) {
|
||||
setLoading(true)
|
||||
removeLoadingMessageInChat()
|
||||
this.currentConversationId = data.conversation_id
|
||||
updateChaMessageContentById(data.message.id, data)
|
||||
}
|
||||
if (data.message?.type === 'finish') {
|
||||
setLoading(false)
|
||||
getInputFocus()
|
||||
}
|
||||
},
|
||||
onSystemMessage(data) {
|
||||
data.message = {
|
||||
content: data.system_message,
|
||||
role: 'assistant',
|
||||
create_time: new Date()
|
||||
}
|
||||
removeLoadingMessageInChat()
|
||||
addMessageToActiveChat(data)
|
||||
this.socketReadyStateSuccess = false
|
||||
setLoading(true)
|
||||
},
|
||||
onSendHandle(value) {
|
||||
this.showIntroduction = false
|
||||
this.socket = ws || {}
|
||||
if (ws?.readyState === 1) {
|
||||
this.socketReadyStateSuccess = true
|
||||
const chat = {
|
||||
message: {
|
||||
content: value,
|
||||
role: 'user',
|
||||
create_time: new Date()
|
||||
}
|
||||
}
|
||||
const message = {
|
||||
content: value,
|
||||
prompt: this.prompt,
|
||||
conversation_id: this.currentConversationId || ''
|
||||
}
|
||||
addChatMessageById(chat)
|
||||
onSend(message)
|
||||
addTemporaryLoadingToChat()
|
||||
} else {
|
||||
const chat = {
|
||||
message: {
|
||||
content: this.$t('common.ConnectionDropped'),
|
||||
role: 'assistant',
|
||||
create_time: new Date()
|
||||
},
|
||||
type: 'error'
|
||||
}
|
||||
addChatMessageById(chat)
|
||||
this.socketReadyStateSuccess = false
|
||||
setLoading(true)
|
||||
}
|
||||
},
|
||||
onSelectPromptHandle(value) {
|
||||
this.prompt = value
|
||||
this.currentConversationId = ''
|
||||
this.showIntroduction = false
|
||||
this.onSendHandle(value)
|
||||
},
|
||||
onNewChat() {
|
||||
clearChats()
|
||||
this.initChatMessage()
|
||||
},
|
||||
onStopHandle() {
|
||||
this.$axios.post(
|
||||
'/kael/interrupt_current_ask/',
|
||||
{ id: this.currentConversationId || '' }
|
||||
).finally(() => {
|
||||
removeLoadingMessageInChat()
|
||||
setLoading(false)
|
||||
})
|
||||
},
|
||||
sendIntroduction(item) {
|
||||
this.showIntroduction = false
|
||||
this.onSendHandle(item.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
.introduction {
|
||||
padding: 16px 14px 0;
|
||||
|
||||
.introduction-item {
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
background-color: var(--menu-hover);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 2px 2px #00000014;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.head {
|
||||
margin-bottom: 2px;
|
||||
.title {
|
||||
font-weight: 500;
|
||||
color: #373739;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: inline-block;
|
||||
color: #a7a7ab;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
.chat-list {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
padding: 0 15px 25px;
|
||||
overflow-y: auto;
|
||||
user-select: text;
|
||||
&::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
.input-box {
|
||||
position: relative;
|
||||
height: 160px;
|
||||
padding: 0 15px;
|
||||
margin-bottom: 15px;
|
||||
border-top: 1px solid #ececec;
|
||||
}
|
||||
.stop {
|
||||
position: absolute;
|
||||
top: -37px;
|
||||
left: 50%;
|
||||
z-index: 11;
|
||||
transform: translateX(-50%);
|
||||
>>> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="close-sidebar">
|
||||
<i v-if="hasClose" class="el-icon-close" @click="onClose" />
|
||||
</div>
|
||||
<el-tabs v-model="active" :tab-position="'right'" @tab-click="handleClick">
|
||||
<el-tab-pane v-for="(item) in submenu" :key="item.name" :name="item.name">
|
||||
<span slot="label">
|
||||
<el-tooltip effect="dark" placement="left" :content="item.label">
|
||||
<svg-icon :icon-class="item.icon" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
active: {
|
||||
type: String,
|
||||
default: 'chat'
|
||||
},
|
||||
hasClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
submenu: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick(tab, event) {
|
||||
this.$emit('tab-click', tab)
|
||||
},
|
||||
onClose() {
|
||||
this.$parent.onClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f0f1f5;
|
||||
.close-sidebar {
|
||||
height: 48px;
|
||||
padding: 12px 0;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
i {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
background: var(--menu-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
>>> .el-tabs {
|
||||
.el-tabs__item {
|
||||
padding: 0 13px;
|
||||
font-size: 15px;
|
||||
:hover {
|
||||
color: #7b8085;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<div class="chat">
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="left">
|
||||
<img :src="robotUrl" alt="">
|
||||
<span class="title">{{ title }}</span>
|
||||
</div>
|
||||
<span class="new" @click="onNewChat">
|
||||
<i class="el-icon-plus" />
|
||||
<span>{{ $tc('common.NewChat') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<keep-alive>
|
||||
<component :is="active" ref="component" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
<Sidebar v-bind="$attrs" :active.sync="active" :submenu="submenu" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sidebar from './components/Sidebar/index.vue'
|
||||
import Chat from './components/ChitChat/index.vue'
|
||||
import { getInputFocus } from './useChat.js'
|
||||
import { ws } from '@/utils/socket'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Chat,
|
||||
Sidebar
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return this.$t('setting.ChatAI')
|
||||
}
|
||||
},
|
||||
drawerPanelVisible: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: 'chat',
|
||||
robotUrl: require('../../../assets/img/robot-assistant.png'),
|
||||
submenu: [
|
||||
{
|
||||
name: 'chat',
|
||||
label: this.$t('common.Chat'),
|
||||
icon: 'chat'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
drawerPanelVisible(value) {
|
||||
if (value && !ws) {
|
||||
this.initWebSocket()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initWebSocket() {
|
||||
this.$refs.component?.init()
|
||||
},
|
||||
onClose() {
|
||||
this.$parent.show = false
|
||||
},
|
||||
onNewChat() {
|
||||
this.active = 'chat'
|
||||
this.$nextTick(() => {
|
||||
this.$refs.component?.onNewChat()
|
||||
getInputFocus()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
padding: 0 16px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #ececec;
|
||||
.left {
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
vertical-align: sub;
|
||||
}
|
||||
.title {
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
.new {
|
||||
display: inline-block;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border-radius: 16px;
|
||||
padding: 0 10px;
|
||||
transform: translateY(32%);
|
||||
color: var(--color-primary);
|
||||
background-color: #f7f7f8;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
&:hover {
|
||||
background-color: #ededed;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
.sidebar {
|
||||
height: 100%;
|
||||
width: 42px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,80 +0,0 @@
|
||||
import store from '@/store'
|
||||
import { pageScroll } from '@/utils/common'
|
||||
|
||||
export const getInputFocus = () => {
|
||||
const dom = document.querySelector('.chat-input .el-textarea__inner')
|
||||
setTimeout(() => dom?.focus(), 200)
|
||||
}
|
||||
|
||||
export function useChat() {
|
||||
const chatStore = {}
|
||||
|
||||
const setLoading = (loading) => {
|
||||
store.commit('chat/setLoading', loading)
|
||||
}
|
||||
|
||||
const onNewChat = (name) => {
|
||||
const data = {
|
||||
name: name || `new chat`,
|
||||
id: 1,
|
||||
conversation_id: '',
|
||||
chats: []
|
||||
}
|
||||
store.commit('chat/addChatToStore', data)
|
||||
}
|
||||
|
||||
const clearChats = () => {
|
||||
store.commit('chat/clearChats')
|
||||
}
|
||||
|
||||
const addMessageToActiveChat = (chat) => {
|
||||
store.commit('chat/addMessageToActiveChat', chat)
|
||||
}
|
||||
|
||||
const removeLoadingMessageInChat = () => {
|
||||
store.commit('chat/removeLoadingMessageInChat')
|
||||
}
|
||||
|
||||
const addChatMessageById = (chat) => {
|
||||
store.commit('chat/addMessageToActiveChat', chat)
|
||||
if (chat?.conversation_id) {
|
||||
store.commit('chat/setActiveChatConversationId', chat.conversation_id)
|
||||
}
|
||||
pageScroll('scrollRef')
|
||||
}
|
||||
|
||||
const addTemporaryLoadingToChat = () => {
|
||||
const temporaryChat = {
|
||||
message: {
|
||||
content: 'loading',
|
||||
role: 'assistant',
|
||||
create_time: new Date()
|
||||
}
|
||||
}
|
||||
addChatMessageById(temporaryChat)
|
||||
}
|
||||
|
||||
const newChatAndAddMessageById = (chat) => {
|
||||
onNewChat(chat.message.content)
|
||||
addChatMessageById(chat)
|
||||
}
|
||||
|
||||
const updateChaMessageContentById = (id, data) => {
|
||||
store.commit('chat/updateChaMessageContentById', { id, data })
|
||||
pageScroll('scrollRef')
|
||||
}
|
||||
|
||||
return {
|
||||
chatStore,
|
||||
setLoading,
|
||||
onNewChat,
|
||||
clearChats,
|
||||
getInputFocus,
|
||||
addMessageToActiveChat,
|
||||
newChatAndAddMessageById,
|
||||
removeLoadingMessageInChat,
|
||||
addChatMessageById,
|
||||
addTemporaryLoadingToChat,
|
||||
updateChaMessageContentById
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
<template>
|
||||
<div ref="drawer" :class="{show: show}" class="drawer">
|
||||
<div :style="{'background-color': modal ? 'rgba(0, 0, 0, .3)' : 'transparent'}" class="modal" />
|
||||
<div :style="{'width': width}" class="drawer-panel">
|
||||
<div v-show="!show" ref="dragBox" class="handle-button">
|
||||
<i v-if="icon.startsWith('fa') || icon.startsWith('el')" :class="show ? 'el-icon-close': icon" />
|
||||
<img v-else :src="icon" alt="">
|
||||
</div>
|
||||
<div class="drawer-panel-item">
|
||||
<slot :drawer-panel-visible="show" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'DrawerPanel',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'el-icon-setting'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '440px'
|
||||
},
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
clickNotClose: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(value) {
|
||||
if (value && !this.clickNotClose) {
|
||||
this.addEventClick()
|
||||
}
|
||||
this.$emit('toggle', this.show)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
this.insertToBody()
|
||||
},
|
||||
beforeDestroy() {
|
||||
const element = this.$refs.drawer
|
||||
element.remove()
|
||||
window.removeEventListener('click', this.closeSidebar)
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.$nextTick(() => {
|
||||
const dragBox = this.$refs.dragBox
|
||||
const clientOffset = {}
|
||||
dragBox.addEventListener('mousedown', (event) => {
|
||||
const offsetX = dragBox.getBoundingClientRect().left
|
||||
const offsetY = dragBox.getBoundingClientRect().top
|
||||
const innerX = event.clientX - offsetX
|
||||
const innerY = event.clientY - offsetY
|
||||
|
||||
clientOffset.clientX = event.clientX
|
||||
clientOffset.clientY = event.clientY
|
||||
document.onmousemove = function(event) {
|
||||
dragBox.style.left = event.clientX - innerX + 'px'
|
||||
dragBox.style.top = event.clientY - innerY + 'px'
|
||||
const dragDivTop = window.innerHeight - dragBox.getBoundingClientRect().height
|
||||
const dragDivLeft = window.innerWidth - dragBox.getBoundingClientRect().width
|
||||
dragBox.style.left = dragDivLeft + 'px'
|
||||
dragBox.style.left = '-48px'
|
||||
if (dragBox.getBoundingClientRect().top <= 0) {
|
||||
dragBox.style.top = '0px'
|
||||
}
|
||||
if (dragBox.getBoundingClientRect().top >= dragDivTop) {
|
||||
dragBox.style.top = dragDivTop + 'px'
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
document.onmouseup = function() {
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
}
|
||||
}, false)
|
||||
dragBox.addEventListener('mouseup', (event) => {
|
||||
const clientX = event.clientX
|
||||
const clientY = event.clientY
|
||||
if (this.isDifferenceWithinThreshold(clientX, clientOffset.clientX) && this.isDifferenceWithinThreshold(clientY, clientOffset.clientY)) {
|
||||
this.show = !this.show
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
isDifferenceWithinThreshold(num1, num2, threshold = 5) {
|
||||
const difference = Math.abs(num1 - num2)
|
||||
return difference <= threshold
|
||||
},
|
||||
addEventClick() {
|
||||
window.addEventListener('click', this.closeSidebar)
|
||||
},
|
||||
closeSidebar(evt) {
|
||||
const parent = evt.target.closest('.drawer-panel')
|
||||
if (!parent && evt.target.className === 'modal') {
|
||||
this.show = false
|
||||
}
|
||||
},
|
||||
insertToBody() {
|
||||
const element = this.$refs.drawer
|
||||
const body = document.querySelector('body')
|
||||
body.insertBefore(element, body.firstChild)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
|
||||
background: rgba(0, 0, 0, .3);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.drawer-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
min-width: 260px;
|
||||
height: 100vh;
|
||||
user-select: none;
|
||||
transition: transform .25s cubic-bezier(.7, .3, .1, 1);
|
||||
box-shadow: 0 0 8px 4px #00000014;
|
||||
transform: translate(100%);
|
||||
background: #FFFFFF;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.drawer-panel-item {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.drawer-panel-item::-webkit-scrollbar-track {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.show {
|
||||
transition: all .3s cubic-bezier(.7, .3, .1, 1);
|
||||
}
|
||||
|
||||
.show .modal {
|
||||
z-index: 1003;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.show .drawer-panel {
|
||||
transform: translate(0);
|
||||
}
|
||||
|
||||
.handle-button {
|
||||
position: absolute;
|
||||
bottom: 20%;
|
||||
left: -48px;
|
||||
width: 48px;
|
||||
height: 45px;
|
||||
line-height: 45px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
border-radius: 20px 0 0 20px;
|
||||
z-index: 0;
|
||||
pointer-events: auto;
|
||||
color: #fff;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 0 8px 4px #00000014;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
left: -50px !important;
|
||||
width: 50px !important;
|
||||
transform: scale(1.06);
|
||||
}
|
||||
i {
|
||||
font-size: 20px;
|
||||
line-height: 45px;
|
||||
}
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
transform: translateY(10%);
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-if="iVisible"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="$tc('assets.TestGatewayTestConnection')"
|
||||
:visible.sync="iVisible"
|
||||
top="35vh"
|
||||
width="40%"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col :md="4" :sm="24">
|
||||
<div style="line-height: 34px">{{ $t('assets.SSHPort') }}</div>
|
||||
</el-col>
|
||||
<el-col :md="14" :sm="24">
|
||||
<el-input v-model="port" />
|
||||
<span class="help-tips help-block">{{ $t('assets.TestGatewayHelpMessage') }}</span>
|
||||
</el-col>
|
||||
<el-col :md="4" :sm="24">
|
||||
<el-button
|
||||
:loading="loading"
|
||||
size="mini"
|
||||
style="line-height:20px "
|
||||
type="primary"
|
||||
@click="dialogConfirm"
|
||||
>
|
||||
{{ this.$t('common.Confirm') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'GatewayDialog',
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
port: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
cell: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
iVisible: {
|
||||
get() {
|
||||
return this.visible
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dialogConfirm() {
|
||||
if (isNaN(this.port)) {
|
||||
return this.$message.error(this.$tc('common.TestPortErrorMsg'))
|
||||
}
|
||||
this.$axios.post(
|
||||
`/api/v1/assets/gateways/${this.cell}/test-connective/`,
|
||||
{ port: this.port }
|
||||
)
|
||||
.then((res) => {
|
||||
openTaskPage(res['task'])
|
||||
}).finally(() => {
|
||||
this.iVisible = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<el-row :gutter="24">
|
||||
<el-col :md="20" :sm="22">
|
||||
<ListTable v-bind="config" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/Table/ListTable/index.vue'
|
||||
import { toM2MJsonParams } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'AssetJsonTab',
|
||||
components: {
|
||||
ListTable
|
||||
},
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const [key, value] = toM2MJsonParams(this.object.assets)
|
||||
return {
|
||||
config: {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasImport: false,
|
||||
hasExport: false
|
||||
},
|
||||
tableConfig: {
|
||||
url: `/api/v1/assets/assets/?${key}=${value}`,
|
||||
columns: ['name', 'address', 'platform',
|
||||
'type', 'is_active'
|
||||
],
|
||||
columnsMeta: {
|
||||
name: {
|
||||
label: this.$t('assets.Asset'),
|
||||
formatter: (row) => {
|
||||
const to = {
|
||||
name: 'AssetDetail',
|
||||
params: { id: row.id }
|
||||
}
|
||||
if (this.$hasPerm('assets.view_asset')) {
|
||||
return <router-link to={to} class='text-link'>{row.name}</router-link>
|
||||
} else {
|
||||
return <span>{row.name}</span>
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
has: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iUrl() {
|
||||
return `/api/v1/users/users/`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<el-row :gutter="24">
|
||||
<el-col :md="20" :sm="22">
|
||||
<ListTable v-bind="config" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListTable from '@/components/Table/ListTable/index.vue'
|
||||
import { toM2MJsonParams } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'User',
|
||||
components: {
|
||||
ListTable
|
||||
},
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const [key, value] = toM2MJsonParams(this.object.users)
|
||||
return {
|
||||
config: {
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasImport: false,
|
||||
hasExport: false
|
||||
},
|
||||
tableConfig: {
|
||||
url: `/api/v1/users/users/?${key}=${value}`,
|
||||
columns: [
|
||||
'name', 'username', 'groups', 'system_roles',
|
||||
'org_roles', 'source', 'is_valid'
|
||||
],
|
||||
columnsMeta: {
|
||||
name: {
|
||||
label: this.$t('common.Name'),
|
||||
formatter: (row) => {
|
||||
const to = {
|
||||
name: 'UserDetail',
|
||||
params: { id: row.id }
|
||||
}
|
||||
if (this.$hasPerm('users.view_user')) {
|
||||
return <router-link to={to} class='text-link'>{row.name}</router-link>
|
||||
} else {
|
||||
return <span>{row.name}</span>
|
||||
}
|
||||
}
|
||||
},
|
||||
system_roles: {
|
||||
label: this.$t('users.SystemRoles'),
|
||||
formatter: (row) => {
|
||||
return row['system_roles'].map(item => item['display_name']).join(', ') || '-'
|
||||
},
|
||||
filters: [],
|
||||
columnKey: 'system_roles'
|
||||
},
|
||||
org_roles: {
|
||||
label: this.$t('users.OrgRoles'),
|
||||
formatter: (row) => {
|
||||
return row['org_roles'].map(item => item['display_name']).join(', ') || '-'
|
||||
},
|
||||
filters: [],
|
||||
columnKey: 'org_roles',
|
||||
has: () => {
|
||||
return this.$store.getters.hasValidLicense && !this.currentOrgIsRoot
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
has: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iUrl() {
|
||||
return `/api/v1/users/users/`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :md="12" :sm="24">
|
||||
<IBox :title="title" class="block" v-bind="$attrs">
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(activity, index) in activities"
|
||||
:key="index"
|
||||
:size="activity.size"
|
||||
:timestamp="activity.timestamp"
|
||||
:type="activity.type"
|
||||
placement="bottom"
|
||||
>
|
||||
{{ activity.content }}
|
||||
<el-link
|
||||
v-if="activity['detail_url']"
|
||||
type="primary"
|
||||
@click.native="onClick(activity)"
|
||||
>
|
||||
{{ $tc('common.Detail') }}
|
||||
</el-link>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</IBox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<DiffDetail ref="DetailDialog" :title="$tc('route.OperateLog')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IBox from '@/components/IBox/index.vue'
|
||||
import DiffDetail from '@/components/Dialog/DiffDetail.vue'
|
||||
import { openTaskPage } from '@/utils/jms'
|
||||
|
||||
export default {
|
||||
name: 'ResourceActivity',
|
||||
components: {
|
||||
IBox,
|
||||
DiffDetail
|
||||
},
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activityUrl: `/api/v1/audits/activities/?resource_id=${this.object.id}`,
|
||||
title: `${this.$t('common.Activity')} - ${this.$t('common.Last30')}`,
|
||||
activities: [
|
||||
{
|
||||
content: this.$t('common.Now'),
|
||||
timestamp: this.$moment().format('YYYY-MM-DD HH:mm:ss'),
|
||||
type: 'primary'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getActivities()
|
||||
},
|
||||
methods: {
|
||||
getActivities() {
|
||||
this.$axios.get(this.activityUrl).then(res => {
|
||||
for (const i in res) {
|
||||
this.activities.push(res[i])
|
||||
}
|
||||
})
|
||||
},
|
||||
onClick(activity) {
|
||||
const type = activity['r_type']
|
||||
const taskUrl = activity['detail_url']
|
||||
if (type === 'O') {
|
||||
this.$axios.get(taskUrl).then(
|
||||
res => {
|
||||
this.$refs.DetailDialog.show(res.diff)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
openTaskPage('', 'celery', taskUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,234 +0,0 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:close-on-click-modal="false"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
:show-confirm="false"
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
class="dialog-content"
|
||||
v-bind="$attrs"
|
||||
width="600px"
|
||||
@confirm="visible = false"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<div v-if="confirmTypeRequired === 'relogin'">
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-alert
|
||||
:title="$tc('auth.ReLoginTitle')"
|
||||
center
|
||||
style="margin-bottom: 20px;"
|
||||
type="error"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-button class="confirm-btn" size="mini" type="primary" @click="logout">
|
||||
{{ this.$t('auth.ReLogin') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24" :span="24" class="add">
|
||||
<el-select
|
||||
v-model="subTypeSelected"
|
||||
style="width: 100%; margin-bottom: 20px;"
|
||||
@change="handleSubTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item of subTypeChoices"
|
||||
:key="item.name"
|
||||
:disabled="item.disabled"
|
||||
:label="item.display_name"
|
||||
:value="item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 0 auto;">
|
||||
<el-col :md="24" :sm="24" style="display: flex; margin-bottom: 20px;">
|
||||
<el-input
|
||||
v-model="secretValue"
|
||||
:placeholder="inputPlaceholder"
|
||||
:show-password="showPassword"
|
||||
@keyup.enter.native="handleConfirm"
|
||||
/>
|
||||
<span v-if="subTypeSelected === 'sms'" style="margin: -1px 0 0 20px;">
|
||||
<el-button
|
||||
:disabled="smsBtnDisabled"
|
||||
size="mini"
|
||||
style="line-height:20px; float: right;"
|
||||
type="primary"
|
||||
@click="sendSMSCode"
|
||||
>
|
||||
{{ smsBtnText }}
|
||||
</el-button>
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin: 10px auto;">
|
||||
<el-col :md="24" :sm="24">
|
||||
<el-button class="confirm-btn" size="mini" type="primary" @click="handleConfirm">
|
||||
{{ this.$t('common.Confirm') }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
import { encryptPassword } from '@/utils/crypto'
|
||||
|
||||
export default {
|
||||
name: 'UserConfirmDialog',
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
handler: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: this.$t('common.CurrentUserVerify'),
|
||||
smsWidth: 0,
|
||||
subTypeSelected: '',
|
||||
inputPlaceholder: '',
|
||||
smsBtnText: this.$t('common.SendVerificationCode'),
|
||||
smsBtnDisabled: false,
|
||||
confirmTypeRequired: '',
|
||||
subTypeChoices: [],
|
||||
secretValue: '',
|
||||
visible: false,
|
||||
callback: null,
|
||||
cancel: null,
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showPassword() {
|
||||
return this.confirmTypeRequired === 'password'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$eventBus.$on('showConfirmDialog', this.performConfirm)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('showConfirmDialog', this.performConfirm)
|
||||
},
|
||||
methods: {
|
||||
handleSubTypeChange(val) {
|
||||
this.inputPlaceholder = this.subTypeChoices.filter(item => item.name === val)[0]?.placeholder
|
||||
this.smsWidth = val === 'sms' ? 6 : 0
|
||||
},
|
||||
performConfirm: _.throttle(function({ response, callback, cancel }) {
|
||||
if (this.processing || this.visible) {
|
||||
return
|
||||
}
|
||||
this.processing = true
|
||||
this.callback = callback
|
||||
this.cancel = cancel
|
||||
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.confirmTypeRequired = data.confirm_type
|
||||
|
||||
if (this.confirmTypeRequired === 'relogin') {
|
||||
this.$axios.post(confirmUrl, { 'confirm_type': 'relogin', 'secret_key': 'x' }).then(() => {
|
||||
this.callback()
|
||||
this.visible = false
|
||||
}).catch(() => {
|
||||
this.title = this.$t('auth.NeedReLogin')
|
||||
this.visible = true
|
||||
})
|
||||
return
|
||||
}
|
||||
this.subTypeChoices = data.content
|
||||
const defaultSubType = this.subTypeChoices.filter(item => !item.disabled)[0]
|
||||
this.subTypeSelected = defaultSubType.name
|
||||
this.inputPlaceholder = defaultSubType.placeholder
|
||||
this.visible = true
|
||||
}).catch((err) => {
|
||||
const data = err.response?.data
|
||||
const msg = data?.error || data?.detail || data?.msg || this.$t('common.GetConfirmTypeFailed')
|
||||
this.$message.error(msg)
|
||||
this.cancel(err)
|
||||
}).finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
}, 300),
|
||||
logout() {
|
||||
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
|
||||
},
|
||||
sendSMSCode() {
|
||||
this.$axios.post(`/api/v1/authentication/mfa/select/`, { type: 'sms' }).then(res => {
|
||||
this.$message.success(this.$tc('common.VerificationCodeSent'))
|
||||
let time = 60
|
||||
const interval = setInterval(() => {
|
||||
const originText = this.smsBtnText
|
||||
this.smsBtnText = this.$t('common.Pending') + `: ${time}`
|
||||
this.smsBtnDisabled = true
|
||||
time -= 1
|
||||
|
||||
if (time === 0) {
|
||||
this.smsBtnText = originText
|
||||
this.smsBtnDisabled = false
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
handleConfirm() {
|
||||
if (this.confirmTypeRequired === 'relogin') {
|
||||
return this.logout()
|
||||
}
|
||||
if (this.subTypeSelected === 'otp' && this.secretValue.length !== 6) {
|
||||
return this.$message.error(this.$tc('common.MFAErrorMsg'))
|
||||
}
|
||||
const data = {
|
||||
confirm_type: this.confirmTypeRequired,
|
||||
mfa_type: this.confirmTypeRequired === 'mfa' ? this.subTypeSelected : '',
|
||||
secret_key: this.confirmTypeRequired === 'password' ? encryptPassword(this.secretValue) : this.secretValue
|
||||
}
|
||||
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(res => {
|
||||
this.callback()
|
||||
this.secretValue = ''
|
||||
this.visible = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog-content >>> .el-dialog__footer {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dialog-content >>> .el-dialog {
|
||||
padding: 8px;
|
||||
|
||||
.el-dialog__body {
|
||||
padding-top: 30px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
width: 100%;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<IBox :fa="icon" :title="title" :type="type" v-bind="$attrs">
|
||||
<IBox :fa="icon" :type="type" :title="title" v-bind="$attrs">
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<AssetSelect ref="assetSelect" :can-select="canSelect" :disabled="disabled" />
|
||||
<AssetSelect ref="assetSelect" :disabled="disabled" :can-select="canSelect" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<el-button :disabled="disabled" :type="type" size="small" @click="addObjects">{{ $t('common.Add') }}</el-button>
|
||||
<el-button :type="type" size="small" :disabled="disabled" @click="addObjects">{{ $t('common.Add') }}</el-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -16,8 +16,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IBox from '@/components/IBox/index.vue'
|
||||
import AssetSelect from '@/components/Apps/AssetSelect/index.vue'
|
||||
import IBox from '@/components/IBox'
|
||||
import AssetSelect from '@/components/AssetSelect'
|
||||
|
||||
export default {
|
||||
name: 'AssetRelationCard',
|
||||
206
src/components/AssetSelect/index.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="asset-select-dialog">
|
||||
<Select2
|
||||
ref="select2"
|
||||
v-model="select2Config.value"
|
||||
v-bind="select2Config"
|
||||
@input="onInputChange"
|
||||
@focus.stop="handleFocus"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
<Dialog
|
||||
v-if="dialogVisible"
|
||||
:title="this.$t('assets.Assets')"
|
||||
:visible.sync="dialogVisible"
|
||||
custom-class="asset-select-dialog"
|
||||
width="80vw"
|
||||
top="1vh"
|
||||
@confirm="handleConfirm"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<TreeTable
|
||||
ref="ListPage"
|
||||
:tree-setting="treeSetting"
|
||||
:table-config="tableConfig"
|
||||
:header-actions="headerActions"
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TreeTable from '@/components/TreeTable'
|
||||
import { DetailFormatter } from '@/components/TableFormatters'
|
||||
import Select2 from '@/components/FormFields/Select2'
|
||||
import Dialog from '@/components/Dialog'
|
||||
|
||||
export default {
|
||||
componentName: 'AssetSelect',
|
||||
components: { TreeTable, Select2, Dialog },
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
canSelect: {
|
||||
type: Function,
|
||||
default(row, index) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: [Boolean, Function],
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const select2Config = {
|
||||
value: this.value,
|
||||
multiple: true,
|
||||
clearable: true,
|
||||
ajax: {
|
||||
url: '/api/v1/assets/assets/?fields_size=mini',
|
||||
transformOption: (item) => {
|
||||
return { label: item.hostname + '(' + item.ip + ')', value: item.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
const vm = this
|
||||
return {
|
||||
dialogVisible: false,
|
||||
initialValue: _.cloneDeep(this.value),
|
||||
rowSelected: [],
|
||||
initSelection: null,
|
||||
treeSetting: {
|
||||
showMenu: false,
|
||||
showRefresh: true,
|
||||
showAssets: false,
|
||||
url: '/api/v1/assets/assets/?fields_size=mini',
|
||||
nodeUrl: '/api/v1/assets/nodes/',
|
||||
// ?assets=0不显示资产. =1显示资产
|
||||
treeUrl: '/api/v1/assets/nodes/children/tree/?assets=0'
|
||||
},
|
||||
select2Config: select2Config,
|
||||
dialogSelect2Config: select2Config,
|
||||
tableConfig: {
|
||||
url: '/api/v1/assets/assets/?fields_size=mini',
|
||||
hasTree: true,
|
||||
canSelect: this.canSelect,
|
||||
columns: [
|
||||
{
|
||||
prop: 'hostname',
|
||||
label: this.$t('assets.Hostname'),
|
||||
sortable: true,
|
||||
showOverflowTooltip: true,
|
||||
formatter: DetailFormatter,
|
||||
formatterArgs: {
|
||||
route: 'AssetDetail'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'ip',
|
||||
label: this.$t('assets.ipDomain'),
|
||||
sortable: 'custom'
|
||||
},
|
||||
{
|
||||
prop: 'platform',
|
||||
label: this.$t('assets.Platform'),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
prop: 'protocols',
|
||||
formatter: function(row) {
|
||||
return <span> {row.protocols.toString()} </span>
|
||||
},
|
||||
label: this.$t('assets.Protocols')
|
||||
}
|
||||
],
|
||||
listeners: {
|
||||
'toggle-row-selection': (isSelected, row) => {
|
||||
if (isSelected) {
|
||||
vm.addRowToSelect(row)
|
||||
} else {
|
||||
vm.removeRowFromSelect(row)
|
||||
}
|
||||
}
|
||||
},
|
||||
theRowDefaultIsSelected: (row) => {
|
||||
return this.value.indexOf(row.id) > -1
|
||||
}
|
||||
},
|
||||
headerActions: {
|
||||
hasLeftActions: false,
|
||||
hasRightActions: false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFocus() {
|
||||
this.$refs.select2.selectRef.blur()
|
||||
this.dialogVisible = true
|
||||
},
|
||||
handleConfirm() {
|
||||
this.dialogVisible = false
|
||||
},
|
||||
handleCancel() {
|
||||
this.$refs.select2.iValue = this.initialValue
|
||||
this.dialogVisible = false
|
||||
},
|
||||
onInputChange(val) {
|
||||
this.$emit('change', val)
|
||||
},
|
||||
addToSelect(options, row) {
|
||||
const selectOptionsHas = options.find(item => item.value === row.id)
|
||||
// 如果select2的options中没有,那么可能无法显示正常的值
|
||||
if (selectOptionsHas === undefined) {
|
||||
const option = {
|
||||
label: `${row.hostname}(${row.ip})`,
|
||||
value: row.id
|
||||
}
|
||||
options.push(option)
|
||||
}
|
||||
},
|
||||
addRowToSelect(row) {
|
||||
const outSelectOptions = this.$refs.select2.options
|
||||
this.addToSelect(outSelectOptions, row)
|
||||
|
||||
const selectValue = this.$refs.select2.iValue
|
||||
const selectValueIndex = selectValue.indexOf(row.id)
|
||||
if (selectValueIndex === -1) {
|
||||
selectValue.push(row.id)
|
||||
}
|
||||
this.onInputChange(selectValue)
|
||||
},
|
||||
removeRowFromSelect(row) {
|
||||
const selectValue = this.$refs.select2.iValue
|
||||
const selectValueIndex = selectValue.indexOf(row.id)
|
||||
if (selectValueIndex > -1) {
|
||||
selectValue.splice(selectValueIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.el-select{
|
||||
width: 100%;
|
||||
}
|
||||
.page ::v-deep .page-heading{
|
||||
display: none;
|
||||
}
|
||||
.el-dialog__wrapper ::v-deep .el-dialog__body{
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.page ::v-deep .treebox {
|
||||
height: inherit !important;
|
||||
}
|
||||
.asset-select-dialog >>> .transition-box:first-child {
|
||||
background-color: #f3f3f3 ;
|
||||
}
|
||||
|
||||
.el-dialog__wrapper ::v-deep .el-dialog__body .wrapper-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,19 +1,15 @@
|
||||
<template>
|
||||
<DataForm
|
||||
v-if="!loading"
|
||||
:disabled="disabled"
|
||||
:fields="iFields"
|
||||
:form="value"
|
||||
style="margin-left: -26%;margin-right: -6%"
|
||||
v-bind="kwargs"
|
||||
@change="updateValue($event)"
|
||||
@input="updateValue($event)"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataForm from '@/components/Form/DataForm/index.vue'
|
||||
import DataForm from '@/components/DataForm'
|
||||
|
||||
export default {
|
||||
name: 'NestedField',
|
||||
@@ -32,16 +28,10 @@ export default {
|
||||
errors: {
|
||||
type: [Object, String],
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
formJson: JSON.stringify(this.value),
|
||||
kwargs: {
|
||||
hasReset: false,
|
||||
hasSaveContinue: false,
|
||||
@@ -70,26 +60,7 @@ export default {
|
||||
return fields
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler(val) {
|
||||
const valJson = JSON.stringify(val)
|
||||
// 如果不想等,证明是 value 自己变化导致的, 需要重新渲染
|
||||
if (valJson !== this.formJson) {
|
||||
this.loading = true
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
}, 10)
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue(val) {
|
||||
this.formJson = JSON.stringify(val)
|
||||
this.$emit('input', val)
|
||||
},
|
||||
objectToString(obj) {
|
||||
let data = ''
|
||||
// eslint-disable-next-line prefer-const
|
||||
117
src/components/AutoDataForm/index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<DataForm ref="dataForm" v-loading="loading" :fields="totalFields" :form="iForm" v-bind="$attrs" v-on="$listeners">
|
||||
<FormGroupHeader
|
||||
v-for="(group, i) in groups"
|
||||
:slot="'id:'+group.name"
|
||||
:key="'group-'+group.name"
|
||||
:group="group"
|
||||
:index="i"
|
||||
:line="i !== 0"
|
||||
/>
|
||||
</DataForm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataForm from '../DataForm'
|
||||
import FormGroupHeader from '@/components/FormGroupHeader'
|
||||
import { FormFieldGenerator } from '@/components/AutoDataForm/utils'
|
||||
export default {
|
||||
name: 'AutoDataForm',
|
||||
components: {
|
||||
DataForm,
|
||||
FormGroupHeader
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
method: {
|
||||
type: String,
|
||||
default: 'post'
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
fieldsMeta: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
remoteMeta: {},
|
||||
totalFields: [],
|
||||
loading: true,
|
||||
groups: [],
|
||||
iForm: this.form,
|
||||
errors: {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.optionUrlMetaAndGenerateColumns()
|
||||
},
|
||||
methods: {
|
||||
optionUrlMetaAndGenerateColumns() {
|
||||
this.$store.dispatch('common/getUrlMeta', { url: this.url }).then(data => {
|
||||
this.remoteMeta = data.actions[this.method.toUpperCase()] || {}
|
||||
this.generateColumns()
|
||||
this.cleanFormValue()
|
||||
}).catch(err => {
|
||||
this.$log.error(err)
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
generateColumns() {
|
||||
const generator = new FormFieldGenerator()
|
||||
this.totalFields = generator.generateFields(this.fields, this.fieldsMeta, this.remoteMeta)
|
||||
this.groups = generator.groups
|
||||
this.$log.debug('Total fields: ', this.totalFields)
|
||||
},
|
||||
_cleanFormValue(form, remoteMeta) {
|
||||
for (const [k, v] of Object.entries(remoteMeta)) {
|
||||
if (v.default === undefined) {
|
||||
continue
|
||||
}
|
||||
const valueSet = form[k]
|
||||
if (valueSet !== undefined) {
|
||||
continue
|
||||
}
|
||||
if (v.type === 'nested object' && typeof valueSet === 'object') {
|
||||
this._cleanFormValue(valueSet, v.children)
|
||||
}
|
||||
form[k] = v.default
|
||||
}
|
||||
},
|
||||
cleanFormValue() {
|
||||
this._cleanFormValue(this.iForm, this.remoteMeta)
|
||||
},
|
||||
setFieldError(name, error) {
|
||||
const field = this.totalFields.find((v) => v.prop === name)
|
||||
if (!field) {
|
||||
return
|
||||
}
|
||||
if (field.attrs.error === error) {
|
||||
error += '.'
|
||||
}
|
||||
if (field.type === 'nestedField') {
|
||||
field.el.errors = error
|
||||
} else {
|
||||
field.attrs.error = error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,36 +1,25 @@
|
||||
import Vue from 'vue'
|
||||
import ObjectSelect2 from '@/components/Form/FormFields/NestedObjectSelect2.vue'
|
||||
import NestedField from '@/components/Form/AutoDataForm/components/NestedField.vue'
|
||||
import Switcher from '@/components/Form/FormFields/Switcher.vue'
|
||||
import rules from '@/components/Form/DataForm/rules'
|
||||
import BasicTree from '@/components/Form/FormFields/BasicTree.vue'
|
||||
import JsonEditor from '@/components/Form/FormFields/JsonEditor.vue'
|
||||
import Select2 from '@/components/FormFields/Select2'
|
||||
import NestedField from '@/components/AutoDataForm/components/NestedField'
|
||||
import rules from '@/components/DataForm/rules'
|
||||
import { assignIfNot } from '@/utils/common'
|
||||
import TagInput from '@/components/Form/FormFields/TagInput.vue'
|
||||
|
||||
export class FormFieldGenerator {
|
||||
constructor(emit) {
|
||||
this.$emite = emit
|
||||
constructor() {
|
||||
this.groups = []
|
||||
}
|
||||
|
||||
generateFieldByType(type, field, fieldMeta, fieldRemoteMeta) {
|
||||
switch (type) {
|
||||
case 'labeled_choice':
|
||||
case 'choice':
|
||||
// Value 处理事在 AutoDataForm 中处理的
|
||||
if (!fieldRemoteMeta['read_only']) {
|
||||
field.options = fieldRemoteMeta.choices
|
||||
}
|
||||
type = 'radio-group'
|
||||
if (!fieldRemoteMeta.read_only) {
|
||||
field.options = fieldRemoteMeta.choices.map(v => {
|
||||
return { label: v.display_name, value: v.value }
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'multiple choice':
|
||||
field.options = fieldRemoteMeta.choices
|
||||
type = 'checkbox-group'
|
||||
break
|
||||
case 'tree':
|
||||
field.el.tree = fieldRemoteMeta.tree
|
||||
field.component = BasicTree
|
||||
field.el.choices = fieldRemoteMeta['choices']
|
||||
break
|
||||
case 'datetime':
|
||||
type = 'date-picker'
|
||||
@@ -38,20 +27,12 @@ export class FormFieldGenerator {
|
||||
type: 'datetime'
|
||||
}
|
||||
break
|
||||
case 'json':
|
||||
type = 'json-editor'
|
||||
field.component = JsonEditor
|
||||
break
|
||||
case 'field':
|
||||
type = ''
|
||||
field.component = ObjectSelect2
|
||||
field.component = Select2
|
||||
if (fieldRemoteMeta.required) {
|
||||
field.el.clearable = false
|
||||
}
|
||||
field.el.label = field.label
|
||||
// if (fieldRemoteMeta.child && fieldRemoteMeta.child.type === 'nested object') {
|
||||
// field.component = ObjectSelect2
|
||||
// }
|
||||
break
|
||||
case 'string':
|
||||
type = 'input'
|
||||
@@ -64,56 +45,40 @@ export class FormFieldGenerator {
|
||||
}
|
||||
break
|
||||
case 'boolean':
|
||||
type = ''
|
||||
field.component = Switcher
|
||||
break
|
||||
case 'list':
|
||||
type = 'input'
|
||||
field.component = TagInput
|
||||
break
|
||||
case 'object_related_field':
|
||||
field.component = ObjectSelect2
|
||||
break
|
||||
case 'm2m_related_field':
|
||||
field.component = ObjectSelect2
|
||||
field.el.label = field.label
|
||||
type = 'checkbox'
|
||||
break
|
||||
case 'nested object':
|
||||
type = 'nestedField'
|
||||
field.component = NestedField
|
||||
field.label = ''
|
||||
field.labelWidth = 0
|
||||
field.el = { ...field.el, ...fieldMeta }
|
||||
field.el.fields = this.generateNestFields(field, fieldMeta, fieldRemoteMeta)
|
||||
field.el.errors = {}
|
||||
field.hidden = () => {
|
||||
const hidden = fieldMeta['hiddenFields'] || (() => field.el.fields.length === 0)
|
||||
return hidden(fieldMeta, fieldRemoteMeta, field.el.fields)
|
||||
}
|
||||
Vue.$log.debug('All fields in generate: ', field.el.allFields)
|
||||
break
|
||||
default:
|
||||
type = 'input'
|
||||
break
|
||||
}
|
||||
// 上面重写了 type
|
||||
if (type === 'radio-group') {
|
||||
if (field.options.length > 4) {
|
||||
type = 'select'
|
||||
field.el.filterable = true
|
||||
if (!fieldRemoteMeta.read_only) {
|
||||
const options = fieldRemoteMeta.choices.map(v => {
|
||||
return { label: v.display_name, value: v.value }
|
||||
})
|
||||
if (options.length > 4) {
|
||||
type = 'select'
|
||||
field.el.filterable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
field.type = type
|
||||
return field
|
||||
}
|
||||
|
||||
generateNestFields(field, fieldMeta, fieldRemoteMeta) {
|
||||
const fields = []
|
||||
let nestedFields = fieldMeta.fields || []
|
||||
const nestedFields = fieldMeta.fields || []
|
||||
const nestedFieldsMeta = fieldMeta.fieldsMeta || {}
|
||||
const nestedFieldsRemoteMeta = fieldRemoteMeta.children || {}
|
||||
if (nestedFields === '__all__') {
|
||||
nestedFields = Object.keys(nestedFieldsRemoteMeta)
|
||||
}
|
||||
for (const name of nestedFields) {
|
||||
const f = this.generateField(name, nestedFieldsMeta, nestedFieldsRemoteMeta)
|
||||
fields.push(f)
|
||||
@@ -121,7 +86,6 @@ export class FormFieldGenerator {
|
||||
Vue.$log.debug('NestFields: ', fields)
|
||||
return fields
|
||||
}
|
||||
|
||||
generateFieldByName(name, field) {
|
||||
switch (name) {
|
||||
case 'email':
|
||||
@@ -136,7 +100,6 @@ export class FormFieldGenerator {
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
generateFieldByOther(field, fieldMeta, fieldRemoteMeta) {
|
||||
const filedRules = field.rules || []
|
||||
if (fieldRemoteMeta.required) {
|
||||
@@ -146,20 +109,15 @@ export class FormFieldGenerator {
|
||||
filedRules.push(rules.RequiredChange)
|
||||
}
|
||||
}
|
||||
// 一些 field 有 choices 但不是 choiceField
|
||||
if (fieldRemoteMeta.choices && field.type.indexOf('choice') === -1) {
|
||||
field.el.choices = fieldRemoteMeta.choices
|
||||
}
|
||||
field.rules = filedRules
|
||||
return field
|
||||
}
|
||||
|
||||
generateField(name, fieldsMeta, remoteFieldsMeta) {
|
||||
let field = { id: name, prop: name, el: {}, attrs: {}, rules: [] }
|
||||
const remoteFieldMeta = remoteFieldsMeta[name] || {}
|
||||
const fieldMeta = fieldsMeta[name] || {}
|
||||
field.label = remoteFieldMeta.label
|
||||
field.helpText = remoteFieldMeta['help_text']
|
||||
field.helpText = remoteFieldMeta.help_text
|
||||
field = this.generateFieldByType(remoteFieldMeta.type, field, fieldMeta, remoteFieldMeta)
|
||||
field = this.generateFieldByName(name, field)
|
||||
field = this.generateFieldByOther(field, fieldMeta, remoteFieldMeta)
|
||||
@@ -172,25 +130,18 @@ export class FormFieldGenerator {
|
||||
// Vue.$log.debug('Generate field: ', name, field)
|
||||
return field
|
||||
}
|
||||
|
||||
generateFieldGroup(field, fieldsMeta, remoteFieldsMeta) {
|
||||
const [groupTitle, fields] = field
|
||||
const _fields = this.generateFields(fields, fieldsMeta, remoteFieldsMeta)
|
||||
const group = {
|
||||
this.groups.push({
|
||||
id: groupTitle,
|
||||
title: groupTitle,
|
||||
fields: _fields,
|
||||
name: _fields[0]?.id
|
||||
}
|
||||
this.groups.push(group)
|
||||
return _fields
|
||||
name: fields[0],
|
||||
fields: fields
|
||||
})
|
||||
return this.generateFields(fields, fieldsMeta, remoteFieldsMeta)
|
||||
}
|
||||
|
||||
generateFields(_fields, fieldsMeta, remoteFieldsMeta) {
|
||||
let fields = []
|
||||
if (_fields === '__all__') {
|
||||
_fields = Object.keys(remoteFieldsMeta)
|
||||
}
|
||||
for (let field of _fields) {
|
||||
if (field instanceof Array) {
|
||||
const items = this.generateFieldGroup(field, fieldsMeta, remoteFieldsMeta)
|
||||
@@ -1,16 +1,10 @@
|
||||
<template>
|
||||
<span>
|
||||
<el-button v-if="shouldFold" circle class="search-btn" size="mini" @click="handleManualSearch">
|
||||
<svg-icon icon-class="search" />
|
||||
</el-button>
|
||||
<TagSearch v-else :options="iOption" v-bind="$attrs" v-on="$listeners" @tag-search="handleTagSearch" />
|
||||
</span>
|
||||
<TagSearch :options="iOption" v-bind="$attrs" v-on="$listeners" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TagSearch from '@/components/Table/TagSearch/index.vue'
|
||||
import TagSearch from '@/components/TagSearch'
|
||||
import i18n from '@/i18n/i18n'
|
||||
|
||||
export default {
|
||||
name: 'AutoDataSearch',
|
||||
components: {
|
||||
@@ -30,27 +24,16 @@ export default {
|
||||
exclude: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 建议折叠
|
||||
fold: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalOptions: [],
|
||||
tags: [],
|
||||
manualSearch: false
|
||||
internalOptions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iOption() {
|
||||
const options = this.options.concat(this.internalOptions)
|
||||
return _.uniqWith(options, _.isEqual)
|
||||
},
|
||||
shouldFold() {
|
||||
return this.fold && this.tags.length === 0 && !this.manualSearch
|
||||
return this.options.concat(this.internalOptions)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -67,19 +50,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTagSearch(tags) {
|
||||
if (_.isEqual(tags, this.tags)) {
|
||||
return
|
||||
}
|
||||
this.tags = tags
|
||||
if (tags.length === 0) {
|
||||
this.manualSearch = false
|
||||
}
|
||||
this.$emit('tagSearch', tags)
|
||||
},
|
||||
handleManualSearch() {
|
||||
this.manualSearch = true
|
||||
},
|
||||
async genericOptions() {
|
||||
const vm = this // 透传This
|
||||
vm.internalOptions = [] // 重置
|
||||
@@ -97,16 +67,16 @@ export default {
|
||||
type: field.type,
|
||||
value: name
|
||||
}
|
||||
if (['choice', 'labeled_choice'].indexOf(field.type) > -1 && field.choices) {
|
||||
if (field.type === 'choice' && field.choices) {
|
||||
option.children = field.choices.map(item => {
|
||||
if (typeof (item.value) === 'boolean') {
|
||||
if (item.value) {
|
||||
return { label: item.label, value: 'True' }
|
||||
return { label: item.display_name, value: 'True' }
|
||||
} else {
|
||||
return { label: item.label, value: 'False' }
|
||||
return { label: item.display_name, value: 'False' }
|
||||
}
|
||||
}
|
||||
return { label: item.label, value: item.value }
|
||||
return { label: item.display_name, value: item.value }
|
||||
})
|
||||
}
|
||||
if (field.type === 'boolean') {
|
||||
@@ -115,9 +85,6 @@ export default {
|
||||
{ label: i18n.t('common.No'), value: false }
|
||||
]
|
||||
}
|
||||
if (option.value === 'id') {
|
||||
option.label = 'ID'
|
||||
}
|
||||
vm.internalOptions.push(option)
|
||||
}
|
||||
},
|
||||
@@ -130,11 +97,4 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang='less' scoped>
|
||||
.search-btn {
|
||||
margin-top: 4px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-if="showColumnSettingPopover"
|
||||
:cancel-title="$tc('common.RestoreDefault')"
|
||||
:destroy-on-close="true"
|
||||
:title="$tc('common.CustomCol')"
|
||||
:title="$t('common.CustomCol')"
|
||||
:visible.sync="showColumnSettingPopover"
|
||||
:destroy-on-close="true"
|
||||
:show-cancel="false"
|
||||
width="35%"
|
||||
top="10%"
|
||||
width="50%"
|
||||
@cancel="restoreDefault()"
|
||||
@confirm="handleColumnConfirm()"
|
||||
>
|
||||
<el-alert type="success">
|
||||
@@ -24,20 +23,24 @@
|
||||
style="margin-top:5px;"
|
||||
>
|
||||
<el-checkbox
|
||||
:disabled="item.prop==='actions' || minColumns.indexOf(item.prop)!==-1"
|
||||
:label="item.prop"
|
||||
:disabled="
|
||||
item.prop==='id' ||
|
||||
item.prop==='actions' ||
|
||||
minColumns.indexOf(item.prop)!==-1
|
||||
"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</el-checkbox-group>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dialog from '@/components/Dialog/index.vue'
|
||||
|
||||
import Dialog from '@/components/Dialog/index'
|
||||
export default {
|
||||
name: 'ColumnSettingPopover',
|
||||
components: {
|
||||
@@ -79,10 +82,6 @@ export default {
|
||||
handleColumnConfirm() {
|
||||
this.showColumnSettingPopover = false
|
||||
this.$emit('columnsUpdate', { columns: this.iCurrentColumns, url: this.url })
|
||||
},
|
||||
restoreDefault() {
|
||||
this.showColumnSettingPopover = false
|
||||
this.$emit('columnsUpdate', { columns: null, url: this.url })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@
|
||||
/>
|
||||
<ColumnSettingPopover
|
||||
:current-columns="popoverColumns.currentCols"
|
||||
:min-columns="popoverColumns.minCols"
|
||||
:total-columns-list="popoverColumns.totalColumnsList"
|
||||
:min-columns="popoverColumns.minCols"
|
||||
:url="config.url"
|
||||
@columnsUpdate="handlePopoverColumnsChange"
|
||||
/>
|
||||
@@ -20,16 +20,17 @@
|
||||
</template>
|
||||
|
||||
<script type="text/jsx">
|
||||
import DataTable from '@/components/Table/DataTable/index.vue'
|
||||
import DataTable from '../DataTable'
|
||||
import {
|
||||
ActionsFormatter, ArrayFormatter, ChoicesFormatter, DateFormatter, DetailFormatter, DisplayFormatter,
|
||||
ObjectRelatedFormatter
|
||||
} from '@/components/Table/TableFormatters'
|
||||
DateFormatter,
|
||||
DetailFormatter,
|
||||
DisplayFormatter,
|
||||
ActionsFormatter,
|
||||
ChoicesFormatter
|
||||
} from '@/components/TableFormatters'
|
||||
import i18n from '@/i18n/i18n'
|
||||
import { newURL, replaceAllUUID } from '@/utils/common'
|
||||
import ColumnSettingPopover from './components/ColumnSettingPopover.vue'
|
||||
import LabelsFormatter from '@/components/Table/TableFormatters/LabelsFormatter.vue'
|
||||
|
||||
import ColumnSettingPopover from './components/ColumnSettingPopover'
|
||||
import { newURL } from '@/utils/common'
|
||||
export default {
|
||||
name: 'AutoDataTable',
|
||||
components: {
|
||||
@@ -62,12 +63,13 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
computed: {
|
||||
},
|
||||
watch: {
|
||||
config: {
|
||||
handler: function(iNew, iOld) {
|
||||
handler(iNew) {
|
||||
this.optionUrlMetaAndGenCols()
|
||||
this.$log.debug('AutoDataTable Config change found: ')
|
||||
this.$log.debug('AutoDataTable Config change found')
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
@@ -77,12 +79,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async optionUrlMetaAndGenCols() {
|
||||
if (this.config.url === '') {
|
||||
return
|
||||
}
|
||||
const url = (this.config.url.indexOf('?') === -1)
|
||||
? `${this.config.url}?draw=1&display=1`
|
||||
: `${this.config.url}&draw=1&display=1`
|
||||
if (this.config.url === '') { return }
|
||||
const url = (this.config.url.indexOf('?') === -1) ? `${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] : {}
|
||||
@@ -119,71 +117,33 @@ export default {
|
||||
case 'is_valid':
|
||||
col.label = i18n.t('common.Validity')
|
||||
col.formatter = ChoicesFormatter
|
||||
col.formatterArgs = {
|
||||
textChoices: {
|
||||
true: i18n.t('common.Yes'),
|
||||
false: i18n.t('common.No')
|
||||
}
|
||||
}
|
||||
col.width = '80px'
|
||||
break
|
||||
case 'is_active':
|
||||
col.formatter = ChoicesFormatter
|
||||
col.formatterArgs = {
|
||||
textChoices: {
|
||||
true: i18n.t('common.Active'),
|
||||
false: i18n.t('common.Inactive')
|
||||
}
|
||||
}
|
||||
col.align = 'center'
|
||||
col.width = '80px'
|
||||
break
|
||||
case 'datetime':
|
||||
case 'date_start':
|
||||
col.formatter = DateFormatter
|
||||
break
|
||||
case 'labels':
|
||||
col.formatter = LabelsFormatter
|
||||
break
|
||||
case 'comment':
|
||||
col.showOverflowTooltip = true
|
||||
}
|
||||
return col
|
||||
},
|
||||
generateColumnByType(type, col, meta) {
|
||||
generateColumnByType(type, col) {
|
||||
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.align = 'center'
|
||||
col.width = '80px'
|
||||
break
|
||||
case 'datetime':
|
||||
col.formatter = DateFormatter
|
||||
col.width = '160px'
|
||||
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
|
||||
},
|
||||
addHelpTipsIfNeed(col) {
|
||||
@@ -195,9 +155,9 @@ export default {
|
||||
return (
|
||||
<span>{column.label}
|
||||
<el-tooltip placement='bottom' effect='light' popperClass='help-tips'>
|
||||
<div slot='content' domPropsInnerHTML={helpTips}/>
|
||||
<div slot='content' domPropsInnerHTML={helpTips} />
|
||||
<el-button style='padding: 0'>
|
||||
<i class='fa fa-info-circle'/>
|
||||
<i class='fa fa-info-circle' />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
@@ -223,12 +183,12 @@ export default {
|
||||
col.filters = column.choices.map(item => {
|
||||
if (typeof (item.value) === 'boolean') {
|
||||
if (item.value) {
|
||||
return { text: item['label'], value: 'True' }
|
||||
return { text: item['display_name'], value: 'True' }
|
||||
} else {
|
||||
return { text: item['label'], value: 'False' }
|
||||
return { text: item['display_name'], value: 'False' }
|
||||
}
|
||||
}
|
||||
return { text: item['label'], value: item.value }
|
||||
return { text: item['display_name'], value: item.value }
|
||||
})
|
||||
col.sortable = false
|
||||
col['column-key'] = col.prop
|
||||
@@ -236,65 +196,22 @@ export default {
|
||||
}
|
||||
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
|
||||
},
|
||||
|
||||
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 }
|
||||
let col = { prop: name, label: colMeta.label }
|
||||
|
||||
col = this.generateColumnByName(name, col)
|
||||
col = this.generateColumnByType(colMeta.type, col, colMeta)
|
||||
col = this.setDefaultFormatterIfNeed(col)
|
||||
col = this.generateColumnByType(colMeta.type, col)
|
||||
col = Object.assign(col, customMeta)
|
||||
col = this.addHelpTipsIfNeed(col)
|
||||
col = this.addFilterIfNeed(col)
|
||||
col = this.addOrderingIfNeed(col)
|
||||
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 || []
|
||||
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) {
|
||||
for (let col of config.columns) {
|
||||
if (typeof col === 'object') {
|
||||
columns.push(col)
|
||||
} else if (typeof col === 'string') {
|
||||
@@ -302,11 +219,7 @@ export default {
|
||||
columns.push(col)
|
||||
}
|
||||
}
|
||||
|
||||
columns = columns.filter(item => {
|
||||
if (item?.showFullContent) {
|
||||
item.className = 'show-full-content'
|
||||
}
|
||||
let has = item.has
|
||||
if (has === undefined) {
|
||||
has = true
|
||||
@@ -333,14 +246,13 @@ export default {
|
||||
|
||||
// 最小列
|
||||
const minColumnsNames = _.get(this.iConfig, 'columnsShow.min', ['actions', 'id'])
|
||||
.filter(n => totalColumnsNames.includes(n))
|
||||
.filter(n => defaultColumnsNames.indexOf(n) > -1)
|
||||
|
||||
// 应该显示的列
|
||||
const _tableConfig = localStorage.getItem('tableConfig')
|
||||
? JSON.parse(localStorage.getItem('tableConfig'))
|
||||
: {}
|
||||
let tableName = this.config.name || this.$route.name + '_' + newURL(this.iConfig.url).pathname
|
||||
tableName = replaceAllUUID(tableName)
|
||||
const tableName = this.config.name || this.$route.name + '_' + newURL(this.iConfig.url).pathname
|
||||
const configShowColumnsNames = _.get(_tableConfig[tableName], 'showColumns', null)
|
||||
let showColumnsNames = configShowColumnsNames || defaultColumnsNames
|
||||
if (showColumnsNames.length === 0) {
|
||||
@@ -381,17 +293,11 @@ export default {
|
||||
},
|
||||
handlePopoverColumnsChange({ columns, url }) {
|
||||
this.$log.debug('Columns change: ', columns)
|
||||
if (columns === null) {
|
||||
columns = this.cleanedColumnsShow.default
|
||||
}
|
||||
this.popoverColumns.currentCols = columns
|
||||
const _tableConfig = localStorage.getItem('tableConfig')
|
||||
? JSON.parse(localStorage.getItem('tableConfig'))
|
||||
: {}
|
||||
let tableName = this.config.name || this.$route.name + '_' + newURL(url).pathname
|
||||
// 替换url中的uuid,避免同一个类型接口生成多个key,localStorage中的数据无法共用
|
||||
tableName = replaceAllUUID(tableName)
|
||||
|
||||
const tableName = this.config.name || this.$route.name + '_' + newURL(url).pathname
|
||||
_tableConfig[tableName] = {
|
||||
'showColumns': columns
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
<DataZTree ref="dataztree" :setting="treeSetting" class="data-z-tree" v-on="$listeners">
|
||||
<slot v-if="treeSetting.hasRightMenu" slot="rMenu">
|
||||
<li v-if="treeSetting.showCreate" id="m_create" class="rmenu" tabindex="-1" @click="createTreeNode">
|
||||
<i class="fa fa-plus-square-o" /> {{ this.$t('tree.CreateNode') }}
|
||||
<i class="fa fa-plus-square-o" /> {{ this.$t('tree.CreateNode') }}
|
||||
</li>
|
||||
<li v-if="treeSetting.showUpdate" id="m_edit" class="rmenu" tabindex="-1" @click="editTreeNode">
|
||||
<i class="fa fa-pencil-square-o" /> {{ this.$t('tree.RenameNode') }}
|
||||
<i class="fa fa-pencil-square-o" /> {{ this.$t('tree.RenameNode') }}
|
||||
</li>
|
||||
<li v-if="treeSetting.showDelete" id="m_del" class="rmenu" tabindex="-1" @click="removeTreeNode">
|
||||
<i class="fa fa-minus-square" /> {{ this.$t('tree.DeleteNode') }}
|
||||
<i class="fa fa-minus-square" /> {{ this.$t('tree.DeleteNode') }}
|
||||
</li>
|
||||
<slot name="rMenu" />
|
||||
</slot>
|
||||
@@ -16,9 +16,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DataZTree from '../DataZTree/index.vue'
|
||||
import DataZTree from '../DataZTree'
|
||||
import $ from '@/utils/jquery-vendor'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'AutoDataZTree',
|
||||
@@ -38,13 +37,9 @@ export default {
|
||||
showCreate: true,
|
||||
showDelete: true,
|
||||
showUpdate: true,
|
||||
showSearch: false,
|
||||
customTreeHeaderName: this.$t('assets.AssetTree'),
|
||||
async: {
|
||||
enable: true,
|
||||
url: (process.env.VUE_APP_ENV === 'production')
|
||||
? (`${this.setting.treeUrl}`)
|
||||
: (`${process.env.VUE_APP_BASE_API}${this.setting.treeUrl}`),
|
||||
url: (process.env.VUE_APP_ENV === 'production') ? (`${this.setting.treeUrl}`) : (`${process.env.VUE_APP_BASE_API}${this.setting.treeUrl}`),
|
||||
autoParam: ['id=key', 'name=n', 'level=lv'],
|
||||
type: 'get',
|
||||
headers: {
|
||||
@@ -57,8 +52,7 @@ export default {
|
||||
onSelected: this.onSelected.bind(this),
|
||||
beforeDrop: this.beforeDrop.bind(this),
|
||||
onDrop: this.onDrop.bind(this),
|
||||
refresh: this.refresh.bind(this),
|
||||
onAsyncSuccess: this.onAsyncSuccess.bind(this)
|
||||
refresh: this.refresh.bind(this)
|
||||
// 尚未定义的函数
|
||||
// beforeClick
|
||||
// beforeDrag
|
||||
@@ -72,9 +66,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'currentOrg'
|
||||
]),
|
||||
treeSetting() {
|
||||
this.$log.debug('Settings: ', this.setting)
|
||||
return _.merge(this.defaultSetting, this.setting)
|
||||
@@ -90,19 +81,9 @@ export default {
|
||||
$('body').unbind('mousedown')
|
||||
},
|
||||
methods: {
|
||||
onAsyncSuccess(event, treeId, treeNode, msg) {
|
||||
const nodes = JSON.parse(msg)
|
||||
nodes.forEach((node) => {
|
||||
if (treeNode.checked) {
|
||||
const currentNode = this.zTree.getNodeByParam('id', node.id, null)
|
||||
currentNode.checked = true
|
||||
this.zTree.updateNode(currentNode)
|
||||
}
|
||||
})
|
||||
},
|
||||
refreshTree: function() {
|
||||
// const refreshIconRef = $('#tree-refresh')
|
||||
// refreshIconRef.click()
|
||||
const refreshIconRef = $('#tree-refresh')
|
||||
refreshIconRef.click()
|
||||
},
|
||||
editTreeNode: function() {
|
||||
this.hideRMenu()
|
||||
@@ -131,17 +112,16 @@ export default {
|
||||
}
|
||||
let url = ''
|
||||
const query = Object.assign({}, this.$route.query)
|
||||
const objectId = treeNode.meta.data.id
|
||||
if (treeNode.meta.type === 'node') {
|
||||
this.currentNode = treeNode
|
||||
this.currentNodeId = treeNode.meta.data.id
|
||||
query['node'] = this.currentNodeId
|
||||
query['asset'] = ''
|
||||
url = `${this.setting.url}${combinator}node_id=${objectId}&show_current_asset=${show_current_asset}`
|
||||
url = `${this.setting.url}${combinator}node_id=${treeNode.meta.data.id}&show_current_asset=${show_current_asset}`
|
||||
} else if (treeNode.meta.type === 'asset') {
|
||||
query['asset'] = treeNode.meta.data?.id || treeNode.id
|
||||
query['asset'] = treeNode.meta.data.id
|
||||
query['node'] = ''
|
||||
url = `${this.setting.url}${combinator}asset_id=${query.asset}&show_current_asset=${show_current_asset}`
|
||||
url = `${this.setting.url}${combinator}asset_id=${treeNode.meta.data.id}&show_current_asset=${show_current_asset}`
|
||||
}
|
||||
this.$router.push({ query })
|
||||
this.$emit('urlChange', url)
|
||||
@@ -155,31 +135,30 @@ export default {
|
||||
this.$axios.delete(
|
||||
`${this.treeSetting.nodeUrl}${currentNode.meta.data.id}/`
|
||||
).then(() => {
|
||||
this.$message.success(this.$tc('common.deleteSuccessMsg'))
|
||||
this.$message.success(this.$t('common.deleteSuccessMsg'))
|
||||
this.zTree.removeNode(currentNode)
|
||||
this.refreshTree()
|
||||
}).catch(() => {
|
||||
// this.$message.error(this.$tc('common.deleteErrorMsg') + ' ' + error)
|
||||
// this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
|
||||
})
|
||||
},
|
||||
onRename: function(event, treeId, treeNode, isCancel) {
|
||||
const currentNodeId = this.currentNodeId || treeNode.meta.data?.id || ''
|
||||
const url = `${this.treeSetting.nodeUrl}${currentNodeId}/`
|
||||
const url = `${this.treeSetting.nodeUrl}${this.currentNodeId}/`
|
||||
if (isCancel) {
|
||||
return
|
||||
}
|
||||
this.$axios.patch(url, { 'value': treeNode.name }).then(res => {
|
||||
let assetsAmount = treeNode.meta.data['assetsAmount']
|
||||
this.$axios.patch(
|
||||
url,
|
||||
{ 'value': treeNode.name }
|
||||
).then(res => {
|
||||
let assetsAmount = treeNode.meta.data.assetsAmount
|
||||
if (!assetsAmount) {
|
||||
assetsAmount = 0
|
||||
}
|
||||
treeNode.name = treeNode.name + ' (' + assetsAmount + ')'
|
||||
treeNode.meta.data = res
|
||||
this.zTree.updateNode(treeNode)
|
||||
this.$message.success(this.$tc('common.updateSuccessMsg'))
|
||||
}).finally(() => {
|
||||
this.refreshTree()
|
||||
})
|
||||
this.$message.success(this.$t('common.updateSuccessMsg'))
|
||||
}).finally(() => { this.refreshTree() })
|
||||
},
|
||||
onBodyMouseDown: function(event) {
|
||||
const rMenuID = this.$refs.dataztree.$refs.ztree.iRMenuID
|
||||
@@ -191,7 +170,7 @@ export default {
|
||||
const rMenuID = this.$refs.dataztree.$refs.ztree.iRMenuID
|
||||
const zTreeID = this.$refs.dataztree.$refs.ztree.iZTreeID
|
||||
const offset = $(`#${zTreeID}`).offset()
|
||||
const scrollTop = document.querySelector('.treebox')?.scrollTop
|
||||
const scrollTop = document.querySelector('.treebox').scrollTop
|
||||
x -= offset.left
|
||||
// Tmp
|
||||
y -= (offset.top + scrollTop) / 3 - 10
|
||||
@@ -211,7 +190,7 @@ export default {
|
||||
return
|
||||
}
|
||||
// 屏蔽收藏资产
|
||||
if (treeNode?.id === '-12') {
|
||||
if (treeNode.id === '-12') {
|
||||
return
|
||||
}
|
||||
if (!treeNode && event.target.tagName.toLowerCase() !== 'button' && $(event.target).parents('a').length === 0) {
|
||||
@@ -219,9 +198,6 @@ export default {
|
||||
this.showRMenu('root', event.clientX, event.clientY)
|
||||
} else if (treeNode && !treeNode.noR) {
|
||||
this.zTree.selectNode(treeNode)
|
||||
if (treeNode.meta?.data?.id) {
|
||||
this.currentNodeId = treeNode.meta.data.id
|
||||
}
|
||||
this.showRMenu('node', event.clientX, event.clientY)
|
||||
}
|
||||
},
|
||||
@@ -248,10 +224,10 @@ export default {
|
||||
nodes: treeNodesIds
|
||||
}
|
||||
).then((res) => {
|
||||
this.$message.success(this.$tc('common.updateSuccessMsg'))
|
||||
this.$message.success(this.$t('common.updateSuccessMsg'))
|
||||
}).catch(error => {
|
||||
this.$message.error(this.$tc('common.updateErrorMsg' + ' ' + error))
|
||||
}).finally()
|
||||
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + error))
|
||||
}).finally(() => this.refreshTree())
|
||||
},
|
||||
createTreeNode: function() {
|
||||
this.hideRMenu()
|
||||
@@ -267,20 +243,19 @@ export default {
|
||||
id: data['key'],
|
||||
name: data['value'],
|
||||
pId: parentNode.id,
|
||||
isParent: true,
|
||||
meta: {
|
||||
data: data,
|
||||
type: 'node'
|
||||
data: data
|
||||
}
|
||||
}
|
||||
newNode.checked = this.zTree.getSelectedNodes()[0].checked
|
||||
this.zTree.addNodes(parentNode, 0, newNode)
|
||||
// vm.$refs.dataztree.refresh()
|
||||
const node = this.zTree.getNodeByParam('id', newNode.id, parentNode)
|
||||
this.currentNodeId = node.meta.data.id || newNode.id
|
||||
this.zTree.editName(node)
|
||||
this.$message.success(this.$tc('common.createSuccessMsg'))
|
||||
this.$message.success(this.$t('common.createSuccessMsg'))
|
||||
}).catch(error => {
|
||||
this.$message.error(this.$tc('common.createErrorMsg') + ' ' + error)
|
||||
this.$message.error(this.$t('common.createErrorMsg') + ' ' + error)
|
||||
})
|
||||
},
|
||||
refresh: function() {
|
||||
@@ -299,32 +274,30 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rmenu {
|
||||
font-size: 12px;
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #606266;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rmenu {
|
||||
font-size: 12px;
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #606266;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rmenu > a:hover, .dropdown-menu > a:focus {
|
||||
color: #262626;
|
||||
text-decoration: none;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.rmenu > a:hover, .dropdown-menu > a:focus {
|
||||
color: #262626;
|
||||
text-decoration: none;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.rmenu:hover{
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.rmenu:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.data-z-tree >>> .fa {
|
||||
width: 10px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
.data-z-tree >>> .fa {
|
||||
width: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
|
||||
<span v-if="item.redirect==='noRedirect' || index === levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
|
||||
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
|
||||
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
|
||||
<a v-else @click.prevent="handleLink(item)">{{ $tr( item.meta.title) }}</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
@@ -1,105 +0,0 @@
|
||||
<script type="text/jsx">
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
|
||||
export default {
|
||||
name: 'ItemValue',
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Function, Array, Object, Boolean],
|
||||
default: ''
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
formatter: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
displayValue() {
|
||||
if ([null, undefined, ''].includes(this.value)) {
|
||||
return '-'
|
||||
}
|
||||
if (typeof this.value === 'boolean') {
|
||||
return this.toChoicesDisplay(this.value)
|
||||
} else if (typeof this.value === 'object') {
|
||||
return this.value
|
||||
} else if (this.value instanceof Array) {
|
||||
return this.value.map(item => {
|
||||
if (typeof item === 'object') {
|
||||
return item.label || item.title
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
}).join(', ')
|
||||
} else if (this.isDatetime(this.value)) {
|
||||
return toSafeLocalDateStr(this.value)
|
||||
} else {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toChoicesDisplay(value) {
|
||||
if (!value) {
|
||||
return this.$t('common.No')
|
||||
}
|
||||
return this.$t('common.Yes')
|
||||
},
|
||||
isDatetime(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return false
|
||||
}
|
||||
if (value.split(' ').length !== 3) {
|
||||
return false
|
||||
}
|
||||
if (value.split(' ')[1].split(':').length !== 3) {
|
||||
return false
|
||||
}
|
||||
if (isNaN(value) && !isNaN(Date.parse(value))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
let formatterData = ''
|
||||
if (typeof this.formatter === 'function') {
|
||||
const data = this.formatter(this.item, this.value)
|
||||
if (data instanceof Promise) {
|
||||
data.then(res => {
|
||||
formatterData = res
|
||||
})
|
||||
} else {
|
||||
formatterData = data
|
||||
}
|
||||
return (
|
||||
<span>{formatterData}</span>
|
||||
)
|
||||
}
|
||||
if (this.value instanceof Array) {
|
||||
const newArr = this.value || []
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
newArr.map((item, index) => <div key={index}>{item.key}:{item.value} </div>)
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span>{this.displayValue}</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
color: var(--color-success);
|
||||
}
|
||||
</style>
|
||||
@@ -1,185 +0,0 @@
|
||||
<template>
|
||||
<DetailCard v-if="!loading && hasObject && items.length > 0" :items="items" v-bind="$attrs" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DetailCard from './index.vue'
|
||||
import { copy, toSafeLocalDateStr } from '@/utils/common'
|
||||
|
||||
export default {
|
||||
name: 'AutoDetailCard',
|
||||
components: { DetailCard },
|
||||
props: {
|
||||
object: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
excludes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
showUndefine: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
formatters: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
nested: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iObject() {
|
||||
if (this.nested) {
|
||||
return this.object[this.nested] || {}
|
||||
} else {
|
||||
return this.object
|
||||
}
|
||||
},
|
||||
hasObject() {
|
||||
return Object.keys(this.iObject).length > 0
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.optionAndGenFields()
|
||||
this.loading = false
|
||||
},
|
||||
methods: {
|
||||
defaultFormatter(fields) {
|
||||
const formatter = {}
|
||||
for (const name of fields) {
|
||||
formatter[name] = function(item, val) {
|
||||
if (val === '-') {
|
||||
return <span>{'-'}</span>
|
||||
}
|
||||
return (<span style={{ cursor: 'pointer' }} onClick={() => copy(val)}>
|
||||
{val}
|
||||
</span>)
|
||||
}
|
||||
}
|
||||
return formatter
|
||||
},
|
||||
async optionAndGenFields() {
|
||||
const data = await this.$store.dispatch('common/getUrlMeta', { url: this.url })
|
||||
let remoteMeta = data.actions['GET'] || {}
|
||||
if (this.nested) {
|
||||
remoteMeta = remoteMeta[this.nested]?.children || {}
|
||||
}
|
||||
let fields = this.fields
|
||||
fields = fields || Object.keys(remoteMeta)
|
||||
const defaultExcludes = ['org_id']
|
||||
const excludes = (this.excludes || []).concat(defaultExcludes)
|
||||
fields = fields.filter(item => !excludes.includes(item))
|
||||
const defaultFormatter = this.defaultFormatter(fields)
|
||||
for (const name of fields) {
|
||||
if (typeof name === 'object') {
|
||||
this.items.push(name)
|
||||
continue
|
||||
}
|
||||
const fieldMeta = remoteMeta[name]
|
||||
if (!fieldMeta) {
|
||||
continue
|
||||
}
|
||||
if (fieldMeta['write_only']) {
|
||||
continue
|
||||
}
|
||||
|
||||
let value = this.iObject[name]
|
||||
const label = fieldMeta.label
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (typeof value[0] === 'object') {
|
||||
const firstValue = value[0]
|
||||
if (firstValue.hasOwnProperty('name')) {
|
||||
value.forEach(item => {
|
||||
const fieldName = `${name}.${item.name}`
|
||||
if (excludes.includes(fieldName)) {
|
||||
return
|
||||
}
|
||||
this.items.push({
|
||||
key: item.label,
|
||||
value: item.value
|
||||
})
|
||||
})
|
||||
} else {
|
||||
value.forEach((item, index) => {
|
||||
const v = Object.entries(item).map(([key, value]) => `${key}:${value}`).join(', ')
|
||||
const data = { value: v }
|
||||
if (index === 0) {
|
||||
data['key'] = label
|
||||
}
|
||||
this.items.push(data)
|
||||
})
|
||||
}
|
||||
} else if (typeof value[0] === 'string') {
|
||||
value.forEach((item, index) => {
|
||||
let data = {}
|
||||
if (index === 0) {
|
||||
data = {
|
||||
key: label,
|
||||
value: value[index]
|
||||
}
|
||||
} else {
|
||||
data = {
|
||||
value: value[index]
|
||||
}
|
||||
}
|
||||
this.items.push(data)
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (value === null || value === '') {
|
||||
value = '-'
|
||||
} else if (fieldMeta.type === 'datetime') {
|
||||
value = toSafeLocalDateStr(value)
|
||||
} else if (fieldMeta.type === 'labeled_choice') {
|
||||
value = value?.['label']
|
||||
} else if (fieldMeta.type === 'related_field' || fieldMeta.type === 'nested object') {
|
||||
value = value?.['name']
|
||||
} else if (fieldMeta.type === 'm2m_related_field') {
|
||||
value = value?.map(item => item['name']).join(', ')
|
||||
} else if (fieldMeta.type === 'boolean') {
|
||||
value = value ? this.$t('common.Yes') : this.$t('common.No')
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
if (this.showUndefine) {
|
||||
value = '-'
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const item = {
|
||||
key: label,
|
||||
value: value,
|
||||
formatter: this.formatters[name] || defaultFormatter[name]
|
||||
}
|
||||
this.items.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<IBox :fa="fa" :title="title">
|
||||
<el-form class="content" label-position="left" label-width="25%">
|
||||
<el-form-item v-for="item in iItems" :key="item.key" :label="item.key">
|
||||
<ItemValue :value="item.value" class="item-value" v-bind="item" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<slot />
|
||||
</IBox>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IBox from '../../IBox/index.vue'
|
||||
import ItemValue from './ItemValue.vue'
|
||||
|
||||
export default {
|
||||
name: 'DetailCard',
|
||||
components: { IBox, ItemValue },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$t('common.BasicInfo')
|
||||
}
|
||||
},
|
||||
fa: {
|
||||
type: String,
|
||||
default: 'fa-info-circle'
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
align: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
iItems: this.items.filter(item => {
|
||||
return !item.hasOwnProperty('has') || item.has === true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-card__body {
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border-bottom: 1px dashed #EBEEF5;
|
||||
padding: 1px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
}
|
||||
|
||||
>>> .el-form-item__label {
|
||||
padding-right: 8%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
>>> .el-form-item__content {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
>>> .el-tag--mini {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-value span {
|
||||
word-break: break-word;
|
||||
}
|
||||
.content {
|
||||
font-size: 13px;
|
||||
line-height: 2.5;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane v-if="shouldHide('min')" :label="$tc('common.CronTab.min')" class="crontab-panel">
|
||||
<el-tab-pane v-if="shouldHide('min')" :label="this.$t('common.CronTab.min')">
|
||||
<CrontabMin
|
||||
ref="cronmin"
|
||||
:check="checkNumber"
|
||||
@@ -11,7 +11,7 @@
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="shouldHide('hour')" :label="$tc('common.CronTab.hour')">
|
||||
<el-tab-pane v-if="shouldHide('hour')" :label="this.$t('common.CronTab.hour')">
|
||||
<CrontabHour
|
||||
ref="cronhour"
|
||||
:check="checkNumber"
|
||||
@@ -20,7 +20,7 @@
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="shouldHide('day')" :label="$tc('common.CronTab.day')">
|
||||
<el-tab-pane v-if="shouldHide('day')" :label="this.$t('common.CronTab.day')">
|
||||
<CrontabDay
|
||||
ref="cronday"
|
||||
:check="checkNumber"
|
||||
@@ -29,7 +29,7 @@
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="shouldHide('month')" :label="$tc('common.CronTab.month')">
|
||||
<el-tab-pane v-if="shouldHide('month')" :label="this.$t('common.CronTab.month')">
|
||||
<CrontabMonth
|
||||
ref="cronmonth"
|
||||
:check="checkNumber"
|
||||
@@ -38,7 +38,7 @@
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="shouldHide('week')" :label="$tc('common.CronTab.week')">
|
||||
<el-tab-pane v-if="shouldHide('week')" :label="this.$t('common.CronTab.week')">
|
||||
<CrontabWeek
|
||||
ref="cronweek"
|
||||
:check="checkNumber"
|
||||
@@ -59,38 +59,38 @@
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.min"
|
||||
max="5"
|
||||
min="0"
|
||||
max="5"
|
||||
size="small"
|
||||
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.hour"
|
||||
size="small"
|
||||
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.day"
|
||||
size="small"
|
||||
onkeyup="value=value.replace(/[^\0-9\\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.month"
|
||||
size="small"
|
||||
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<el-input
|
||||
v-model.trim="contabValueObj.week"
|
||||
size="small"
|
||||
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
|
||||
size="mini"
|
||||
/>
|
||||
</td>
|
||||
</tbody>
|
||||
@@ -100,7 +100,7 @@
|
||||
<div style="font-size: 13px;">{{ contabValueString }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<CrontabResult :ex="contabValueString" @crontabDiffChange="crontabDiffChangeHandle" />
|
||||
<CrontabResult :ex="contabValueString" />
|
||||
|
||||
<div class="pop_btn">
|
||||
<el-button
|
||||
@@ -130,7 +130,7 @@ import CrontabWeek from './components/Crontab-Week.vue'
|
||||
import CrontabResult from './components/Crontab-Result.vue'
|
||||
|
||||
export default {
|
||||
name: 'VCrontab',
|
||||
name: 'Vcrontab',
|
||||
components: {
|
||||
CrontabMin,
|
||||
CrontabHour,
|
||||
@@ -167,8 +167,7 @@ export default {
|
||||
week: '*'
|
||||
// year: "",
|
||||
},
|
||||
newContabValueString: '',
|
||||
crontabDiff: 0
|
||||
newContabValueString: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -239,7 +238,7 @@ export default {
|
||||
updateContabValue(name, value, from) {
|
||||
this.contabValueObj[name] = value
|
||||
if (from && from !== name) {
|
||||
// debug(`来自组件 ${from} 改变了 ${name} ${value}`)
|
||||
console.log(`来自组件 ${from} 改变了 ${name} ${value}`)
|
||||
this.changeRadio(name, value)
|
||||
}
|
||||
},
|
||||
@@ -365,12 +364,6 @@ export default {
|
||||
},
|
||||
// 填充表达式
|
||||
submitFill() {
|
||||
const crontabDiffMin = this.crontabDiff / 1000 / 60
|
||||
if (crontabDiffMin > 0 && crontabDiffMin < 10) {
|
||||
const msg = this.$tc('common.crontabDiffError')
|
||||
this.$message.error(msg)
|
||||
return
|
||||
}
|
||||
this.$emit('fill', this.contabValueString)
|
||||
this.hidePopup()
|
||||
},
|
||||
@@ -388,19 +381,15 @@ export default {
|
||||
for (const j in this.contabValueObj) {
|
||||
this.changeRadio(j, this.contabValueObj[j])
|
||||
}
|
||||
},
|
||||
crontabDiffChangeHandle(diff) {
|
||||
this.crontabDiff = diff
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang='scss' scoped>
|
||||
<style scoped>
|
||||
.pop_btn {
|
||||
float: right;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.popup-main {
|
||||
position: relative;
|
||||
margin: 10px auto 0;
|
||||
@@ -409,14 +398,12 @@ export default {
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
overflow: hidden;
|
||||
line-height: 34px;
|
||||
padding-top: 6px;
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.popup-result {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
@@ -426,7 +413,6 @@ export default {
|
||||
border: 1px solid #dcdfe6;
|
||||
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 12%), 0 0 6px 0 rgb(0 0 0 / 4%);
|
||||
}
|
||||
|
||||
.popup-result .title {
|
||||
position: absolute;
|
||||
top: -17px;
|
||||
@@ -438,13 +424,11 @@ export default {
|
||||
line-height: 30px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.popup-result table {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.popup-result table span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -455,20 +439,12 @@ export default {
|
||||
overflow: hidden;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.popup-result-scroll {
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
height: 10em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.crontab-panel {
|
||||
> > > .el-input-number {
|
||||
margin: 0 5px
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item--mini.el-form-item,
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
@@ -10,28 +10,22 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<el-input-number v-model="cycle01" :max="31" :min="0" size="mini" /> -
|
||||
<el-input-number v-model="cycle02" :max="31" :min="0" size="mini" /> {{ this.$t('common.CronTab.day') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="31" /> -
|
||||
<el-input-number v-model="cycle02" :min="0" :max="31" /> {{ this.$t('common.CronTab.day') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
{{ this.$t('common.CronTab.every') }}
|
||||
<el-input-number v-model="average02" :max="31" :min="1" size="mini" /> {{ this.$t('common.CronTab.day') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
<el-input-number v-model="average02" :min="1" :max="31" /> {{ this.$t('common.CronTab.day') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="7">
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
|
||||
<el-option v-for="item in 31" :key="item" :value="item">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
@@ -51,8 +45,7 @@ export default {
|
||||
},
|
||||
check: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
}
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -191,6 +184,6 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -10,28 +10,22 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<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('common.CronTab.hour') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
|
||||
<el-input-number v-model="cycle02" :min="0" :max="60" /> {{ this.$t('common.CronTab.hour') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.every') }}
|
||||
<el-input-number v-model="average02" :max="60" :min="1" size="mini" /> {{ this.$t('common.CronTab.hour') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
<el-input-number v-model="average02" :min="1" :max="60" /> {{ this.$t('common.CronTab.hour') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
|
||||
<el-option v-for="item in 24" :key="item" :value="item-1">{{ item-1 }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
@@ -51,8 +45,7 @@ export default {
|
||||
},
|
||||
check: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
}
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -160,6 +153,6 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px
|
||||
}
|
||||
margin-bottom: 10px
|
||||
}
|
||||
</style>
|
||||
@@ -7,26 +7,26 @@
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
|
||||
<el-input-number v-model="cycle02" :min="0" :max="60" /> {{ this.$t('common.CronTab.min') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<el-input-number v-model="average02" :max="60" :min="1" size="mini" />
|
||||
{{ this.$t('common.CronTab.min') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
<el-input-number v-model="average02" :min="1" :max="60" /> {{ this.$t('common.CronTab.min') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
size="small"
|
||||
style="width:100%"
|
||||
>
|
||||
<el-option v-for="item in 60" :key="item" :value="item-1">{{ item - 1 }}</el-option>
|
||||
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%" size="small">
|
||||
<el-option v-for="item in 60" :key="item" :value="item-1">{{ item-1 }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
@@ -46,8 +46,7 @@ export default {
|
||||
},
|
||||
check: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
}
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -151,7 +150,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -10,28 +10,22 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="2">
|
||||
{{ this.$t('common.CronTab.from') }}
|
||||
<el-input-number v-model="cycle01" :max="12" :min="1" size="mini" /> -
|
||||
<el-input-number v-model="cycle02" :max="12" :min="1" size="mini" /> {{ this.$t('common.CronTab.month') }}
|
||||
<el-input-number v-model="cycle01" :min="1" :max="12" /> -
|
||||
<el-input-number v-model="cycle02" :min="1" :max="12" /> {{ this.$t('common.CronTab.month') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.every') }}
|
||||
<el-input-number v-model="average02" :max="12" :min="1" size="mini" /> {{ this.$t('common.CronTab.month') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
<el-input-number v-model="average02" :min="1" :max="12" /> {{ this.$t('common.CronTab.month') }}{{ this.$t('common.CronTab.executeOnce') }}
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="4">
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
|
||||
<el-option v-for="item in 12" :key="item" :value="item">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
@@ -51,8 +45,7 @@ export default {
|
||||
},
|
||||
check: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
}
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -165,6 +158,6 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -13,8 +13,7 @@
|
||||
|
||||
<script>
|
||||
import parser from 'cron-parser'
|
||||
import { toSafeLocalDateStr } from '@/utils/common'
|
||||
|
||||
import moment from 'moment'
|
||||
export default {
|
||||
name: 'CrontabResult',
|
||||
props: {
|
||||
@@ -47,18 +46,14 @@ export default {
|
||||
const rule = 0 + ' ' + this.$options.propsData.ex
|
||||
try {
|
||||
this.resultList = []
|
||||
const interval = parser.parseExpression(rule)
|
||||
var interval = parser.parseExpression(rule)
|
||||
for (let index = 0; index < 5; index++) {
|
||||
const cur = interval.next().toString()
|
||||
this.resultList.push(toSafeLocalDateStr(cur))
|
||||
this.resultList.push(moment(cur).format('YYYY-MM-DD HH:mm:ss'))
|
||||
}
|
||||
const first = new Date(this.resultList[0])
|
||||
const second = new Date(this.resultList[1])
|
||||
const diff = Math.abs(second - first)
|
||||
this.$emit('crontabDiffChange', diff)
|
||||
} catch (error) {
|
||||
this.isShow = false
|
||||
// debug(error, 'error')
|
||||
console.log(error, 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,24 +10,16 @@
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="3">
|
||||
{{ this.$t('common.CronTab.cycleFromWeek') }}
|
||||
<el-input-number v-model="cycle01" :max="7" :min="1" size="mini" /> -
|
||||
<el-input-number v-model="cycle02" :max="7" :min="1" size="mini" />
|
||||
<el-input-number v-model="cycle01" :min="1" :max="7" /> -
|
||||
<el-input-number v-model="cycle02" :min="1" :max="7" />
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-radio v-model="radioValue" :label="6">
|
||||
{{ this.$t('common.CronTab.appoint') }}
|
||||
<el-select
|
||||
v-model="checkboxList"
|
||||
:placeholder="$tc('common.CronTab.manyChoose')"
|
||||
clearable
|
||||
multiple
|
||||
style="width:100%"
|
||||
>
|
||||
<el-option v-for="(item,index) of weekList" :key="index" :value="index === 6 ? 0 : (index + 1)">
|
||||
{{ item }}
|
||||
</el-option>
|
||||
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
|
||||
<el-option v-for="(item,index) of weekList" :key="index" :value="index+1">{{ item }}</el-option>
|
||||
</el-select>
|
||||
</el-radio>
|
||||
</el-form-item>
|
||||
@@ -47,8 +39,7 @@ export default {
|
||||
},
|
||||
check: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
}
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -182,6 +173,6 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.el-form-item--small.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="box">
|
||||
<el-input v-model="input" clearable @clear="onClear" @focus="showDialog" />
|
||||
<el-input v-model="input" clearable @focus="showDialog" @clear="onClear" />
|
||||
</div>
|
||||
<el-dialog
|
||||
:title="$tc('common.CronTab.newCron')"
|
||||
:visible.sync="showCron"
|
||||
append-to-body
|
||||
top="8vh"
|
||||
width="650px"
|
||||
>
|
||||
<el-dialog :title="this.$t('common.CronTab.newCron')" :visible.sync="showCron" top="8vh" width="580px" append-to-body>
|
||||
<Crontab
|
||||
:expression="expression"
|
||||
@fill="crontabFill"
|
||||
@hide="showCron = false"
|
||||
@fill="crontabFill"
|
||||
/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -21,7 +15,6 @@
|
||||
|
||||
<script>
|
||||
import Crontab from './Crontab.vue'
|
||||
|
||||
export default {
|
||||
components: { Crontab },
|
||||
props: {
|
||||
@@ -58,5 +51,5 @@ export default {
|
||||
<style scoped>
|
||||
.el-dialog__body {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="grouped ? 'el-button-group' : 'el-button-ungroup'" class="layout">
|
||||
<div :class="grouped ? 'el-button-group' : 'el-button-ungroup'" style="display: flex">
|
||||
<template v-for="action in iActions">
|
||||
<el-dropdown
|
||||
v-if="action.dropdown"
|
||||
@@ -10,17 +10,12 @@
|
||||
placement="bottom-start"
|
||||
@command="handleDropdownCallback"
|
||||
>
|
||||
<el-button class="more-action" :size="size" v-bind="cleanButtonAction(action)">
|
||||
<el-button :size="size" v-bind="cleanButtonAction(action)">
|
||||
{{ action.title }}<i class="el-icon-arrow-down el-icon--right" />
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown" style="overflow: auto;max-height: 60vh">
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<template v-for="option in action.dropdown">
|
||||
<div
|
||||
v-if="option.group"
|
||||
:key="'group:'+option.name"
|
||||
class="dropdown-menu-title"
|
||||
style="width:130px"
|
||||
>
|
||||
<div v-if="option.group" :key="'group:'+option.name" class="dropdown-menu-title" style="width:130px">
|
||||
{{ option.group }}
|
||||
</div>
|
||||
<el-dropdown-item
|
||||
@@ -28,10 +23,6 @@
|
||||
:command="[option, action]"
|
||||
v-bind="option"
|
||||
>
|
||||
<span v-if="option.fa">
|
||||
<i v-if="option.fa.startsWith('fa-')" :class="'fa ' + option.fa" />
|
||||
<svg-icon v-else :icon-class="option.fa" style="font-size: 14px; margin-right: 2px; margin-left: -2px;" />
|
||||
</span>
|
||||
{{ option.title }}
|
||||
</el-dropdown-item>
|
||||
</template>
|
||||
@@ -48,11 +39,7 @@
|
||||
>
|
||||
<el-tooltip :disabled="!action.tip" :content="action.tip" placement="top">
|
||||
<span>
|
||||
<span v-if="action.fa" style="vertical-align: initial;">
|
||||
<i v-if="action.fa.startsWith('fa-')" :class="'fa ' + action.fa" />
|
||||
<svg-icon v-else :icon-class="action.fa" style="font-size: 14px;" />
|
||||
</span>
|
||||
{{ action.title }}
|
||||
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
@@ -161,12 +148,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
<style scoped>
|
||||
.dropdown-menu-title {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
@@ -183,28 +165,10 @@ export default {
|
||||
}
|
||||
|
||||
.el-button-ungroup .action-item {
|
||||
margin-left: 4px;
|
||||
margin-left: 4px
|
||||
}
|
||||
|
||||
.el-button-ungroup .action-item:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
::v-deep .more-batch-processing {
|
||||
&.el-dropdown-menu__item--divided {
|
||||
margin-top: 0;
|
||||
border-top: none;
|
||||
color: #909399;
|
||||
cursor: auto;
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
border-bottom: 1px solid #E4E7ED;
|
||||
&:before {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
&.el-dropdown-menu__item:not(.is-disabled):hover {
|
||||
color: #909399;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
v-bind="data.attrs"
|
||||
>
|
||||
<template v-if="data.helpTips" #label>
|
||||
{{ data.label }}
|
||||
<el-tooltip placement="top" effect="light" popper-class="help-tips">
|
||||
<el-tooltip placement="bottom" effect="light" popper-class="help-tips">
|
||||
<div slot="content" v-html="data.helpTips" />
|
||||
<i class="fa fa-question-circle-o" />
|
||||
<el-button style="padding: 0">
|
||||
<i class="fa fa-info-circle" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
{{ data.label }}
|
||||
</template>
|
||||
<template v-if="readonly && hasReadonlyContent">
|
||||
<div
|
||||
@@ -41,25 +43,16 @@
|
||||
<template v-for="opt in options">
|
||||
<el-option
|
||||
v-if="data.type === 'select'"
|
||||
:key="opt.label"
|
||||
:key="opt.value"
|
||||
v-bind="opt"
|
||||
/>
|
||||
<el-checkbox-button
|
||||
v-else-if="data.type === 'checkbox-group' && data.style === 'button'"
|
||||
:key="opt.value"
|
||||
v-bind="opt"
|
||||
:label="'value' in opt ? opt.value : opt.label"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</el-checkbox-button>
|
||||
|
||||
<!-- TODO: 支持 el-checkbox-button 变体 -->
|
||||
<el-checkbox
|
||||
v-else-if="data.type === 'checkbox-group' && data.style !== 'button'"
|
||||
:key="opt.value"
|
||||
v-else-if="data.type === 'checkbox-group'"
|
||||
:key="opt.label"
|
||||
v-bind="opt"
|
||||
:label="'value' in opt ? opt.value : opt.label"
|
||||
>
|
||||
{{ opt.label }}
|
||||
{{ opt.value }}
|
||||
</el-checkbox>
|
||||
<!-- WARNING: radio 用 label 属性来表示 value 的含义 -->
|
||||
<!-- FYI: radio 的 value 属性可以在没有 radio-group 时用来关联到同一个 v-model -->
|
||||
@@ -68,8 +61,7 @@
|
||||
:key="opt.label"
|
||||
v-bind="opt"
|
||||
:label="'value' in opt ? opt.value : opt.label"
|
||||
>{{ opt.label }}
|
||||
</el-radio>
|
||||
>{{ opt.label }}</el-radio>
|
||||
</template>
|
||||
</custom-component>
|
||||
<div v-if="data.helpText" class="help-block" v-html="data.helpText" />
|
||||
@@ -237,9 +229,7 @@ export default {
|
||||
.then(resp => {
|
||||
if (isOptionsCase) {
|
||||
let formRenderer = this.$parent
|
||||
while (formRenderer.$options._componentTag !== 'el-form-renderer') {
|
||||
formRenderer = formRenderer.$parent
|
||||
}
|
||||
while (formRenderer.$options._componentTag !== 'el-form-renderer') { formRenderer = formRenderer.$parent }
|
||||
formRenderer.setOptions(this.prop, resp)
|
||||
} else {
|
||||
this.propsInner = { [prop]: resp }
|
||||
@@ -146,7 +146,7 @@ export default {
|
||||
* - el-form 的 resetFields 不会触发 input & change 事件,无法监听
|
||||
* - bug1: https://github.com/FEMessage/el-data-table/issues/176#issuecomment-587280825
|
||||
* - bug2:
|
||||
* 0. 建议先在监听器 watch.value 里 // debug(v.name, oldV.name)
|
||||
* 0. 建议先在监听器 watch.value 里 console.log(v.name, oldV.name)
|
||||
* 1. 打开 basic 示例
|
||||
* 2. 在 label 为 name 的输入框里输入 1,此时 log:'1' ''
|
||||
* 3. 点击 reset 按钮,此时 log 两条数据: '1' '1', '' ''
|
||||