Merge branch 'v3' of github.com:jumpserver/lina into v3

This commit is contained in:
ibuler
2022-11-11 19:41:23 +08:00
32 changed files with 1479 additions and 93 deletions

View File

@@ -58,7 +58,7 @@
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vue": "2.6.10",
"vue-codemirror-lite": "^1.0.4",
"vue-codemirror": "4.0.6",
"vue-cookie": "^1.1.4",
"vue-echarts": "^5.0.0-beta.0",
"vue-i18n": "^8.15.5",

View File

@@ -0,0 +1,76 @@
<template>
<div style="font-size: 12px">
<div
v-if="toolbar.length>0"
style="height: 100%;width: 100%;vertical-align: middle;display: inline-block;background-color: #1ab394"
>
<template v-for="(item,index) in toolbar">
<el-button v-if="item.type==='button'" :key="index" size="mini">
<i :class="item.icon" />
</el-button>
</template>
</div>
<codemirror ref="myCm" v-model="iValue" :options="cmOptions" />
</div>
</template>
<script>
import { codemirror } from 'vue-codemirror'
import 'codemirror/mode/shell/shell'
import 'codemirror/mode/powershell/powershell'
import 'codemirror/mode/python/python'
import 'codemirror/mode/ruby/ruby'
// theme css
import 'codemirror/theme/base16-dark.css'
import 'codemirror/theme/base16-light.css'
import 'codemirror/theme/idea.css'
import 'codemirror/lib/codemirror.css'
export default {
components: {
codemirror
},
props: {
toolbar: {
type: Array,
default: () => []
},
value: {
type: [String, Object],
default: () => ''
},
cmOptions: {
type: Object,
default: () => {
return {
tabSize: 4,
mode: 'shell',
theme: 'base16-dark',
lineNumbers: true,
line: true
}
}
}
},
data() {
return {}
},
computed: {
iValue: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -361,6 +361,7 @@
"TableColSettingInfo": "请选择您想显示的列表详细信息。",
"Add": "添加",
"TemplateAdd": "模版添加",
"Task": "任务",
"UpdateAssetDetail": "配置更多信息",
"AddSuccessMsg": "添加成功",
"Auth": "认证",
@@ -682,7 +683,16 @@
"PENDING": "等待中",
"RUNNING": "运行中",
"SUCCESS": "成功",
"FAILURE": "失败"
"FAILURE": "失败",
"script": "脚本列表",
"privilegeOnly": "仅选择特权账号",
"privilegeFirst": "优先选择特权账号",
"skip": "忽略当前资产",
"instantAdhoc": "即时命令",
"myAdhoc": "我的命令",
"history": "历史记录",
"createAdhoc": "创建命令",
"AdhocDetail": "命令详情"
},
"perms": {
"": "",
@@ -862,6 +872,9 @@
"GatewayUpdate": "更新网关",
"TaskCenter": "任务中心",
"JobCenter": "作业中心",
"JobList": "作业列表",
"JobCreate": "创建作业",
"JobUpdate": "更新作业",
"LabelCreate": "创建标签",
"LabelList": "标签管理",
"LabelUpdate": "更新标签",

View File

@@ -90,24 +90,120 @@ export default {
},
children: [
{
path: 'a',
name: 'CommandExecutions2',
component: () => import('@/views/ops/Command'),
path: 'adhoc',
name: 'Adhoc',
component: () => import('@/views/ops/Adhoc'),
meta: {
title: i18n.t('route.BatchCommand'),
icon: 'terminal',
permissions: []
}
},
{
path: '',
name: 'CommandExecutions',
component: () => import('@/views/ops/Command'),
path: 'command/:id',
component: () => import('@/views/ops/Adhoc/my/AdhocDetail'),
name: 'AdhocDetail',
hidden: true,
meta: {
title: i18n.t('route.AdhocDetail'),
permissions: [],
activeMenu: '/workbench/ops/adhoc'
}
},
{
path: 'command/:id/update',
name: 'AdhocUpdate',
component: () => import('@/views/ops/Adhoc/my/AdhocUpdateCreate'),
hidden: true,
meta: {
title: i18n.t('route.updateAdhoc'),
permissions: [],
activeMenu: '/workbench/ops/adhoc'
}
},
{
path: 'command/create',
name: 'AdhocCreate',
hidden: true,
component: () => import('@/views/ops/Adhoc/my/AdhocUpdateCreate'),
meta: {
title: i18n.t('ops.createAdhoc'),
permissions: [],
activeMenu: '/workbench/ops/adhoc'
}
},
{
path: 'playbook',
name: 'Playbook',
component: () => import('@/views/ops/Playbook'),
meta: {
title: i18n.t('route.BatchScript'),
icon: 'book',
permissions: []
}
},
{
path: 'flow/create',
name: 'PlaybookCreate',
hidden: true,
component: () => import('@/views/ops/Playbook/PlaybookUpdateCreate'),
meta: {
title: i18n.t('route.PlaybookCreate'),
permissions: [],
activeMenu: '/workbench/ops/playbook'
}
},
{
path: 'job',
name: 'Job',
component: empty,
redirect: '',
meta: {
title: i18n.t('route.JobList'),
permissions: []
},
children: [
{
path: '',
name: 'JobList',
component: () => import('@/views/ops/Job'),
meta: {
title: i18n.t('route.JobList'),
permissions: []
}
},
{
path: 'create',
component: () => import('@/views/ops/Job/JobUpdateCreate'),
name: 'JobCreate',
hidden: true,
meta: {
title: i18n.t('route.JobCreate'),
permissions: [],
activeMenu: '/workbench/ops/job'
}
},
{
path: 'update',
component: () => import('@/views/ops/Job/JobUpdateCreate'),
name: 'JobUpdate',
hidden: true,
meta: {
title: i18n.t('route.JobUpdate'),
permissions: [],
activeMenu: '/workbench/ops/job'
}
},
{
path: ':id',
component: () => import('@/views/ops/Job/JobDetail'),
name: 'JobDetail',
hidden: true,
meta: {
title: i18n.t('route.JobDetail'),
permissions: [],
activeMenu: '/workbench/ops/job'
}
}
]
}
]
}

View File

@@ -31,7 +31,7 @@ export default {
data() {
return {
tableConfig: {
url: `/api/v1/xpack/change-auth-plan/plan/${this.object.id}/assets/`,
url: `/api/v1/assets/automation/${this.object.id}/assets/`,
columns: [
'name', 'ip', 'delete_action'
],
@@ -51,7 +51,7 @@ export default {
formatter: DeleteActionFormatter,
onDelete: function(col, row, cellValue, reload) {
this.$axios.patch(
`/api/v1/xpack/change-auth-plan/plan/${this.object.id}/asset/remove/`,
`/api/v1/assets/automation/${this.object.id}/asset/remove/`,
{ assets: [row.id] }
).then(res => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
@@ -84,7 +84,7 @@ export default {
return this.object.assets.indexOf(row.id) === -1
},
performAdd: (items, that) => {
const relationUrl = `/api/v1/xpack/change-auth-plan/plan/${this.object.id}/asset/add/`
const relationUrl = `/api/v1/assets/automation/${this.object.id}/asset/add/`
const data = {
assets: items
}
@@ -108,7 +108,7 @@ export default {
disabled: this.$store.getters.currentOrgIsRoot,
hasObjectsId: this.object.nodes,
performAdd: (items, that) => {
const relationUrl = `/api/v1/xpack/change-auth-plan/plan/${this.object.id}/nodes/?action=add`
const relationUrl = `/api/v1/assets/automation/${this.object.id}/nodes/?action=add`
const nodes = items.map(v => v.value)
const iHasObjects = that.iHasObjects.map(v => v.value)
const data = {
@@ -126,7 +126,7 @@ export default {
const data = {
nodes: [item.value]
}
const relationUrl = `/api/v1/xpack/change-auth-plan/plan/${this.object.id}/nodes/?action=remove`
const relationUrl = `/api/v1/assets/automation/${this.object.id}/nodes/?action=remove`
return this.$axios.patch(relationUrl, data)
},
onDeleteSuccess: (obj, that) => {

View File

@@ -28,25 +28,17 @@ export default {
computed: {
detailItems() {
return [
{
key: this.$t('xpack.ChangeAuthPlan.Username'),
value: this.object.username
},
{
key: this.$t('xpack.ChangeAuthPlan.AssetAmount'),
value: this.object.assets_amount
value: this.object.snapshot.asset_amount
},
{
key: this.$t('xpack.ChangeAuthPlan.NodeAmount'),
value: this.object.nodes_amount
value: this.object.snapshot.node_amount
},
{
key: this.$t('xpack.ChangeAuthPlan.PasswordStrategy'),
value: this.object.password_strategy_display
},
{
key: this.$t('xpack.ChangeAuthPlan.TimeDelta'),
value: this.object.timedelta.toFixed(2) + 's'
value: this.object.trigger_display
},
{
key: this.$t('xpack.ChangeAuthPlan.DateStart'),

View File

@@ -19,12 +19,11 @@ export default {
}
},
data() {
const vm = this
return {
tableConfig: {
url: `/api/v1/assets/change-secret-records/?execution_id=${this.object.id}`,
columns: [
'username', 'asset', 'is_success', 'timedelta', 'date_start', 'reason_display', 'actions'
'asset', 'account', 'date_started', 'date_finished', 'status', 'error'
],
columnsMeta: {
asset: {
@@ -33,12 +32,28 @@ export default {
formatterArgs: {
can: this.$hasPerm('assets.view_asset'),
getTitle({ row }) {
return row.asset_info.name
return row.asset.name
},
getRoute({ row }) {
return {
name: 'AssetDetail',
params: { id: row.asset }
params: { id: row.asset.id }
}
}
}
},
account: {
label: this.$t('users.Username'),
formatter: DetailFormatter,
formatterArgs: {
can: this.$hasPerm('assets.view_account'),
getTitle({ row }) {
return row.account.name
},
getRoute({ row }) {
return {
name: 'AssetDetail',
params: { id: row.account.id }
}
}
}
@@ -55,28 +70,6 @@ export default {
},
reason_display: {
label: this.$t('xpack.AccountBackupPlan.Reason')
},
actions: {
formatterArgs: {
hasDelete: false,
hasUpdate: false,
hasClone: false,
extraActions: [
{
name: 'retry',
type: 'info',
title: this.$t('xpack.ChangeAuthPlan.Retry'),
can: vm.$hasPerm('xpack.change_changeauthplantask'),
callback: function({ row, tableData }) {
this.$axios.put(
`/api/v1/assets/change-secret-records/${row.id}/`,
).then(res => {
window.open(`/#/ops/celery/task/${res.task}/log/`, '_blank', 'toolbar=yes, width=900, height=600')
})
}.bind(this)
}
]
}
}
}
},

View File

@@ -21,6 +21,7 @@ export default {
return {
execution: { id: '' },
config: {
url: '/api/v1/assets/automation-executions/',
activeMenu: 'ChangeAuthPlanExecutionInfo',
actions: {
hasUpdate: false,
@@ -30,12 +31,12 @@ export default {
{
title: this.$t('common.BasicInfo'),
name: 'ChangeAuthPlanExecutionInfo',
hidden: () => !this.$hasPerm('xpack.view_changeauthplanexecution')
hidden: () => !this.$hasPerm('assets.view_automationexecution')
},
{
title: this.$t('xpack.ChangeAuthPlan.TaskList'),
name: 'ChangeAuthPlanExecutionTaskList',
hidden: () => !this.$hasPerm('xpack.view_changeauthplantask')
hidden: () => !this.$hasPerm('assets.view_changesecretrecord')
}
],
getTitle: this.getExecutionTitle

View File

@@ -18,41 +18,32 @@ export default {
}
},
data() {
console.log('this', this)
return {
tableConfig: {
url: `/api/v1/xpack/change-auth-plan/plan-execution/?plan_id=${this.object.id}`,
url: `/api/v1/assets/automation-executions/?automation_id=${this.object.id}`,
columns: [
'username', 'assets_amount', 'nodes_amount', 'result_summary', 'password_strategy_display',
'timedelta', 'trigger_display', 'date_start', 'actions'
'asset_amount', 'node_amount', 'status',
'trigger_display', 'date_start', 'actions'
],
columnsMeta: {
username: {
label: this.$t('xpack.ChangeAuthPlan.Username')
},
assets_amount: {
asset_amount: {
label: this.$t('xpack.ChangeAuthPlan.AssetAmount'),
width: '80px'
},
nodes_amount: {
label: this.$t('xpack.ChangeAuthPlan.NodeAmount'),
width: '80px'
},
result_summary: {
label: this.$t('xpack.ChangeAuthPlan.Result'),
width: '80px',
showOverflowTooltip: true,
formatter: function(row) {
const summary = <div>
<span class='text-primary'>{row.result_summary.succeed}</span>/
<span class='text-danger'>{row.result_summary.failed}</span>/
<span>{row.result_summary.total}</span>
</div>
return summary
return <span>{ row.snapshot.asset_amount }</span>
}
},
password_strategy_display: {
label: this.$t('xpack.ChangeAuthPlan.PasswordStrategy'),
width: '220px',
node_amount: {
label: this.$t('xpack.ChangeAuthPlan.NodeAmount'),
width: '80px',
formatter: function(row) {
return <span>{ row.snapshot.node_amount }</span>
}
},
status: {
label: this.$t('xpack.ChangeAuthPlan.Result'),
width: '80px',
showOverflowTooltip: true
},
timedelta: {

View File

@@ -34,12 +34,12 @@ export default {
attrs: {
type: 'primary',
label: this.$t('xpack.ChangeAuthPlan.Execute'),
disabled: !this.$hasPerm('xpack.add_changeauthplanexecution')
disabled: !this.$hasPerm('assets.add_automationexecution')
},
callbacks: {
click: function() {
this.$axios.post(
`/api/v1/xpack/change-auth-plan/plan-execution/`,
`/api/v1/assets/automation-executions/`,
{ plan: this.object.id }
).then(res => {
window.open(`/#/ops/celery/task/${res.task}/log/`, '_blank', 'toolbar=yes, width=900, height=600')
@@ -59,19 +59,19 @@ export default {
},
{
key: this.$t('xpack.ChangeAuthPlan.Username'),
value: this.object.username
value: this.object.accounts.join(', ')
},
{
key: this.$t('xpack.ChangeAuthPlan.AssetAmount'),
value: this.object.assets_amount
value: this.object.assets.length
},
{
key: this.$t('xpack.ChangeAuthPlan.NodeAmount'),
value: this.object.nodes_amount
value: this.object.nodes.length
},
{
key: this.$t('xpack.ChangeAuthPlan.PasswordStrategy'),
value: this.object.password_strategy_display
value: this.object.secret_strategy.label
},
{
key: this.$t('xpack.ChangeAuthPlan.RegularlyPerform'),

View File

@@ -24,21 +24,22 @@ export default {
plan: { name: '', username: '', comment: '' },
config: {
activeMenu: 'ChangeAuthPlanInfo',
url: '/api/v1/assets/change-secret-automations/',
submenu: [
{
title: this.$t('common.BasicInfo'),
name: 'ChangeAuthPlanInfo',
hidden: () => !this.$hasPerm('xpack.view_changeauthplan')
hidden: () => !this.$hasPerm('assets.view_changesecretautomation')
},
{
title: this.$t('xpack.ChangeAuthPlan.AssetAndNode'),
name: 'ChangeAuthPlanAsset',
hidden: () => !this.$hasPerm('xpack.change_changeauthplan')
hidden: () => !this.$hasPerm('assets.change_changesecretautomation')
},
{
title: this.$t('xpack.ChangeAuthPlan.ExecutionList'),
name: 'ChangeAuthPlanExecutionList',
hidden: () => !this.$hasPerm('xpack.view_changeauthplanexecution')
hidden: () => !this.$hasPerm('assets.view_automationexecution')
}
]
}

View File

@@ -92,7 +92,10 @@ export default {
callback: function({ row }) {
this.$axios.post(
`/api/v1/assets/automation-executions/`,
{ plan: row.id }
{
automation: row.id,
type: row.type
}
).then(res => {
openTaskPage(res['task'])
})

View File

@@ -126,8 +126,7 @@ export const getFields = () => {
},
accounts: {
label: i18n.t('common.Username'),
component: TagInput,
helpText: i18n.t('xpack.ChangeAuthPlan.HelpText.UsernameOfCreateUpdatePage')
component: TagInput
},
secret: {
hidden: ({ secret_strategy, secret_type }) => (secret_strategy !== 'specific' || secret_type !== 'password')

View File

@@ -7,6 +7,7 @@
:fields-meta="fieldsMeta"
:clean-form-value="cleanFormValue"
:after-get-form-value="afterGetFormValue"
:after-get-remote-meta="handleAfterGetRemoteMeta"
/>
</div>
</template>
@@ -74,6 +75,9 @@ export default {
}
},
methods: {
handleAfterGetRemoteMeta(meta) {
this.fieldsMeta.su_method.options = meta?.su_method?.choices || []
},
async setCategories() {
const category = this.$route.query.category
const type = this.$route.query.type

View File

@@ -72,6 +72,7 @@ export const platformFieldsMeta = (vm) => {
},
su_method: {
type: 'select',
options: [],
hidden: (form) => !form['su_enabled']
}
}

View File

@@ -0,0 +1,278 @@
<template>
<el-collapse-transition>
<div style="display: flex;justify-items: center; flex-wrap: nowrap;justify-content:space-between;">
<el-form ref="form" label-width="80px">
<el-form-item label="选择资产">
<el-input />
</el-form-item>
<el-form-item label="选择账号">
<el-input />
</el-form-item>
</el-form>
<div :style="iShowTree?('display: flex;width: 80%;'):('display: flex;width:100%;')">
<IBox class="transition-box" style="width: calc(100% - 17px);">
<div style="margin-bottom: 20px">
<CodeEditor />
</div>
Output
<Term ref="xterm" />
</IBox>
</div>
</div>
</el-collapse-transition>
</template>
<script>
import Term from '@/components/Term'
import IBox from '@/components/IBox'
import CodeEditor from '@/components/FormFields/CodeEditor'
export default {
name: 'CommandExecution',
components: {
Term,
IBox,
CodeEditor
},
data() {
return {
languageOptions: [
{
label: 'Shell',
value: 'shell'
},
{
label: 'Python',
value: 'python'
},
{
label: 'Ruby',
value: 'ruby'
},
{
label: 'PowerShell',
value: 'powershell'
}
],
DataZTree: 0,
codeMirrorOptions: {
lineNumbers: true,
lineWrapping: true,
mode: 'shell'
},
treeSetting: {
treeUrl: '/api/v1/perms/users/nodes-with-assets/tree/',
showRefresh: true,
showMenu: false,
showSearch: true,
customTreeHeader: true,
check: {
enable: true
},
view: {
dblClickExpand: false,
showLine: true
},
data: {
simpleData: {
enable: true
}
},
edit: {
enable: true,
showRemoveBtn: false,
showRenameBtn: false,
drag: {
isCopy: true,
isMove: true
}
},
callback: {
onCheck: this.onCheck.bind(this)
},
async: {
enable: false
}
},
iShowTree: true,
actions: '',
options: [],
ws: '',
wsConnected: false
}
},
computed: {
codemirror() {
return this.$refs.myCm.codemirror
},
zTree() {
return this.$refs.AutoDataZTree.$refs.dataztree.$refs.ztree.zTree
},
xterm() {
return this.$refs.xterm.xterm
}
},
mounted() {
this.fetchAccountUsernames()
this.xterm.write(this.$t('ops.selectAssetsMessage'))
this.enableWS()
},
methods: {
fetchAccountUsernames() {
const temp = {}
this.$axios.get('/api/v1/assets/accounts/',).then(data => {
data.forEach((item) => {
temp[item.username] = {}
})
for (const key of Object.keys(temp)) {
this.accountConfig.usernameOptions.push({ 'label': key, 'value': key })
}
})
},
onChangeLanguage() {
this.codemirror.setOption('mode', this.scriptMeta.language)
},
onOpenScriptListDialog() {
this.showScriptListDialog = true
},
onOpenScriptSaveDialog() {
this.showScriptAddDialog = true
},
onOpenAccountPolicyDialog() {
this.showAccountPolicyDialog = true
},
onSelectScript(scriptId) {
this.$axios.get(`/api/v1/ops/scripts/${scriptId}`).then((data) => {
this.scriptMeta.content = data['content']
})
},
onSelectAccountPolicy(policy) {
this.accountConfig.accountPolicy = policy
},
getSelectedAssetsNode() {
const nodes = this.$refs.AutoDataZTree.$refs.dataztree.$refs.ztree.getCheckedNodes()
const assetsNode = []
nodes.forEach(function(node) {
if (node.meta.type === 'asset' && !node.isHidden) {
assetsNode.push(node)
}
})
return assetsNode
},
onCheck(e, treeId, treeNode) {
const nodes = this.getSelectedAssetsNode()
const nodes_names = nodes.map(function(node) {
return node.name
})
let message = this.$t('ops.selectedAssets')
message += nodes_names.join(', ')
message += '\r\n'
message += this.$t('ops.inTotal') + `${nodes_names.length} \r\n`
this.xterm.clear()
this.xterm.write(message)
},
enableWS() {
const scheme = document.location.protocol === 'https:' ? 'wss' : 'ws'
const port = document.location.port ? ':' + document.location.port : ''
const url = '/ws/ops/tasks/log/'
const wsURL = scheme + '://' + document.location.hostname + port + url
this.ws = new WebSocket(wsURL)
this.ws.onerror = (e) => {
this.xterm.write(this.wrapperError('Connect websocket server error'))
}
this.setWsCallback()
},
setWsCallback() {
this.ws.onmessage = (e) => {
const data = JSON.parse(e.data)
let message = data.message
message = message.replace(/Task ops\.tasks\.run_command_execution.*/, '')
this.xterm.write(message)
}
},
wrapperError(msg) {
return `\r\n${msg}\r\n`
},
writeExecutionOutput(taskId) {
let msg = this.$t('assets.Pending')
this.xterm.write(msg)
msg = JSON.stringify({ task: taskId })
this.ws.send(msg)
},
execute() {
const size = 'rows=' + this.xterm.rows + '&cols=' + this.xterm.cols
const url = '/api/v1/ops/command-executions/?' + size
const runAs = this.accountConfig.selectedUsername
const command = this.scriptMeta.content
const hosts = this.getSelectedAssetsNode().map(function(node) {
return node.id
})
if (hosts.length === 0) {
this.xterm.write(this.wrapperError(this.$t('assets.UnselectedAssets')))
return
}
if (!command) {
this.xterm.write(this.wrapperError(this.$t('assets.NoInputCommand')))
return
}
if (!runAs) {
this.xterm.write(this.wrapperError(this.$t('assets.NoSystemUserWasSelected')))
return
}
const data = {
hosts: hosts,
run_as: runAs,
command: command
}
this.$axios.post(
url, data
).then(res => {
this.writeExecutionOutput(res.id)
})
}
}
}
</script>
<style lang="scss" scoped>
> > > .el-input input {
height: 100%;
}
.mini-button {
width: 12px;
float: right;
text-align: center;
padding: 5px 0;
background-color: var(--color-primary);
border-color: var(--color-primary);
color: #FFFFFF;
border-radius: 3px;
}
.el-tree {
background-color: inherit !important;
}
.mini {
margin-right: 5px;
width: 12px !important;
}
.auto-data-ztree {
overflow: auto;
/*border-right: solid 1px red;*/
}
.vue-codemirror-wrap ::v-deep .CodeMirror {
//width: calc(100% - 17px);
height: 480px;
border: 1px solid #eee;
}
.tree-box {
margin-right: 2px;
border: 1px solid #e0e0e0;
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<GenericListTable :table-config="tableConfig" :header-actions="headerActions" />
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import { ActionsFormatter } from '@/components/TableFormatters'
export default {
components: {
GenericListTable
},
data() {
return {
tableConfig: {
url: '/api/v1/ops/adhocs/',
columns: [
'name', 'module', 'date_updated', 'date_created', 'actions'
],
columnsMeta: {
name: {
formatterArgs: {
can: true,
route: 'AdhocDetail'
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: true,
canUpdate: true,
updateRoute: 'AdhocUpdate',
hasDelete: true,
canDelete: true,
hasClone: false,
extraActions: [
{
title: this.$t('run'),
name: 'run',
type: 'running',
can: true,
callback: ({ row }) => {
this.$router.push({ name: 'JobCreate', query: { type: 'adhoc', id: row.id }})
}
}
]
}
}
}
},
headerActions: {
canCreate: true,
createRoute: 'AdhocCreate',
hasRefresh: true,
hasExport: false,
hasImport: false,
hasMoreActions: false
}
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,51 @@
<template>
<TabPage
:submenu="submenu"
:active-menu.sync="activeMenu"
>
<component :is="activeMenu" :query="{type:'adhoc'}" />
</TabPage>
</template>
<script>
import { TabPage } from '@/layout/components'
import MyAdhoc from '@/views/ops/Adhoc/MyAdhoc'
import InstantAdhoc from '@/views/ops/Adhoc/InstantAdhoc'
import JobHistory from '@/views/ops/Job/JobDetail/JobHistory'
export default {
name: 'Index',
components: {
TabPage,
MyAdhoc,
InstantAdhoc,
JobHistory
},
data() {
return {
activeMenu: 'MyAdhoc',
submenu: [
{
title: this.$t('ops.instantAdhoc'),
name: 'InstantAdhoc'
},
{
title: this.$t('ops.myAdhoc'),
name: 'MyAdhoc'
},
{
title: this.$t('ops.history'),
name: 'JobHistory'
}
]
}
},
mounted() {
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,52 @@
<template>
<el-row :gutter="20">
<el-col :md="14" :sm="24">
<DetailCard :title="cardTitle" :items="detailCardItems" />
</el-col>
</el-row>
</template>
<script type="text/jsx">
import DetailCard from '@/components/DetailCard'
export default {
components: {
DetailCard
},
props: {
object: {
type: Object,
default: () => ({})
}
},
data() {
return {}
},
computed: {
cardTitle() {
return this.object.name
},
detailCardItems() {
return [
{
key: this.$t('common.Name'),
value: this.object.name
},
{
key: 'Module',
value: this.object.module
},
{
key: 'content',
value: this.object.args
}
]
}
},
methods: {}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,42 @@
<template>
<GenericDetailPage :object.sync="AdhocDetail" :active-menu.sync="config.activeMenu" v-bind="config" v-on="$listeners">
<keep-alive>
<component :is="config.activeMenu" :object="AdhocDetail" />
</keep-alive>
</GenericDetailPage>
</template>
<script>
import { GenericDetailPage } from '@/layout/components'
import AdhocDetail from '@/views/ops/Adhoc/my/AdhocDetail/AdhocDetail'
export default {
components: {
GenericDetailPage,
AdhocDetail
},
data() {
return {
AdhocDetail: {},
config: {
getTitle(row) {
return row['name']
},
url: '/api/v1/ops/adhocs/',
activeMenu: 'AdhocDetail',
submenu: [
{
title: this.$t('ops.AdhocDetail'),
name: 'AdhocDetail'
}
],
hasRightSide: false
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,49 @@
<template>
<GenericCreateUpdatePage v-bind="$data" />
</template>
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import CodeEditor from '@/components/FormFields/CodeEditor'
export default {
components: {
GenericCreateUpdatePage
},
data() {
return {
cmOptions: {
tabSize: 4,
mode: 'shell',
theme: 'base16-light',
lineNumbers: true,
line: true
},
url: '/api/v1/ops/adhocs/',
fields: [
[this.$t('common.Basic'), ['name', 'module', 'args']]
],
initial: {
module: 'shell',
args: ''
},
fieldsMeta: {
args: {
label: 'content',
component: CodeEditor
}
},
createSuccessNextRoute: {
name: 'Adhoc'
},
updateSuccessNextRoute: {
name: 'Adhoc'
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,44 @@
<template>
<el-row :gutter="20">
<el-col :md="14" :sm="24">
<DetailCard :title="cardTitle" :items="detailCardItems" />
</el-col>
</el-row>
</template>
<script type="text/jsx">
import DetailCard from '@/components/DetailCard'
export default {
components: {
DetailCard
},
props: {
object: {
type: Object,
default: () => ({})
}
},
data() {
return {}
},
computed: {
cardTitle() {
return this.object.name
},
detailCardItems() {
return [
{
key: this.$t('common.Name'),
value: this.object.name
}
]
}
},
methods: {}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,113 @@
<template>
<div>
<GenericListTable :table-config="tableConfig" :header-actions="headerActions" />
</div>
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import { ActionsFormatter } from '@/components/TableFormatters'
import { openTaskPage } from '@/utils/jms'
export default {
components: {
GenericListTable
},
props: {
object: {
type: Object,
default: null
},
query: {
type: Object,
default: null
}
},
data() {
return {
showLogViewer: false,
showLogId: '',
tableConfig: {
url: `/api/v1/ops/job-executions/`,
columns: [
'date_start', 'is_finished', 'is_success', 'time_cost', 'short_id', 'actions'
],
columnsMeta: {
is_finished: {
label: this.$t('ops.isFinished'),
width: '96px',
formatter: (row) => {
if (row.is_finished) {
return <i Class='fa fa-check text-primary' />
}
return <i Class='fa fa-times text-danger' />
},
formatterArgs: {
width: '14px'
}
},
is_success: {
label: this.$t('ops.isSuccess'),
width: '96px',
formatter: (row) => {
if (!row.is_finished) {
return <i Class='fa fa fa-spinner fa-spin' />
}
if (row.is_success) {
return <i Class='fa fa-check text-primary' />
}
return <i Class='fa fa-times text-danger' />
},
formatterArgs: {
width: '14px'
}
},
time_cost: {
label: this.$t('ops.time'),
width: '100px',
formatter: function(row) {
if (row.time_cost) {
return row.time_cost.toFixed(2) + 's'
}
return '-'
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false,
hasDelete: false,
hasClone: false,
extraActions: [
{
name: 'showLog',
title: this.$t('ops.output'),
can: true,
callback: ({ row }) => {
openTaskPage(row.task_id)
}
}
]
}
}
}
},
headerActions: {
hasRightActions: false,
hasLeftActions: false
}
}
}, mounted() {
if (this.object) {
this.tableConfig.url += `?job_id=${this.object.id}`
}
if (this.query) {
this.tableConfig.url += `?type=${this.query.type}`
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,48 @@
<template>
<GenericDetailPage :object.sync="JobDetail" :active-menu.sync="config.activeMenu" v-bind="config" v-on="$listeners">
<keep-alive>
<component :is="config.activeMenu" :object="JobDetail" />
</keep-alive>
</GenericDetailPage>
</template>
<script>
import { GenericDetailPage } from '@/layout/components'
import JobDetail from '@/views/ops/Job/JobDetail/JobDetail'
import JobHistory from '@/views/ops/Job/JobDetail/JobHistory'
export default {
components: {
GenericDetailPage,
JobDetail,
JobHistory
},
data() {
return {
JobDetail: {},
config: {
getTitle(row) {
return row['name']
},
url: '/api/v1/ops/jobs/',
activeMenu: 'JobDetail',
submenu: [
{
title: this.$t('ops.AdhocDetail'),
name: 'JobDetail'
},
{
title: this.$t('ops.History'),
name: 'JobHistory'
}
],
hasRightSide: false
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,124 @@
<template>
<div v-if="ready">
<GenericCreateUpdatePage v-bind="$data" />
</div>
</template>
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import CodeEditor from '@/components/FormFields/CodeEditor'
import AssetSelect from '@/components/AssetSelect'
export default {
components: {
GenericCreateUpdatePage
},
data() {
return {
ready: false,
instantTask: false,
jobType: '',
url: '/api/v1/ops/jobs/',
fields: [
[this.$t('common.Basic'), ['name', 'type', 'instant']],
[this.$t('common.Task'), ['module', 'args', 'playbook']],
['资产', ['assets', 'runas', 'runas_policy']],
[this.$t('common.Plan'), ['runAfterSave']]
],
initial: {
type: 'adhoc',
module: 'shell',
args: '',
runas_policy: 'skip',
runAfterSave: false,
instant: false
},
fieldsMeta: {
name: {
hidden: (formValue) => {
return this.instantTask
}
},
type: {},
module: {
hidden: (formValue) => {
return formValue.type !== 'adhoc'
}
},
playbook: {
hidden: (formValue) => {
return formValue.type !== 'playbook'
},
el: {
multiple: false,
value: [],
ajax: {
url: '/api/v1/ops/playbooks/',
transformOption: (item) => {
return { label: item.name, value: item.id }
}
}
}
},
assets: {
type: 'assetSelect',
component: AssetSelect,
label: this.$t('perms.Asset'),
rules: [{
required: false
}],
el: {
value: []
}
},
args: {
hidden: (formValue) => {
return formValue.type !== 'adhoc'
},
component: CodeEditor
},
instant: {
hidden: () => {
return true
}
},
runAfterSave: {
label: '保存后立即执行',
type: 'checkbox',
hidden: (formValue) => {
return formValue.instant
}
}
}
}
},
mounted() {
if (this.$route.query && this.$route.query.type && this.$route.query.id) {
this.initial.type = 'adhoc'
switch (this.$route.query.type) {
case 'adhoc':
this.initial.type = 'adhoc'
this.$axios.get(`/api/v1/ops/adhocs/${this.$route.query.id}`).then((data) => {
this.initial.module = data.module
this.initial.args = data.args
this.initial.instant = true
this.initial.runAfterSave = true
this.instantTask = true
this.createSuccessNextRoute = { name: 'Adhoc' }
this.ready = true
})
break
case 'playbook':
break
}
} else {
this.ready = true
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,75 @@
<template>
<GenericListPage :table-config="tableConfig" :header-actions="headerActions" />
</template>
<script>
import GenericListPage from '@/layout/components/GenericListPage'
import { ActionsFormatter } from '@/components/TableFormatters'
import { openTaskPage } from '@/utils/jms'
export default {
components: {
GenericListPage
},
data() {
return {
tableConfig: {
url: '/api/v1/ops/jobs/',
columns: [
'name', 'type', 'date_updated', 'date_created', 'actions'
],
columnsMeta: {
name: {
formatterArgs: {
can: true
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: true,
canUpdate: true,
updateRoute: 'JobUpdate',
hasDelete: true,
canDelete: true,
hasClone: false,
extraActions: [
{
title: '执行',
name: 'run',
type: 'running',
can: true,
callback: ({ row }) => {
this.runJob(row)
}
}
]
}
}
}
},
headerActions: {
canCreate: true,
createRoute: 'JobCreate',
hasRefresh: true,
hasExport: false,
hasImport: false,
hasMoreActions: false
}
}
},
methods: {
runJob(row) {
this.$axios.post('/api/v1/ops/job-executions/', { job: row.id }).then(data => {
this.$axios.get(`/api/v1/ops/job-executions/${data.id}/`).then(d => {
openTaskPage(d.task_id)
})
})
}
}
}
</script>
<style>
</style>

View File

@@ -4,7 +4,6 @@
<script>
export default {
name: 'Command'
}
</script>

View File

@@ -0,0 +1,74 @@
<template>
<div>
<GenericListTable :table-config="tableConfig" :header-actions="headerActions" />
<UploadDialog :visible.sync="uploadDialogVisible" />
</div>
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import { ActionsFormatter } from '@/components/TableFormatters'
import UploadDialog from '@/views/ops/Playbook/UploadDialog'
export default {
components: {
UploadDialog,
GenericListTable
},
data() {
return {
uploadDialogVisible: false,
tableConfig: {
url: '/api/v1/ops/playbooks/',
columns: [
'name', 'path', 'date_updated', 'date_created', 'actions'
],
columnsMeta: {
name: {
formatterArgs: {
can: true,
route: 'AdhocDetail'
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: true,
canUpdate: true,
updateRoute: 'AdhocUpdate',
hasDelete: true,
canDelete: true,
hasClone: false,
extraActions: [
{
title: '执行',
name: 'run',
type: 'running',
can: true,
callback: ({ row }) => {
this.$router.push({ name: 'JobCreate', query: { type: 'adhoc', id: row.id }})
}
}
]
}
}
}
},
headerActions: {
canCreate: true,
hasRefresh: true,
hasExport: false,
hasImport: false,
hasMoreActions: false,
onCreate: () => {
this.uploadDialogVisible = true
}
}
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,49 @@
<template>
<GenericCreateUpdatePage v-bind="$data" />
</template>
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import CodeEditor from '@/components/FormFields/CodeEditor'
export default {
components: {
GenericCreateUpdatePage
},
data() {
return {
cmOptions: {
tabSize: 4,
mode: 'shell',
theme: 'base16-light',
lineNumbers: true,
line: true
},
url: '/api/v1/ops/adhocs/',
fields: [
[this.$t('common.Basic'), ['name', 'module', 'args']]
],
initial: {
module: 'shell',
args: ''
},
fieldsMeta: {
args: {
label: 'content',
component: CodeEditor
}
},
createSuccessNextRoute: {
name: 'Adhoc'
},
updateSuccessNextRoute: {
name: 'Adhoc'
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,101 @@
<template>
<Dialog
title="离线上传"
v-bind="$attrs"
@confirm="onSubmit"
v-on="$listeners"
>
<el-form label-position="top">
<el-form-item
:label="$tc('common.Upload' )"
:label-width="'100px'"
class="file-uploader"
>
<el-upload
ref="upload"
drag
action="string"
list-type="text/csv"
:limit="1"
:auto-upload="false"
:on-change="onFileChange"
:before-upload="beforeUpload"
accept=".zip"
>
<i class="el-icon-upload" />
<div class="el-upload__text">
{{ $t('common.imExport.dragUploadFileInfo') }}
</div>
<div slot="tip" class="el-upload__tip">
<span :class="{'hasError': hasFileFormatOrSizeError }">
{{ $t('terminal.uploadZipTips') }}
</span>
<div v-if="renderError" class="hasError">{{ renderError }}</div>
</div>
</el-upload>
</el-form-item>
</el-form>
</Dialog>
</template>
<script>
import { Dialog } from '@/components'
export default {
components: {
Dialog
},
data() {
return {
hasFileFormatOrSizeError: false,
renderError: '',
file: null
}
},
methods: {
onFileChange(file, fileList) {
if (file.status !== 'ready') {
return
}
this.file = file
},
beforeUpload(file) {
},
onSubmit() {
if (!this.file) {
return
}
const form = new FormData()
form.append('path', this.file.raw)
this.$axios.post(
'/api/v1/ops/playbooks/',
form,
{
headers: { 'Content-Type': 'multipart/form-data' },
disableFlashErrorMsg: true
}
).then(res => {
this.$message.success('上传成功')
this.$emit('update:visible', false)
}).catch(err => {
this.$message.error(err)
})
}
}
}
</script>
<style lang="scss" scoped>
.file-uploader.el-form-item {
margin-bottom: 0;
> > > .el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<TabPage
:submenu="submenu"
:active-menu.sync="activeMenu"
>
<component :is="activeMenu" />
</TabPage>
</template>
<script>
import { TabPage } from '@/layout/components'
import PlaybookList from '@/views/ops/Playbook/PlaybookList'
import History from '@/views/ops/Playbook/HIstory'
export default {
name: 'Index',
components: {
TabPage,
PlaybookList,
History
},
data() {
return {
activeMenu: 'PlaybookList',
submenu: [
{
title: 'Playbook',
name: 'PlaybookList'
},
{
title: this.$t('ops.history'),
name: 'History'
}
]
}
},
mounted() {
}
}
</script>
<style scoped>
</style>

View File

@@ -14,7 +14,7 @@ export default {
tableConfig: {
url: '/api/v1/ops/tasks/',
columns: [
'name', 'queue', 'comment', 'count', 'state', 'last_published_time'
'name', 'display_name', 'queue', 'comment', 'count', 'state', 'last_published_time'
],
columnsMeta: {
name: {
@@ -22,6 +22,12 @@ export default {
can: true
}
},
display_name: {
label: 'display_name',
formatter: row => {
return row.meta.display_name ? row.meta.display_name : '-'
}
},
comment: {
label: 'comment',
formatter: (row) => {