This commit is contained in:
OrangeM21
2020-04-10 14:54:10 +08:00
30 changed files with 722 additions and 232 deletions

View File

@@ -7,3 +7,17 @@ export function terminateSession(data) {
data: data data: data
}) })
} }
export function getSessionDetail(id) {
return request({
url: `/api/v1/terminal/sessions/${id}/`,
method: 'get'
})
}
export function getSessionCommands(id) {
return request({
url: `/api/v1/terminal/commands/?session_id=${id}`,
method: 'get'
})
}

View File

@@ -1,5 +1,5 @@
<template> <template>
<DataForm v-loading="loading" :fields="totalFields" v-bind="$attrs" v-on="$listeners"> <DataForm ref="form" v-loading="loading" :fields="totalFields" v-bind="$attrs" v-on="$listeners">
<FormGroupHeader v-for="(group, i) in groups" :slot="'id:'+group.name" :key="'group-'+group.name" :title="group.title" :line="i != 0" /> <FormGroupHeader v-for="(group, i) in groups" :slot="'id:'+group.name" :key="'group-'+group.name" :title="group.title" :line="i != 0" />
</DataForm> </DataForm>
</template> </template>
@@ -48,7 +48,6 @@ export default {
}, },
mounted() { mounted() {
this.optionUrlMeta() this.optionUrlMeta()
console.log('auto data form', this.$attrs)
}, },
methods: { methods: {
optionUrlMeta() { optionUrlMeta() {
@@ -76,47 +75,65 @@ export default {
type = '' type = ''
field.component = Select2 field.component = Select2
break break
case 'string':
type = 'input'
if (!fieldMeta.max_length) {
field.el.type = 'textarea'
}
break
default: default:
type = 'input' type = 'input'
break break
} }
if (type === 'radio-group') {
const options = fieldMeta.choices.map(v => {
return { label: v.display_name, value: v.value }
})
if (options.length > 4) {
type = 'select'
field.el.filterable = true
}
}
field.type = type field.type = type
return field return field
}, },
generateFieldByName(name, field) { generateFieldByName(name, field) {
switch (name) { switch (name) {
case 'email': case 'email':
field.el = { type: 'email' } field.el.type = 'email'
break break
case 'password': case 'password':
field.el = { type: 'password' } field.el.type = 'password'
break break
case 'comment': case 'comment':
field.el = { type: 'textarea' } field.el.type = 'textarea'
break break
} }
return field return field
}, },
generateFieldByOther(field, fieldMeta) { generateFieldByOther(field, fieldMeta) {
const filedRules = field.rules || []
if (fieldMeta.required) { if (fieldMeta.required) {
if (field.type === 'input') { if (field.type === 'input') {
field.rules = [rules.Required] filedRules.push(rules.Required)
} else { } else {
field.rules = [rules.RequiredChange] filedRules.push(rules.RequiredChange)
} }
} }
field.rules = filedRules
return field return field
}, },
generateField(name) { generateField(name) {
let field = {} let field = { id: name, prop: name, el: {}}
const fieldMeta = this.meta[name] || {} const fieldMeta = this.meta[name] || {}
field.id = name
field.prop = name
field.label = fieldMeta.label field.label = fieldMeta.label
field = this.generateFieldByType(fieldMeta.type, field, fieldMeta) field = this.generateFieldByType(fieldMeta.type, field, fieldMeta)
field = this.generateFieldByName(name, field) field = this.generateFieldByName(name, field)
field = this.generateFieldByOther(field, fieldMeta) field = this.generateFieldByOther(field, fieldMeta)
field = Object.assign(field, this.fieldsMeta[name] || {}) field = Object.assign(field, this.fieldsMeta[name] || {})
if (name === 'name') {
console.log(field)
}
return field return field
}, },
generateFieldGroup(data) { generateFieldGroup(data) {
@@ -132,9 +149,6 @@ export default {
generateFields(data) { generateFields(data) {
let fields = [] let fields = []
for (let field of data) { for (let field of data) {
console.log('is array', field instanceof Array)
console.log('is string', typeof field === 'string')
console.log('is object', field instanceof Object)
if (field instanceof Array) { if (field instanceof Array) {
const items = this.generateFieldGroup(field) const items = this.generateFieldGroup(field)
fields = [...fields, ...items] fields = [...fields, ...items]

View File

@@ -4,7 +4,7 @@
<script> <script>
import DataTable from '../DataTable' import DataTable from '../DataTable'
import { DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/ListTable/formatters/index' import { DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/ListTable/formatters'
import { optionUrlMeta } from '@/api/common' import { optionUrlMeta } from '@/api/common'
export default { export default {
name: 'AutoDataTable', name: 'AutoDataTable',
@@ -25,6 +25,11 @@ export default {
meta: {} meta: {}
} }
}, },
computed: {
expandField() {
return true
}
},
mounted() { mounted() {
this.optionUrlMeta() this.optionUrlMeta()
}, },
@@ -54,7 +59,8 @@ export default {
label: this.$tc('Actions'), label: this.$tc('Actions'),
align: 'center', align: 'center',
formatter: ActionsFormatter, formatter: ActionsFormatter,
width: '200px' width: '150px',
actions: this.config.actions || {}
} }
break break
case 'is_valid': case 'is_valid':

View File

@@ -42,7 +42,9 @@
v-else-if="data.type === 'checkbox-group'" v-else-if="data.type === 'checkbox-group'"
:key="opt.label" :key="opt.label"
v-bind="opt" v-bind="opt"
/> >
{{ opt.value }}
</el-checkbox>
<!-- WARNING: radio label 属性来表示 value 的含义 --> <!-- WARNING: radio label 属性来表示 value 的含义 -->
<!-- FYI: radio value 属性可以在没有 radio-group 时用来关联到同一个 v-model --> <!-- FYI: radio value 属性可以在没有 radio-group 时用来关联到同一个 v-model -->
<el-radio <el-radio

View File

@@ -1,6 +1,6 @@
<template> <template>
<ElFormRender <ElFormRender
ref="dataForm" ref="form"
:content="fields" :content="fields"
v-bind="$attrs" v-bind="$attrs"
:form="basicForm" :form="basicForm"
@@ -14,8 +14,8 @@
<el-form-item v-if="defaultButton"> <el-form-item v-if="defaultButton">
<slot name="button-start" /> <slot name="button-start" />
<el-button size="small" @click="resetForm('dataForm')">{{ $tc('Reset') }}</el-button> <el-button size="small" @click="resetForm('form')">{{ $tc('Reset') }}</el-button>
<el-button size="small" type="primary" @click="submitForm('dataForm')">{{ $tc('Submit') }}</el-button> <el-button size="small" type="primary" @click="submitForm('form')">{{ $tc('Submit') }}</el-button>
</el-form-item> </el-form-item>
<slot name="Actions" /> <slot name="Actions" />
</ElFormRender> </ElFormRender>
@@ -53,9 +53,10 @@ export default {
methods: { methods: {
// 获取表单数据 // 获取表单数据
submitForm(formName) { submitForm(formName) {
this.$refs[formName].validate((valid) => { const form = this.$refs[formName]
form.validate((valid) => {
if (valid) { if (valid) {
this.$emit('submit', this.$refs[formName].getFormValue()) this.$emit('submit', form.getFormValue(), form)
} else { } else {
this.$emit('invalid', valid) this.$emit('invalid', valid)
return false return false

View File

@@ -0,0 +1,37 @@
<template>
<div style="border: none; background: none;">
<span v-show="row.users.length"><label>{{ this.$t('perms.User') }}:</label>{{ format(row.users) }}<br></span>
<span v-show="row.user_groups.length"><label>{{ this.$t('perms.UserGroups') }}:</label>{{ format(row.user_groups) }}<br></span>
<span v-show="row.assets.length"><label>{{ this.$t('perms.Asset') }}:</label>{{ format(row.assets) }}<br></span>
<span v-show="row.nodes.length"><label>{{ this.$t('perms.Node') }}</label>:{{ format(row.nodes) }}<br></span>
<span v-show="row.system_users.length"><label>{{ this.$t('perms.SystemUser') }}:</label>{{ format(row.system_users) }}<br></span>
<span v-show="row.actions.length"><label>{{ this.$t('perms.Actions') }}:</label>{{ format(row.actions) }}</span>
</div>
</template>
<script>
import BaseFormatter from '@/components/ListTable/formatters/base'
export default {
name: 'ExpandAssetPermissionFormatter',
extends: BaseFormatter,
methods: {
format(val) {
if (val instanceof Array) {
return val.join(',')
}
return val
}
}
}
</script>
<style scoped>
label {
display: inline-block;
max-width: 100%;
margin: 5px;
font-weight: 700;
}
</style>

View File

@@ -0,0 +1,15 @@
<template>
<pre>{{ cellValue }}</pre>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'ExpandPreFormatter',
extends: BaseFormatter
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,21 @@
<template>
<pre style="border: none; background: none; white-space: pre-wrap">
{{ '$ '+ row.input }}
<br>
{{ row.output }}
</pre>
</template>
<script>
import BaseFormatter from '@/components/ListTable/formatters/base'
export default {
name: 'OutputExpandFormatter',
extends: BaseFormatter
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,23 @@
<template>
<el-link class="detail" :type="col.type || 'success'" @click="goDetail">{{ col.linkName }}</el-link>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'RouterFormatter',
extends: BaseFormatter,
methods: {
goDetail() {
const routeName = this.col.route || ''
this.$router.push({ name: routeName, params: { id: this.cellValue }})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,6 +1,33 @@
export { default as DetailFormatter } from './DetailFormatter' import DetailFormatter from './DetailFormatter'
export { default as DisplayFormatter } from './DisplayFormatter' import DisplayFormatter from './DisplayFormatter'
export { default as BooleanFormatter } from './ChoicesFormatter' import BooleanFormatter from './ChoicesFormatter'
export { default as ActionsFormatter } from './ActionsFormatter' import ActionsFormatter from './ActionsFormatter'
export { default as LengthFormatter } from './LengthFormatter' import ExpandPreFormatter from './ExpandPreFormatter'
import LengthFormatter from './LengthFormatter'
import RouterFormatter from './RouterFormatter'
import OutputExpandFormatter from './OutputExpandFormatter'
import ExpandAssetPermissionFormatter from './ExpandAssetPermissionFormatter'
export default {
DetailFormatter,
DisplayFormatter,
BooleanFormatter,
ActionsFormatter,
ExpandPreFormatter,
LengthFormatter,
RouterFormatter,
OutputExpandFormatter,
ExpandAssetPermissionFormatter
}
export {
DetailFormatter,
DisplayFormatter,
BooleanFormatter,
ActionsFormatter,
ExpandPreFormatter,
LengthFormatter,
RouterFormatter,
OutputExpandFormatter,
ExpandAssetPermissionFormatter
}

View File

@@ -67,7 +67,10 @@ const cn = {
'cancel': '取消', 'cancel': '取消',
'Import': '导入', 'Import': '导入',
'Export': '导出', 'Export': '导出',
'Other': '其它' 'Other': '其它',
'Create success': '创建成功',
'Deactive selected': '禁用所选',
'Active selected': '激活所选'
}, },
route: { route: {
'dashboard': '仪表盘', 'dashboard': '仪表盘',
@@ -96,6 +99,7 @@ const cn = {
'Sessions': '会话管理', 'Sessions': '会话管理',
'SessionOnline': '在线会话', 'SessionOnline': '在线会话',
'SessionOffline': '历史会话', 'SessionOffline': '历史会话',
'SessionDetail': '会话详情',
'Commands': '命令记录', 'Commands': '命令记录',
'WebTerminal': 'Web终端', 'WebTerminal': 'Web终端',
'FileManager': '文件管理', 'FileManager': '文件管理',
@@ -112,7 +116,8 @@ const cn = {
'PasswordChangeLog': '改密日志', 'PasswordChangeLog': '改密日志',
'Settings': '系统设置', 'Settings': '系统设置',
'UserCreate': '创建用户', 'UserCreate': '创建用户',
'UserGroupCreate': '创建用户组' 'UserGroupCreate': '创建用户组',
'UserUpdate': '更新用户'
}, },
// 用户模块翻译 // 用户模块翻译
users: { users: {
@@ -326,41 +331,22 @@ const cn = {
'app_path': '应用路径' 'app_path': '应用路径'
}, },
perms: { perms: {
'asset_permission': '资产授权',
'asset_permission_list': '资产授权列表',
'asset_permission_detail': '资产授权详情',
'create_asset_permission': '创建资产授权规则',
'update_asset_permission': '更新资产授权规则',
'Asset permissions': '资产授权', 'Asset permissions': '资产授权',
'name': '名称', 'RefreshPermissionCache': '刷新授权缓存',
'user': '用户', 'ReFreshSuccess': '刷新成功',
'userGroup': '用户组', 'ReFreshFail': '刷新失败',
'asset': '资产', 'All': '全部',
'node': '节点', 'Connect': '连接',
'systemUser': '系统用户', 'UpDownload': '上传下载',
'validity': '有效', 'UploadFile': '上传文件',
'action': '动作', 'DownloadFile': '下载文件',
'update': '更新',
'delete': '删除',
'search': '搜索',
'user_count': '用户数量',
'user_group_count': '用户组数量',
'asset_count': '资产数量',
'node_count': '节点数量',
'system_user_count': '系统用户数量',
'date_start': '开始日期',
'date_expired': '失效日期',
'date_created': '创建日期',
'created_by': '创建者',
'comment': '备注',
'quick_update': '快速更新',
'active': '激活中',
'users_and_user_groups': '用户或用户组',
'assets_and_node': '资产或节点',
'Basic': '基本', 'Basic': '基本',
'User': '用户', 'User': '用户',
'Asset': '资产', 'Asset': '资产',
'Actions': '动作', 'Actions': '动作',
'UserGroups': '用户组',
'Node': '节点',
'SystemUser': '系统用户',
// //
'RemoteApp': '远程应用', 'RemoteApp': '远程应用',
// //
@@ -373,30 +359,33 @@ const cn = {
'systemUser': '系统用户', 'systemUser': '系统用户',
'remoteAddr': '远端地址', 'remoteAddr': '远端地址',
'protocol': '协议', 'protocol': '协议',
'loginForm': '登录来源', 'loginFrom': '登录来源',
'command': '命令', 'command': '命令',
'dateStart': '开始日期', 'dateStart': '开始日期',
'duration': '时长', 'duration': '时长',
'terminate': '终断', 'terminate': '终断',
'date_end': '结束日期', 'dateEnd': '结束日期',
'commands': '命令记录', 'commands': '命令记录',
'replay': '回放', 'replay': '回放',
'download': '下载', 'download': '下载',
'RiskLevel': '风险等级', 'riskLevel': '风险等级',
'session': '会话', 'session': '会话',
'date': '日期', 'date': '日期',
'addr': '地址', 'addr': '地址',
'active': '激活中', 'active': '激活中',
'alive': '在线', 'alive': '在线',
'StorageConfiguration': '存储配置', 'StorageConfiguration': '存储配置',
'join': '加入' 'join': '加入',
'goto': '转到',
'sessionDetail': '会话详情',
'quickModify': '快速修改'
}, },
jobcenter: { jobcenter: {
'RunTimes': '执行次数', 'RunTimes': '执行次数',
'hosts': '主机', 'Hosts': '主机',
'success': '成功', 'Success': '成功',
'date': '日期', 'Date': '日期',
'time': '时间', 'Time': '时间',
'run': '执行' 'run': '执行'
}, },
tickets: { tickets: {

View File

@@ -18,7 +18,11 @@ const en = {
'assets': 'Assets', 'assets': 'Assets',
'applications': 'Applications', 'applications': 'Applications',
'perms': 'Perms', 'perms': 'Perms',
'sessions': 'Sessions', 'Sessions': 'Sessions',
'SessionOnline': 'Session Online',
'SessionOffline': 'Session Offline',
'SessionDetail': 'Session Detail',
'Goto': 'Goto',
'jobcenter': 'Job Center' 'jobcenter': 'Job Center'
}, },
users: { users: {
@@ -265,18 +269,16 @@ const en = {
'database_app_count': 'DatabaseApp count' 'database_app_count': 'DatabaseApp count'
}, },
sessions: { sessions: {
'session_online_list': '',
'session_detail': ' Session detail',
'id': '', 'id': '',
'user': 'Use', 'user': 'Use',
'asset': 'Asset', 'asset': 'Asset',
'system_user': 'System user', 'systemUser': 'System user',
'remote_addr': 'Remote addr', 'remoteAddr': 'Remote addr',
'protocol': 'Protocol', 'protocol': 'Protocol',
'login_form': 'Login from', 'loginFrom': 'Login from',
'command': 'Command', 'command': 'Command',
'date_start': 'Date start', 'dateStart': 'Date start',
'duration': 'Druation', 'duration': 'Duration',
'action': 'Action', 'action': 'Action',
'search': 'Search', 'search': 'Search',
'terminate_selected': 'Terminate selected', 'terminate_selected': 'Terminate selected',
@@ -284,12 +286,15 @@ const en = {
'submit': 'Submit', 'submit': 'Submit',
'terminate': 'Terminate', 'terminate': 'Terminate',
'command_list': 'Command list', 'command_list': 'Command list',
'date_end': 'Date end', 'dateEnd': 'Date end',
'quick_modify': 'Quick modify', 'quick_modify': 'Quick modify',
'terminate_session': 'Terminate session', 'terminate_session': 'Terminate session',
'confirm': 'Confirm', 'confirm': 'Confirm',
'commands': 'Commands', 'commands': 'Commands',
'join': 'join' 'join': 'join',
'goto': 'Goto',
'sessionDetail': 'Session Detail',
'quickModify': 'Quick Modify'
}, },
setting: { setting: {
'setting': 'System Setting', 'setting': 'System Setting',

View File

@@ -1,10 +1,17 @@
<template> <template>
<Page> <Page v-loading="loadding">
<IBox> <IBox>
<AutoDataForm :form="form" :fields="fields" :url="url" v-bind="$attrs" v-on="$listeners" @submit="handleSubmit"> <AutoDataForm
<slot v-for="item in fields" :slot="`id:${item}`" :name="`id:${item}`" /> v-if="!loadding"
<slot v-for="item in fields" :slot="`$id:${item}`" :name="`$id:${item}`" /> ref="form"
</AutoDataForm> :method="method"
:form="form"
:fields="fields"
:url="totalUrl"
v-bind="$attrs"
v-on="$listeners"
@submit="handleSubmit"
/>
</IBox> </IBox>
</Page> </Page>
</template> </template>
@@ -21,46 +28,105 @@ export default {
type: String, type: String,
required: true required: true
}, },
method: {
type: String,
default: 'post'
},
fields: { fields: {
type: Array, type: Array,
default: () => { default: () => {
return [] return []
} }
}, },
form: { object: {
type: Object, type: Object,
default: () => { return {} } default: () => ({})
},
initial: {
type: Object,
default: () => ({})
}, },
onSubmit: { onSubmit: {
type: Function, type: Function,
default: null default: null
},
getMethod: {
type: Function,
default: function() {
const params = this.$route.params
if (params.id) {
return 'put'
} else {
return 'post'
}
}
},
getUrl: {
type: Function,
default: function() {
const params = this.$route.params
let url = this.url
if (params.id) {
url = `${url}/${params.id}/`
}
return url
}
}
},
data() {
return {
form: {},
loadding: true
}
},
computed: {
method() {
const method = this.getMethod(this)
return method
},
totalUrl() {
return this.getUrl()
} }
}, },
mounted() { mounted() {
console.log('generic', this.$attrs) if (this.method === 'put') {
console.log(this.fields) this.getObjectDetail()
} else {
this.form = Object.assign(this.form, this.initial)
this.loadding = false
}
}, },
methods: { methods: {
handleSubmit(values) { handleSubmit(values, form) {
let handler = this.onSubmit || this.defaultOnSubmit let handler = this.onSubmit || this.defaultOnSubmit
handler = handler.bind(this) handler = handler.bind(this)
const fields = form.$refs.elForm.fields
console.log('submit', values) console.log('submit', values)
return handler(values) console.log('form.fields', fields)
return handler(values, form)
}, },
defaultOnSubmit(validValues) { defaultPerformSubmit(validValues) {
this.$axios.post(this.url, validValues).then( return this.$axios[this.method](this.totalUrl, validValues)
() => { },
defaultOnSubmit(validValues, form) {
this.defaultPerformSubmit(validValues).then(() => {
const msg = this.$tc('Create success') const msg = this.$tc('Create success')
this.$message.success(msg) this.$message.success(msg)
setTimeout(() => {
this.$router.push({ name: 'UserList' }) this.$router.push({ name: 'UserList' })
}, 500) }).catch(error => {
console.log(form)
const response = error.response
const data = response.data
if (response.status === 400) {
this.errors.name = '你报错了滴滴滴'
console.log(data)
} }
) })
},
getObjectDetail() {
this.$axios.get(this.totalUrl).then(data => {
this.form = data
}).catch(error => {
console.log(error)
}).finally(() => {
this.loadding = false
})
} }
} }
} }

View File

@@ -25,7 +25,7 @@
import Page from '../Page/' import Page from '../Page/'
import ActionsGroup from '@/components/ActionsGroup' import ActionsGroup from '@/components/ActionsGroup'
export default { export default {
name: 'BaseDetailPage', name: 'GenericDetailPage',
components: { components: {
Page, Page,
ActionsGroup ActionsGroup

View File

@@ -3,7 +3,7 @@ export { default as NavHeader } from './NavHeader'
export { default as AppMain } from './AppMain' export { default as AppMain } from './AppMain'
export { default as Page } from './Page' export { default as Page } from './Page'
export { default as TagsView } from './TagsView' export { default as TagsView } from './TagsView'
export { default as BaseDetailPage } from './BaseDetailPage' export { default as GenericDetailPage } from './GenericDetailPage'
export { default as SubMenuPage } from './SubMenuPage' export { default as SubMenuPage } from './SubMenuPage'
export { default as Footer } from './Footer' export { default as Footer } from './Footer'
export { default as IBox } from './IBox' export { default as IBox } from './IBox'

View File

@@ -70,14 +70,14 @@ export const constantRoutes = [
component: () => import('@/views/users/UserCreateUpdate.vue'), // Parent router-view component: () => import('@/views/users/UserCreateUpdate.vue'), // Parent router-view
name: 'UserCreate', name: 'UserCreate',
hidden: true, hidden: true,
meta: { title: 'UserCreate', activeMenu: '/users/users' } meta: { title: 'UserCreate', activeMenu: '/users/users', action: 'create' }
}, },
{ {
path: 'users/update/:id', path: 'users/:id/update',
component: () => import('@/views/users/UserCreateUpdate.vue'), // Parent router-view component: () => import('@/views/users/UserCreateUpdate.vue'), // Parent router-view
name: 'UserEdit', name: 'UserUpdate',
hidden: true, hidden: true,
meta: { title: 'UserEdit' } meta: { title: 'UserUpdate', activeMenu: '/users/users', action: 'update' }
}, },
{ {
path: 'users/detail/:id', path: 'users/detail/:id',
@@ -274,6 +274,13 @@ export const constantRoutes = [
component: () => import('@/views/sessions/CommandList'), component: () => import('@/views/sessions/CommandList'),
meta: { title: 'Commands' } meta: { title: 'Commands' }
}, },
{
path: 'sessions/:id',
name: 'SessionDetail',
component: () => import('@/views/sessions/SessionDetail'),
meta: { title: 'SessionDetail' },
hidden: true
},
{ {
path: 'luna', path: 'luna',
name: 'WebTerminal', name: 'WebTerminal',

View File

@@ -235,3 +235,29 @@ td .el-button.el-button--mini {
.el-radio__input.is-checked+.el-radio__label { .el-radio__input.is-checked+.el-radio__label {
color: inherit; color: inherit;
} }
.el-radio__input.is-checked .el-radio__inner {
border-color: #409EFF;
background-color: #409EFF;
}
.el-checkbox__input.is-checked .el-checkbox__inner {
border-color: #409EFF;
background-color: #409EFF;
}
.el-radio__inner:hover {
border-color: #409EFF;
}
.el-textarea__inner {
border-radius: 0;
}
.el-checkbox__input.is-checked .el-checkbox__inner, .el-checkbox__input.is-indeterminate .el-checkbox__inner {
border-color: #409EFF;
background-color: #409EFF;
}
.el-checkbox__inner:hover {
border-color: #409EFF;
}

View File

@@ -5,7 +5,6 @@
@import './sidebar.scss'; @import './sidebar.scss';
@import './element-index.css'; @import './element-index.css';
@import './menu.scss'; @import './menu.scss';
@import 'vue-select/src/scss/vue-select.scss';
@import "./font-awesome/font-awesome.min.css"; @import "./font-awesome/font-awesome.min.css";
body { body {

View File

@@ -53,17 +53,12 @@ service.interceptors.response.use(
const res = response.data const res = response.data
// if the custom code is not 20000, it is judged as an error. // if the custom code is not 20000, it is judged as an error.
if (response.status < 200 || response.status > 300) { if (response.status >= 200 && response.status < 400) {
Message({ if (response.config.raw === 1) {
message: res.message || res.error || 'Error', return response
type: 'error', }
duration: 5 * 1000 return res
}) } else if (response.status === 50008 || response.status === 50012 || response.status === 50014) {
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
// 自定义错误码
if (response.status === 50008 || response.status === 50012 || response.status === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login', confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel', cancelButtonText: 'Cancel',
@@ -73,17 +68,19 @@ service.interceptors.response.use(
location.reload() location.reload()
}) })
}) })
} } else if (response.status === 400) {
return Promise.reject(new Error(res.message || 'Error')) console.log('status is 400')
return Promise.reject(res || 'Error')
} else { } else {
if (response.config.raw === 1) { Message({
return response message: res.message || res.error || 'Error',
} type: 'error',
return res duration: 5 * 1000
})
return Promise.reject(new Error(res.message || 'Error'))
} }
}, },
error => { error => {
console.log('err' + error) // for debug
Message({ Message({
message: error.message, message: error.message,
type: 'error', type: 'error',

View File

@@ -3,8 +3,9 @@
</template> </template>
<script> <script>
import { timeOffset, toSafeLocalDateStr } from '@/utils/common'
import { GenericListPage } from '@/layout/components' import { GenericListPage } from '@/layout/components'
import { DetailFormatter, ActionsFormatter } from '@/components/ListTable/formatters/index' import { ActionsFormatter } from '@/components/ListTable/formatters/index'
export default { export default {
components: { components: {
@@ -14,47 +15,71 @@ export default {
return { return {
tableConfig: { tableConfig: {
url: '/api/v1/ops/tasks/', url: '/api/v1/ops/tasks/',
columns: [ columns: ['name', 'runtimes', 'host_amount', 'is_success', 'date_start', 'time', 'actions'],
{ columnsMeta: {
prop: 'name', name: {
label: this.$tc('Name'), label: this.$tc('Name'),
formatter: DetailFormatter, showOverflowTooltip: true
sortable: 'custom',
route: 'UserDetail'
}, },
{ runtimes: {
prop: 'latest_execution', label: this.$t('jobcenter.RunTimes'),
label: this.$t('jobcenter.RunTimes') formatter: function(row) {
const summary = <div>
<span class='text-primary'>{row.summary.success}</span>/
<span class='text-danger'>{row.summary.failed}</span>/
<span>{row.summary.total}</span>
</div>
return summary
}
}, },
{ host_amount: {
prop: 'latest_execution.hosts_amount', label: this.$t('jobcenter.Hosts'),
label: this.$t('jobcenter.hosts') formatter: function(row) {
return row.latest_execution.hosts_amount
}
}, },
{ is_success: {
prop: 'latest_execution.is_success', label: this.$t('jobcenter.Success'),
label: this.$t('jobcenter.success') formatter: row => {
if (row.latest_execution.is_success) {
return <i class='fa fa-check text-primary'/>
}
return <i class='fa fa-times text-danger'/>
}
}, },
{ date_start: {
prop: 'latest_execution.date_start', label: this.$t('jobcenter.Date'),
label: this.$t('jobcenter.date'), formatter: function(row) {
sortable: 'custom' return toSafeLocalDateStr(row.latest_execution.date_start)
}
}, },
{ time: {
prop: 'latest_execution.timedelta', label: this.$t('jobcenter.Time'),
label: this.$t('jobcenter.time') formatter: function(row) {
return timeOffset(row.latest_execution.date_start, row.latest_execution.date_finished)
}
}, },
{ actions: {
prop: 'id', prop: 'id',
label: this.$tc('Action'), label: this.$tc('Action'),
align: 'center',
formatter: ActionsFormatter, formatter: ActionsFormatter,
width: '200px', actions: {
actions: [ hasUpdate: false,
extraActions: [
{
name: 'run',
title: this.$t('jobcenter.run'),
type: 'primary',
callback: function({ cellValue, tableData }) {
// 跳转页面
const replayUrl = '/ops/celery/task/' + cellValue
window.open(replayUrl)
}
}
] ]
} }
], }
hasEdit: false, }
hasDelete: false
}, },
headerActions: { headerActions: {
hasCreate: false, hasCreate: false,

View File

@@ -11,6 +11,8 @@ export default {
data() { data() {
return { return {
form: { form: {
is_active: true,
actions: ['all', 'connect', 'updownload', 'upload_file', 'download_file'],
date_expired: '2099-12-31 00:00:00 +0800' date_expired: '2099-12-31 00:00:00 +0800'
}, },
fields: [ fields: [
@@ -53,7 +55,15 @@ export default {
} }
}, },
actions: { actions: {
label: this.$t('perms.Actions') label: this.$t('perms.Actions'),
type: 'checkbox-group',
options: [
{ label: 'all', value: this.$t('perms.All') },
{ label: 'connect', value: this.$t('perms.Connect') },
{ label: 'updownload', value: this.$t('perms.UpDownload') },
{ label: 'upload_file', value: this.$t('perms.UploadFile') },
{ label: 'download_file', value: this.$t('perms.DownloadFile') }
]
}, },
is_active: { is_active: {
type: 'checkbox' type: 'checkbox'

View File

@@ -4,7 +4,7 @@
<script> <script>
import { GenericListPage } from '@/layout/components' import { GenericListPage } from '@/layout/components'
import { LengthFormatter } from '@/components/ListTable/formatters/index' import { LengthFormatter, ExpandAssetPermissionFormatter } from '@/components/ListTable/formatters/index'
export default { export default {
components: { components: {
@@ -14,8 +14,13 @@ export default {
return { return {
tableConfig: { tableConfig: {
url: '/api/v1/perms/asset-permissions/', url: '/api/v1/perms/asset-permissions/',
columns: ['name', 'users', 'user_groups', 'assets', 'nodes', 'system_users', 'is_active', 'actions'], hasSelection: false,
columns: ['expand', 'name', 'users', 'user_groups', 'assets', 'nodes', 'system_users', 'is_active', 'actions'],
columnsMeta: { columnsMeta: {
expand: {
type: 'expand',
formatter: ExpandAssetPermissionFormatter
},
users: { users: {
formatter: LengthFormatter formatter: LengthFormatter
}, },
@@ -36,8 +41,28 @@ export default {
headerActions: { headerActions: {
hasDelete: false, hasDelete: false,
hasUpdate: false, hasUpdate: false,
createRoute: 'AssetPermissionCreate' hasBulkDelete: false,
createRoute: 'AssetPermissionCreate',
extraActions: [
{
name: 'RefreshPermissionCache',
title: this.$t('perms.RefreshPermissionCache'),
type: 'primary',
has: true,
callback: this.HandleRefreshPermissionCache
} }
]
}
}
},
methods: {
HandleRefreshPermissionCache() {
const url = '/api/v1/perms/asset-permissions/cache/refresh/'
this.$axios.get(url).then(res => {
this.$message.success(this.$t('perms.ReFreshSuccess'))
}).catch(err => {
this.$message.error(this.$t('perms.ReFreshFail') + ':' + err)
})
} }
} }
} }

View File

@@ -4,6 +4,8 @@
<script> <script>
import { GenericListPage } from '@/layout/components' import { GenericListPage } from '@/layout/components'
import { toSafeLocalDateStr } from '@/utils/common'
import { RouterFormatter, OutputExpandFormatter } from '@/components/ListTable/formatters'
export default { export default {
components: { components: {
@@ -12,55 +14,47 @@ export default {
data() { data() {
return { return {
tableConfig: { tableConfig: {
axiosConfig: {
raw: 1,
params: {
display: 1,
is_finished: 0
}
},
hasSelection: false, hasSelection: false,
hasOperation: false,
url: '/api/v1/terminal/commands/', url: '/api/v1/terminal/commands/',
columns: [ columns: [
{ 'expandCol', 'input', 'risk_level', 'user',
type: 'expand' 'asset', 'system_user', 'session', 'timestamp'
],
columnsMeta: {
expandCol: {
type: 'expand',
prop: 'output',
formatter: OutputExpandFormatter
}, },
{ input: {
prop: 'input',
label: this.$t('sessions.command') label: this.$t('sessions.command')
}, },
{ risk_level: {
prop: 'output', label: this.$t('sessions.riskLevel')
label: '命令输出结果 (怎么放到隐藏内容 )',
expand: true
}, },
{ user: {
prop: 'risk_level', label: this.$t('sessions.user')
label: this.$t('sessions.RiskLevel')
}, },
{ asset: {
prop: 'user',
label: this.$t('sessions.user'),
sortable: true
},
{
prop: 'asset',
label: this.$t('sessions.asset') label: this.$t('sessions.asset')
}, },
{ system_user: {
prop: 'system_user',
label: this.$t('sessions.systemUser') label: this.$t('sessions.systemUser')
}, },
{ session: {
prop: 'session', label: this.$t('sessions.session'),
label: this.$t('sessions.session') formatter: RouterFormatter,
route: 'SessionDetail',
linkName: this.$t('sessions.goto')
}, },
{ timestamp: {
prop: 'timestamp', label: this.$t('sessions.date'),
label: this.$t('sessions.date') formatter: function(row) {
return toSafeLocalDateStr(row.timestamp * 1000)
} }
], }
},
tableActions: { tableActions: {
hasEdit: false, hasEdit: false,
hasDelete: false hasDelete: false

View File

@@ -0,0 +1,168 @@
<template>
<GenericDetailPage :submenu="submenu" :active-menu="activeSubMenu" :title="title">
<div slot="detail">
<el-row :gutter="20">
<el-col :span="14">
<DetailCard :title="cardTitle" :items="detailItems" />
</el-col>
<el-col :span="10">
<el-card class="box-card primary">
<div slot="header" class="clearfix">
<i class="fa fa-user" />
<span>{{ cardActions }}</span>
</div>
</el-card>
</el-col>
</el-row>
</div>
<div slot="command">
<el-row :gutter="20">
<el-col :span="14">
<ListTable :table-config="tableConfig" :header-actions="headerActions" />
</el-col>
<el-col :span="10">
<el-card class="box-card primary">
<div slot="header" class="clearfix">
<i class="fa fa-user" />
<span>{{ cardActions }}</span>
</div>
</el-card>
</el-col>
</el-row>
</div>
</GenericDetailPage>
</template>
<script>
import { GenericDetailPage } from '@/layout/components'
import DetailCard from '@/components/DetailCard/index'
import ListTable from '@/components/ListTable'
import { getSessionDetail, getSessionCommands } from '@/api/sessions'
import { OutputExpandFormatter } from '@/components/ListTable/formatters'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'SessionDetail',
components: {
GenericDetailPage,
DetailCard,
ListTable
},
data() {
return {
tableConfig: {
hasSelection: false,
url: `/api/v1/terminal/commands/?session_id=${this.$route.params.id}`,
columns: [
'expandCol', 'index', 'input', 'timestamp'
],
columnsMeta: {
expandCol: {
type: 'expand',
prop: 'output',
formatter: OutputExpandFormatter
},
index: {
type: 'index'
},
input: {
label: this.$t('sessions.command')
},
timestamp: {
label: this.$t('sessions.date'),
formatter: function(row) {
return toSafeLocalDateStr(row.timestamp * 1000)
}
}
}
},
headerActions: {
hasExport: false,
hasImport: false,
hasRefresh: false,
hasCreate: false,
hasBulkDelete: false,
hasBulkUpdate: false,
hasLeftActions: false,
hasSearch: false,
hasRightActions: false
},
activeSubMenu: 'detail',
sessionData: {},
commandData: {},
submenu: [
{
title: this.$t('route.SessionDetail'),
name: 'detail'
},
{
title: this.$t('sessions.command'),
name: 'command'
}
]
}
},
computed: {
title() {
return this.$t('sessions.sessionDetail')
},
cardTitle() { return this.sessionData.id },
cardActions() {
return this.$t('sessions.quickModify')
},
detailItems() {
return [
{
key: this.$t('sessions.user'),
value: this.sessionData.user
},
{
key: this.$t('sessions.asset'),
value: this.sessionData.asset
},
{
key: this.$t('sessions.systemUser'),
value: this.sessionData.system_user
},
{
key: this.$t('sessions.protocol'),
value: this.sessionData.protocol
},
{
key: this.$t('sessions.loginFrom'),
value: this.sessionData.login_from
},
{
key: this.$t('sessions.remoteAddr'),
value: this.sessionData.remote_addr
},
{
key: this.$t('sessions.dateStart'),
value: this.sessionData.date_start
},
{
key: this.$t('sessions.dateEnd'),
value: this.sessionData.date_end
}
]
}
},
mounted() {
getSessionDetail(this.$route.params.id).then(data => {
this.sessionData = data
})
getSessionCommands(this.$route.params.id).then(data => {
this.commandData = data
})
},
methods: {
}
}
</script>
<style scoped>
</style>

View File

@@ -22,14 +22,16 @@ export default {
], ],
columnsMeta: { columnsMeta: {
index: { index: {
type: 'index', label: this.$t('sessions.id'),
label: this.$t('sessions.id') formatter: function(row, column, cellValue, index) {
return <a class='detail el-link el-link--success is-underline' href= { '/terminal/sessions/' + row.id }>{ index + 1}</a>
}
}, },
command_amount: { command_amount: {
label: this.$t('sessions.command') label: this.$t('sessions.command')
}, },
login_from: { login_from: {
label: this.$t('sessions.loginForm') label: this.$t('sessions.loginFrom')
}, },
protocol: { protocol: {
label: this.$t('sessions.protocol'), label: this.$t('sessions.protocol'),

View File

@@ -22,8 +22,10 @@ export default {
], ],
columnsMeta: { columnsMeta: {
index: { index: {
type: 'index', label: this.$t('sessions.id'),
label: this.$t('sessions.id') formatter: function(row, column, cellValue, index) {
return <a class='detail el-link el-link--success is-underline' href= { '/terminal/sessions/' + row.id }>{ index + 1}</a>
}
}, },
command_amount: { command_amount: {
label: this.$t('sessions.command') label: this.$t('sessions.command')

View File

@@ -1,10 +1,5 @@
<template> <template>
<GenericCreateUpdatePage :fields="fields" :form="form" :fields-meta="fieldsMeta" :url="url"> <GenericCreateUpdatePage :fields="fields" :initial="initial" :fields-meta="fieldsMeta" :url="url" />
<!-- <FormGroupHeader slot="id:name" title="账户" :line="false" />-->
<!-- <FormGroupHeader slot="id:password_strategy" title="认证" :line="true" />-->
<!-- <FormGroupHeader slot="id:role" title="角色安全" :line="true" />-->
<!-- <FormGroupHeader slot="id:phone" title="认证" :line="true" />-->
</GenericCreateUpdatePage>
</template> </template>
<script> <script>
@@ -14,30 +9,46 @@ export default {
GenericCreateUpdatePage GenericCreateUpdatePage
}, },
data() { data() {
const errors = { name: '' }
return { return {
form: { initial: {
password_strategy: 0, password_strategy: 0,
mfa_level: 0, mfa_level: 0,
source: 'ldap', source: 'local',
role: 'Admin', role: 'Admin',
date_expired: '2099-12-31 00:00:00 +0800' date_expired: '2099-12-31 00:00:00 +0800'
}, },
fields: [ fields: [
[this.$t('users.' + 'Account'), ['name', 'username', 'email', 'groups']], [this.$t('users.' + 'Account'), ['name', 'username', 'email', 'groups']],
[this.$t('users.' + 'Authentication'), ['password_strategy', 'password', 'mfa_level', 'source']], [this.$t('users.' + 'Authentication'), ['password_strategy', 'password', 'public_key', 'mfa_level', 'source']],
[this.$t('users.' + 'Secure'), ['role', 'date_expired']], [this.$t('users.' + 'Secure'), ['role', 'date_expired']],
[this.$tc('Other'), ['phone', 'wechat', 'comment']] [this.$tc('Other'), ['phone', 'wechat', 'comment']]
], ],
errors: errors,
url: '/api/v1/users/users/', url: '/api/v1/users/users/',
fieldsMeta: { fieldsMeta: {
name: {
el: {
error: '无措'
}
},
password_strategy: {
hidden: () => {
return this.$route.params.id
}
},
password: { password: {
hidden: (formValue, item) => { hidden: (formValue, item) => {
console.log('hidden password', formValue.password_strategy) console.log('hidden password', formValue.password_strategy)
if (this.$route.params.id === undefined) { if (this.$route.meta.action === 'update') {
return formValue.password_strategy !== 1 return false
} else {
return true
} }
return formValue.password_strategy !== 1
}
},
public_key: {
hidden: (formValue, item) => {
return this.$route.meta.action !== 'update'
} }
}, },
groups: { groups: {
@@ -49,6 +60,11 @@ export default {
} }
} }
}, },
mounted() {
setTimeout(() => {
this.errors.name = 'dididi'
}, 3000)
},
methods: { methods: {
debug() { debug() {
console.log(this) console.log(this)
@@ -58,10 +74,4 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.el-form /deep/ .el-select{
width:100%;
}
.el-form /deep/ .el-form-item__content > .el-date-editor{
width:100%;
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<BaseDetailPage :submenu="submenu" :active-menu="activeSubMenu" :title="title"> <GenericDetailPage :submenu="submenu" :active-menu="activeSubMenu" :title="title">
<div slot="info"> <div slot="info">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="14"> <el-col :span="14">
@@ -18,18 +18,18 @@
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
</BaseDetailPage> </GenericDetailPage>
</template> </template>
<script> <script>
import { getUserGroupDetail, getUserGroupMembers } from '@/api/user' import { getUserGroupDetail, getUserGroupMembers } from '@/api/user'
import { BaseDetailPage } from '@/layout/components' import { GenericDetailPage } from '@/layout/components'
import DetailCard from '@/components/DetailCard' import DetailCard from '@/components/DetailCard'
import Select2 from '@/components/Select2' import Select2 from '@/components/Select2'
export default { export default {
components: { components: {
BaseDetailPage, GenericDetailPage,
DetailCard, DetailCard,
Select2 Select2
}, },

View File

@@ -26,7 +26,8 @@ export default {
performBulkDelete: function(rows) { performBulkDelete: function(rows) {
console.log('hello') console.log('hello')
} }
} },
helpMessage: '用户列表'
} }
} }
} }

View File

@@ -14,14 +14,18 @@ export default {
tableConfig: { tableConfig: {
url: '/api/v1/users/users/', url: '/api/v1/users/users/',
columns: [ columns: [
'name', 'username', 'role', 'groups_display', 'source', 'is_active', 'actions' 'name', 'username', 'role', 'groups_display', 'source', 'is_valid', 'actions'
], ],
detailRoute: 'UserDetail' columnsMeta: {
},
detailRoute: 'UserDetail',
actions: {
updateRoute: 'UserUpdate'
}
}, },
headerActions: { headerActions: {
createRoute: 'UserCreate', createRoute: 'UserCreate',
onCreate: () => {
},
extraMoreActions: [ extraMoreActions: [
{ {
name: 'deactiveSelected', name: 'deactiveSelected',