🔨 执行命令.
This commit is contained in:
@@ -7,6 +7,7 @@ import com.orion.ops.framework.web.core.annotation.RestWrapper;
|
||||
import com.orion.ops.module.asset.define.operator.ExecOperatorType;
|
||||
import com.orion.ops.module.asset.entity.request.exec.ExecCommandRequest;
|
||||
import com.orion.ops.module.asset.entity.request.exec.ExecInterruptRequest;
|
||||
import com.orion.ops.module.asset.entity.request.exec.ReExecCommandRequest;
|
||||
import com.orion.ops.module.asset.entity.vo.ExecCommandVO;
|
||||
import com.orion.ops.module.asset.service.ExecService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -41,10 +42,18 @@ public class ExecController {
|
||||
@PostMapping("/exec-command")
|
||||
@Operation(summary = "批量执行命令")
|
||||
@PreAuthorize("@ss.hasPermission('asset:exec:exec-command')")
|
||||
public ExecCommandVO execCommand(@RequestBody ExecCommandRequest request) {
|
||||
public ExecCommandVO execCommand(@Validated @RequestBody ExecCommandRequest request) {
|
||||
return execService.execCommand(request);
|
||||
}
|
||||
|
||||
@OperatorLog(ExecOperatorType.EXEC_COMMAND)
|
||||
@PostMapping("/re-exec-command")
|
||||
@Operation(summary = "重新执行命令")
|
||||
@PreAuthorize("@ss.hasPermission('asset:exec:exec-command')")
|
||||
public ExecCommandVO reExecCommand(@Validated @RequestBody ReExecCommandRequest request) {
|
||||
return execService.reExecCommand(request.getLogId());
|
||||
}
|
||||
|
||||
@OperatorLog(ExecOperatorType.INTERRUPT_EXEC)
|
||||
@PutMapping("/interrupt")
|
||||
@Operation(summary = "中断执行命令")
|
||||
@@ -66,7 +75,6 @@ public class ExecController {
|
||||
}
|
||||
|
||||
// TODO tail log
|
||||
// TODO 重新执行
|
||||
// TODO 删除时 中断
|
||||
// TODO parameterSchema 存储
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.orion.ops.framework.mybatis.core.mapper.IMapper;
|
||||
import com.orion.ops.module.asset.entity.domain.ExecHostLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 批量执行主机日志 Mapper 接口
|
||||
*
|
||||
@@ -14,4 +16,18 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper
|
||||
public interface ExecHostLogDAO extends IMapper<ExecHostLogDO> {
|
||||
|
||||
/**
|
||||
* 通过 logId 查询
|
||||
*
|
||||
* @param logId logId
|
||||
* @return rows
|
||||
*/
|
||||
default List<ExecHostLogDO> selectByLogId(Long logId) {
|
||||
return this.of()
|
||||
.createWrapper()
|
||||
.eq(ExecHostLogDO::getLogId, logId)
|
||||
.then()
|
||||
.list();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,9 +22,6 @@ import java.util.List;
|
||||
@Schema(name = "ExecCommandRequest", description = "批量执行命令 请求对象")
|
||||
public class ExecCommandRequest {
|
||||
|
||||
@Schema(description = "执行模板id")
|
||||
private Long templateId;
|
||||
|
||||
@Size(max = 128)
|
||||
@Schema(description = "执行描述")
|
||||
private String description;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.orion.ops.module.asset.entity.request.exec;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 重新执行命令 请求对象
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024/3/11 11:46
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(name = "ReExecCommandRequest", description = "重新执行命令 请求对象")
|
||||
public class ReExecCommandRequest {
|
||||
|
||||
@NonNull
|
||||
@Schema(description = "logId")
|
||||
private Long logId;
|
||||
|
||||
}
|
||||
@@ -20,6 +20,14 @@ public interface ExecService {
|
||||
*/
|
||||
ExecCommandVO execCommand(ExecCommandRequest request);
|
||||
|
||||
/**
|
||||
* 重新执行命令
|
||||
*
|
||||
* @param id id
|
||||
* @return result
|
||||
*/
|
||||
ExecCommandVO reExecCommand(Long id);
|
||||
|
||||
/**
|
||||
* 中断命令执行
|
||||
*
|
||||
|
||||
@@ -9,6 +9,9 @@ import com.orion.ops.module.asset.convert.ExecHostLogConvert;
|
||||
import com.orion.ops.module.asset.dao.ExecHostLogDAO;
|
||||
import com.orion.ops.module.asset.entity.domain.ExecHostLogDO;
|
||||
import com.orion.ops.module.asset.entity.vo.ExecHostLogVO;
|
||||
import com.orion.ops.module.asset.handler.host.exec.handler.IExecCommandHandler;
|
||||
import com.orion.ops.module.asset.handler.host.exec.handler.IExecTaskHandler;
|
||||
import com.orion.ops.module.asset.handler.host.exec.manager.ExecManager;
|
||||
import com.orion.ops.module.asset.service.ExecHostLogService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -16,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 批量执行主机日志 服务实现类
|
||||
@@ -31,6 +35,9 @@ public class ExecHostLogServiceImpl implements ExecHostLogService {
|
||||
@Resource
|
||||
private ExecHostLogDAO execHostLogDAO;
|
||||
|
||||
@Resource
|
||||
private ExecManager execManager;
|
||||
|
||||
@Override
|
||||
public List<ExecHostLogVO> getExecHostLogList(Long logId) {
|
||||
return execHostLogDAO.of()
|
||||
@@ -65,6 +72,14 @@ public class ExecHostLogServiceImpl implements ExecHostLogService {
|
||||
// 检查数据是否存在
|
||||
ExecHostLogDO record = execHostLogDAO.selectById(id);
|
||||
Valid.notNull(record, ErrorMessage.DATA_ABSENT);
|
||||
// 中断
|
||||
Optional.ofNullable(record.getLogId())
|
||||
.map(execManager::getTask)
|
||||
.map(IExecTaskHandler::getHandlers)
|
||||
.flatMap(s -> s.stream()
|
||||
.filter(h -> h.getHostId().equals(record.getHostId()))
|
||||
.findFirst())
|
||||
.ifPresent(IExecCommandHandler::interrupted);
|
||||
// 删除
|
||||
int effect = execHostLogDAO.deleteById(id);
|
||||
log.info("ExecHostLogService-deleteExecHostLogById id: {}, effect: {}", id, effect);
|
||||
|
||||
@@ -18,6 +18,8 @@ import com.orion.ops.module.asset.entity.request.exec.ExecLogQueryRequest;
|
||||
import com.orion.ops.module.asset.entity.vo.ExecHostLogVO;
|
||||
import com.orion.ops.module.asset.entity.vo.ExecLogStatusVO;
|
||||
import com.orion.ops.module.asset.entity.vo.ExecLogVO;
|
||||
import com.orion.ops.module.asset.handler.host.exec.handler.IExecTaskHandler;
|
||||
import com.orion.ops.module.asset.handler.host.exec.manager.ExecManager;
|
||||
import com.orion.ops.module.asset.service.ExecHostLogService;
|
||||
import com.orion.ops.module.asset.service.ExecLogService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -26,6 +28,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -48,6 +51,9 @@ public class ExecLogServiceImpl implements ExecLogService {
|
||||
@Resource
|
||||
private ExecHostLogService execHostLogService;
|
||||
|
||||
@Resource
|
||||
private ExecManager execManager;
|
||||
|
||||
@Override
|
||||
public DataGrid<ExecLogVO> getExecLogPage(ExecLogQueryRequest request) {
|
||||
// 条件
|
||||
@@ -74,6 +80,7 @@ public class ExecLogServiceImpl implements ExecLogService {
|
||||
List<ExecHostLogVO> hostList = execHostLogDAO.of()
|
||||
.createWrapper()
|
||||
.select(ExecHostLogDO::getId,
|
||||
ExecHostLogDO::getLogId,
|
||||
ExecHostLogDO::getStatus,
|
||||
ExecHostLogDO::getStartTime,
|
||||
ExecHostLogDO::getFinishTime,
|
||||
@@ -101,6 +108,8 @@ public class ExecLogServiceImpl implements ExecLogService {
|
||||
// 检查数据是否存在
|
||||
ExecLogDO record = execLogDAO.selectById(id);
|
||||
Valid.notNull(record, ErrorMessage.DATA_ABSENT);
|
||||
// 中断命令执行
|
||||
this.interruptedTask(Lists.singleton(id));
|
||||
// 删除执行日志
|
||||
int effect = execLogDAO.deleteById(id);
|
||||
// 删除主机日志
|
||||
@@ -115,6 +124,8 @@ public class ExecLogServiceImpl implements ExecLogService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Integer deleteExecLogByIdList(List<Long> idList) {
|
||||
log.info("ExecLogService-deleteExecLogByIdList idList: {}", idList);
|
||||
// 中断命令执行
|
||||
this.interruptedTask(idList);
|
||||
// 删除执行日志
|
||||
int effect = execLogDAO.deleteBatchIds(idList);
|
||||
// 删除主机日志
|
||||
@@ -138,6 +149,8 @@ public class ExecLogServiceImpl implements ExecLogService {
|
||||
.collect(Collectors.toList());
|
||||
int effect = 0;
|
||||
if (!idList.isEmpty()) {
|
||||
// 中断命令执行
|
||||
this.interruptedTask(idList);
|
||||
// 删除执行日志
|
||||
effect = execLogDAO.delete(wrapper);
|
||||
// 删除主机日志
|
||||
@@ -170,4 +183,16 @@ public class ExecLogServiceImpl implements ExecLogService {
|
||||
.orderByDesc(ExecLogDO::getId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 中断任务
|
||||
*
|
||||
* @param idList idList
|
||||
*/
|
||||
private void interruptedTask(List<Long> idList) {
|
||||
idList.stream()
|
||||
.map(execManager::getTask)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(IExecTaskHandler::interrupted);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ import java.util.stream.Collectors;
|
||||
public class ExecServiceImpl implements ExecService {
|
||||
|
||||
private static final ReplacementFormatter FORMATTER = ReplacementFormatters.create("@{{ ", " }}")
|
||||
.noMatchStrategy(NoMatchStrategy.EMPTY);
|
||||
.noMatchStrategy(NoMatchStrategy.KEEP);
|
||||
|
||||
@Resource
|
||||
private FileClient logsFileClient;
|
||||
@@ -124,23 +124,10 @@ public class ExecServiceImpl implements ExecService {
|
||||
.build();
|
||||
}).collect(Collectors.toList());
|
||||
execHostLogDAO.insertBatch(execHostLogs);
|
||||
// 开始执行
|
||||
ExecCommandDTO exec = ExecCommandDTO.builder()
|
||||
.logId(execId)
|
||||
.timeout(request.getTimeout())
|
||||
.hosts(execHostLogs.stream()
|
||||
.map(s -> ExecCommandHostDTO.builder()
|
||||
.hostId(s.getHostId())
|
||||
.hostLogId(s.getId())
|
||||
.command(s.getCommand())
|
||||
.timeout(request.getTimeout())
|
||||
.logPath(s.getLogPath())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.build();
|
||||
ExecTaskExecutors.start(exec);
|
||||
// 操作日志
|
||||
OperatorLogs.add(OperatorLogs.ID, execId);
|
||||
// 开始执行
|
||||
this.startExec(execLog, execHostLogs);
|
||||
// 返回
|
||||
List<ExecCommandHostVO> hostResult = execHostLogs.stream()
|
||||
.map(s -> ExecCommandHostVO.builder()
|
||||
@@ -154,6 +141,29 @@ public class ExecServiceImpl implements ExecService {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecCommandVO reExecCommand(Long logId) {
|
||||
log.info("ExecService.reExecCommand start logId: {}", logId);
|
||||
// 获取执行记录
|
||||
ExecLogDO execLog = execLogDAO.selectById(logId);
|
||||
Valid.notNull(execLog, ErrorMessage.DATA_ABSENT);
|
||||
// 获取执行主机
|
||||
List<ExecHostLogDO> hostLogs = execHostLogDAO.selectByLogId(logId);
|
||||
Valid.notEmpty(hostLogs, ErrorMessage.DATA_ABSENT);
|
||||
List<Long> hostIdList = hostLogs.stream()
|
||||
.map(ExecHostLogDO::getHostId)
|
||||
.collect(Collectors.toList());
|
||||
// 调用执行方法
|
||||
ExecCommandRequest request = ExecCommandRequest.builder()
|
||||
.description(execLog.getDescription())
|
||||
.timeout(execLog.getTimeout())
|
||||
.command(execLog.getCommand())
|
||||
.parameter(hostLogs.get(0).getParameter())
|
||||
.hostIdList(hostIdList)
|
||||
.build();
|
||||
return this.execCommand(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void interruptExec(Long logId) {
|
||||
@@ -250,6 +260,29 @@ public class ExecServiceImpl implements ExecService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始执行命令
|
||||
*
|
||||
* @param execLog execLog
|
||||
* @param execHostLogs hostLogs
|
||||
*/
|
||||
private void startExec(ExecLogDO execLog, List<ExecHostLogDO> execHostLogs) {
|
||||
ExecCommandDTO exec = ExecCommandDTO.builder()
|
||||
.logId(execLog.getId())
|
||||
.timeout(execLog.getTimeout())
|
||||
.hosts(execHostLogs.stream()
|
||||
.map(s -> ExecCommandHostDTO.builder()
|
||||
.hostId(s.getHostId())
|
||||
.hostLogId(s.getId())
|
||||
.command(s.getCommand())
|
||||
.timeout(execLog.getTimeout())
|
||||
.logPath(s.getLogPath())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.build();
|
||||
ExecTaskExecutors.start(exec);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建日志路径
|
||||
*
|
||||
|
||||
@@ -4,11 +4,12 @@ import axios from 'axios';
|
||||
* 执行命令请求
|
||||
*/
|
||||
export interface ExecCommandRequest {
|
||||
templateId?: number;
|
||||
logId?: number;
|
||||
description?: string;
|
||||
timeout?: number;
|
||||
command?: string;
|
||||
parameter?: string;
|
||||
parameterSchema?: string;
|
||||
hostIdList?: number[];
|
||||
}
|
||||
|
||||
@@ -38,6 +39,13 @@ export function execCommand(request: ExecCommandRequest) {
|
||||
return axios.post<ExecCommandResponse>('/asset/exec/exec-command', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新执行命令
|
||||
*/
|
||||
export function reExecCommand(request: ExecCommandRequest) {
|
||||
return axios.post<ExecCommandResponse>('/asset/exec/re-exec-command', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 中断执行命令
|
||||
*/
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
});
|
||||
|
||||
// 打开
|
||||
const open = async (hostIdList: Array<number>) => {
|
||||
const open = async (hostIdList: Array<number> = []) => {
|
||||
setVisible(true);
|
||||
// 加载主机列表
|
||||
await fetchHosts();
|
||||
|
||||
56
orion-ops-ui/src/components/view/exec-editor/const.ts
Normal file
56
orion-ops-ui/src/components/view/exec-editor/const.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// 模板参数
|
||||
export interface TemplateParam {
|
||||
name?: string;
|
||||
default?: string;
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
// 内置参数
|
||||
export const builtinsParams: Array<TemplateParam> = [
|
||||
{
|
||||
name: 'hostId',
|
||||
desc: '执行主机id'
|
||||
}, {
|
||||
name: 'hostName',
|
||||
desc: '执行主机名称'
|
||||
}, {
|
||||
name: 'hostCode',
|
||||
desc: '执行主机编码'
|
||||
}, {
|
||||
name: 'hostAddress',
|
||||
desc: '执行主机地址'
|
||||
}, {
|
||||
name: 'userId',
|
||||
desc: '执行用户id'
|
||||
}, {
|
||||
name: 'username',
|
||||
desc: '执行用户名'
|
||||
}, {
|
||||
name: 'execId',
|
||||
desc: '执行记录id'
|
||||
}, {
|
||||
name: 'uuid',
|
||||
desc: '生成任务维度 uuid'
|
||||
}, {
|
||||
name: 'uuidShort',
|
||||
desc: '生成任务维度 uuid 无 \'-\''
|
||||
}, {
|
||||
name: 'hostUuid',
|
||||
desc: '生成机器维度 uuid'
|
||||
}, {
|
||||
name: 'hostUuidShort',
|
||||
desc: '生成机器维度 uuid 无 \'-\''
|
||||
}, {
|
||||
name: 'timestampMillis',
|
||||
desc: '时间戳毫秒'
|
||||
}, {
|
||||
name: 'timestamp',
|
||||
desc: '时间戳'
|
||||
}, {
|
||||
name: 'date',
|
||||
desc: '执行时间 yyyy-MM-dd'
|
||||
}, {
|
||||
name: 'datetime',
|
||||
desc: '执行时间 yyyy-MM-dd HH:mm:ss'
|
||||
},
|
||||
];
|
||||
86
orion-ops-ui/src/components/view/exec-editor/index.vue
Normal file
86
orion-ops-ui/src/components/view/exec-editor/index.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<editor language="shell"
|
||||
:suggestions="false" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execEditor'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TemplateParam } from './const';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { builtinsParams } from './const';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { language } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
|
||||
|
||||
const props = defineProps<{
|
||||
parameter?: Array<TemplateParam>
|
||||
}>();
|
||||
|
||||
const suggestionsDispose = ref();
|
||||
|
||||
// 加载代码提示
|
||||
onMounted(() => {
|
||||
if (suggestionsDispose.value) {
|
||||
return;
|
||||
}
|
||||
// 代码提示
|
||||
suggestionsDispose.value = monaco.languages.registerCompletionItemProvider('shell', {
|
||||
provideCompletionItems() {
|
||||
const suggestions: any = [];
|
||||
language.keywords?.forEach((item: any) => {
|
||||
suggestions.push({
|
||||
label: item,
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: item
|
||||
});
|
||||
});
|
||||
language.builtins?.forEach((item: any) => {
|
||||
suggestions.push({
|
||||
label: item,
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: item,
|
||||
});
|
||||
});
|
||||
// 内置参数提示
|
||||
builtinsParams.forEach(s => {
|
||||
suggestions.push({
|
||||
label: s.name,
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: `@{{ ${s.name} }}`,
|
||||
detail: s.desc || '',
|
||||
});
|
||||
});
|
||||
// 命令参数提示
|
||||
props.parameter?.forEach(s => {
|
||||
if (!s.name) {
|
||||
return;
|
||||
}
|
||||
suggestions.push({
|
||||
label: s.name,
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: `@{{ ${s.name} }}`,
|
||||
detail: s.desc || '',
|
||||
});
|
||||
});
|
||||
return {
|
||||
suggestions: [...new Set(suggestions)],
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 卸载代码提示
|
||||
onUnmounted(() => {
|
||||
suggestionsDispose.value?.dispose();
|
||||
suggestionsDispose.value = undefined;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -7,9 +7,9 @@ const EXEC: AppRouteRecordRaw = {
|
||||
component: DEFAULT_LAYOUT,
|
||||
children: [
|
||||
{
|
||||
name: 'execTemplate',
|
||||
path: '/exec-template',
|
||||
component: () => import('@/views/exec/exec-template/index.vue'),
|
||||
name: 'execCommand',
|
||||
path: '/exec-command',
|
||||
component: () => import('@/views/exec/exec-command/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'execLog',
|
||||
@@ -17,9 +17,9 @@ const EXEC: AppRouteRecordRaw = {
|
||||
component: () => import('@/views/exec/exec-log/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'execHostLog',
|
||||
path: '/exec-host-log',
|
||||
component: () => import('@/views/exec/exec-host-log/index.vue'),
|
||||
name: 'execTemplate',
|
||||
path: '/exec-template',
|
||||
component: () => import('@/views/exec/exec-template/index.vue'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
17
orion-ops-ui/src/views/exec/exec-command/index.vue
Normal file
17
orion-ops-ui/src/views/exec/exec-command/index.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div>123</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execCommand'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
33
orion-ops-ui/src/views/exec/exec-command/types/form.rules.ts
Normal file
33
orion-ops-ui/src/views/exec/exec-command/types/form.rules.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const description = [{
|
||||
maxLength: 128,
|
||||
message: '执行描述长度不能大于128位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const hostIdList = [{
|
||||
required: true,
|
||||
message: '请选择执行主机'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const command = [{
|
||||
required: true,
|
||||
message: '请输入执行命令'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const timeout = [{
|
||||
required: true,
|
||||
message: '请输入超时时间'
|
||||
}, {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100000,
|
||||
message: '超时时间需要在 0 - 100000 之间'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
description,
|
||||
hostIdList,
|
||||
command,
|
||||
timeout,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -58,7 +58,7 @@
|
||||
日志
|
||||
</a-button>
|
||||
<!-- 中断 -->
|
||||
<a-popconfirm content="确认要中断命令吗?"
|
||||
<a-popconfirm content="确认要中断命令吗, 删除后会中断执行?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="interruptedHost(record)">
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
body-class="modal-form"
|
||||
title-align="start"
|
||||
title="清空执行记录"
|
||||
:align-center="false"
|
||||
:draggable="true"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
ok-text="清空"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@close="handleClose">
|
||||
<a-spin class="full" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:label-col-props="{ span: 5 }"
|
||||
:wrapper-col-props="{ span: 18 }">
|
||||
<!-- 执行用户 -->
|
||||
<a-form-item field="userId" label="执行用户">
|
||||
<user-selector v-model="formModel.userId"
|
||||
placeholder="请选择执行用户"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行描述 -->
|
||||
<a-form-item field="description" label="执行描述">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入执行描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行命令 -->
|
||||
<a-form-item field="command" label="执行命令">
|
||||
<a-input v-model="formModel.command"
|
||||
placeholder="请输入执行命令"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行状态 -->
|
||||
<a-form-item field="status" label="执行状态">
|
||||
<a-select v-model="formModel.status"
|
||||
:options="toOptions(execStatusKey)"
|
||||
placeholder="请选择执行状态" />
|
||||
</a-form-item>
|
||||
<!-- 执行时间 -->
|
||||
<a-form-item field="startTimeRange" label="执行时间">
|
||||
<a-range-picker v-model="formModel.startTimeRange"
|
||||
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execLogClearModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecLogQueryRequest } from '@/api/exec/exec-log';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { execStatusKey } from '../types/const';
|
||||
import { getExecLogCount, clearExecLog } from '@/api/exec/exec-log';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['clear']);
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions } = useDictStore();
|
||||
|
||||
const formRef = ref<any>();
|
||||
const formModel = ref<ExecLogQueryRequest>({});
|
||||
|
||||
const defaultForm = (): ExecLogQueryRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
userId: undefined,
|
||||
description: undefined,
|
||||
command: undefined,
|
||||
status: undefined,
|
||||
startTimeRange: undefined
|
||||
};
|
||||
};
|
||||
|
||||
// 打开
|
||||
const open = (record: any) => {
|
||||
renderForm({ ...defaultForm(), ...record });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: any) => {
|
||||
formModel.value = Object.assign({}, record);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 获取总数量
|
||||
const { data } = await getExecLogCount(formModel.value);
|
||||
if (data) {
|
||||
// 清空
|
||||
doClear(data);
|
||||
} else {
|
||||
// 无数据
|
||||
Message.warning('当前条件未查询到数据');
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 执行删除
|
||||
const doClear = (count: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除清空',
|
||||
content: `确定要删除 ${count} 条数据吗? 确定后将立即删除且无法恢复!`,
|
||||
onOk: async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 调用删除
|
||||
await clearExecLog(formModel.value);
|
||||
emits('clear');
|
||||
// 清空
|
||||
setVisible(false);
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -61,8 +61,26 @@
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 执行命令 -->
|
||||
<a-button v-permission="['asset:exec:exec-command']"
|
||||
type="primary"
|
||||
@click="$router.push({ name: 'execCommand' })">
|
||||
执行命令
|
||||
<template #icon>
|
||||
<icon-thunderbolt />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 清空 -->
|
||||
<a-button v-permission="['infra:exec-log:clear']"
|
||||
status="danger"
|
||||
@click="openClear">
|
||||
清空
|
||||
<template #icon>
|
||||
<icon-close />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm :content="`确认删除选中的 ${selectedKeys.length} 条记录吗?`"
|
||||
<a-popconfirm :content="`确认删除选中的 ${selectedKeys.length} 条记录吗? 删除后会中断执行!`"
|
||||
position="br"
|
||||
type="warning"
|
||||
@ok="deleteSelectRows">
|
||||
@@ -128,7 +146,7 @@
|
||||
<a-popconfirm content="确定要重新执行吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
@ok="doReExecCommand(record)">
|
||||
<a-button v-permission="['asset:exec:exec-command']"
|
||||
type="text"
|
||||
size="mini">
|
||||
@@ -136,14 +154,13 @@
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 命令 -->
|
||||
<a-button v-permission="['asset:exec:interrupt-exec']"
|
||||
type="text"
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
@click="emits('viewCommand', record.command)">
|
||||
命令
|
||||
</a-button>
|
||||
<!-- 日志 -->
|
||||
<a-button v-permission="['asset:exec:interrupt-exec']"
|
||||
<a-button v-permission="['asset:exec:exec-command']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="emits('viewLog', record.id)">
|
||||
@@ -153,7 +170,7 @@
|
||||
<a-popconfirm content="确定要中断执行吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="interruptedExec(record)">
|
||||
@ok="doInterruptExec(record)">
|
||||
<a-button v-permission="['asset:exec:interrupt-exec']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@@ -163,7 +180,7 @@
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
<a-popconfirm content="确认删除这条记录吗, 删除后会中断执行?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
@@ -188,8 +205,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ExecLogQueryRequest, ExecLogQueryResponse } from '@/api/exec/exec-log';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { batchDeleteExecLog, deleteExecLog, getExecHostLogList, getExecLogPage } from '@/api/exec/exec-log';
|
||||
import { reactive, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { batchDeleteExecLog, deleteExecLog, getExecHostLogList, getExecLogPage, getExecLogStatus } from '@/api/exec/exec-log';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
@@ -197,13 +214,13 @@
|
||||
import { useExpandable, usePagination, useRowSelection } from '@/types/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dateFormat, formatDuration } from '@/utils';
|
||||
import { interruptExec } from '@/api/exec/exec';
|
||||
import { interruptExec, reExecCommand } from '@/api/exec/exec';
|
||||
import UserSelector from '@/components/user/user/selector/index.vue';
|
||||
import ExecHostLogTable from './exec-host-log-table.vue';
|
||||
|
||||
const emits = defineEmits(['viewCommand', 'viewParams', 'viewLog']);
|
||||
const emits = defineEmits(['viewCommand', 'viewParams', 'viewLog', 'openClear']);
|
||||
|
||||
// TODO 日志 清理 轮询状态 ctrl日志 ctrl重新执行
|
||||
// TODO 日志 清理 ctrl日志 ctrl重新执行
|
||||
|
||||
const pagination = usePagination();
|
||||
const rowSelection = useRowSelection();
|
||||
@@ -211,6 +228,7 @@
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
|
||||
const intervalId = ref();
|
||||
const tableRef = ref();
|
||||
const selectedKeys = ref<number[]>([]);
|
||||
const tableRenderData = ref<ExecLogQueryResponse[]>([]);
|
||||
@@ -223,6 +241,11 @@
|
||||
startTimeRange: undefined,
|
||||
});
|
||||
|
||||
// 打开清理
|
||||
const openClear = () => {
|
||||
emits('openClear', { ...formModel, id: undefined });
|
||||
};
|
||||
|
||||
// 删除选中行
|
||||
const deleteSelectRows = async () => {
|
||||
try {
|
||||
@@ -256,8 +279,24 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 重新执行命令
|
||||
const doReExecCommand = async (record: ExecLogQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用中断接口
|
||||
await reExecCommand({
|
||||
logId: record.id
|
||||
});
|
||||
Message.success('已重新执行');
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 中断执行
|
||||
const interruptedExec = async (record: ExecLogQueryResponse) => {
|
||||
const doInterruptExec = async (record: ExecLogQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用中断接口
|
||||
@@ -282,6 +321,43 @@
|
||||
record.hosts = data;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const fetchTaskStatus = async () => {
|
||||
const unCompleteIdList = tableRenderData.value
|
||||
.filter(s => s.status === execStatus.WAITING || s.status === execStatus.RUNNING)
|
||||
.map(s => s.id);
|
||||
if (!unCompleteIdList.length) {
|
||||
return;
|
||||
}
|
||||
// 加载未完成的状态
|
||||
const { data: { logList, hostList } } = await getExecLogStatus(unCompleteIdList);
|
||||
// 设置任务状态
|
||||
logList.forEach(s => {
|
||||
const tableRow = tableRenderData.value.find(r => r.id === s.id);
|
||||
if (!tableRow) {
|
||||
return;
|
||||
}
|
||||
tableRow.status = s.status;
|
||||
tableRow.startTime = s.startTime;
|
||||
tableRow.finishTime = s.finishTime;
|
||||
});
|
||||
// 设置主机状态
|
||||
hostList.forEach(s => {
|
||||
const host = tableRenderData.value
|
||||
.find(r => r.id === s.logId)
|
||||
?.hosts
|
||||
?.find(r => r.id === s.id);
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
host.status = s.status;
|
||||
host.startTime = s.startTime;
|
||||
host.finishTime = s.finishTime;
|
||||
host.exitStatus = s.exitStatus;
|
||||
host.errorMessage = s.errorMessage;
|
||||
});
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: ExecLogQueryRequest) => {
|
||||
try {
|
||||
@@ -304,8 +380,20 @@
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
fetchTableData
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 加载数据
|
||||
fetchTableData();
|
||||
// 注册状态轮询
|
||||
intervalId.value = setInterval(fetchTaskStatus, 10000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 卸载状态轮询
|
||||
clearInterval(intervalId.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<exec-log-table @view-command="viewCommand"
|
||||
@view-params="viewParams" />
|
||||
<exec-log-table ref="tableRef"
|
||||
@view-command="viewCommand"
|
||||
@view-params="viewParams"
|
||||
@open-clear="openClearModal" />
|
||||
<!-- 清理模态框 -->
|
||||
<exec-log-clear-modal ref="clearModal"
|
||||
@clear="clearCallback" />
|
||||
<!-- json 模态框 -->
|
||||
<json-editor-modal ref="jsonModal"
|
||||
:esc-to-close="true" />
|
||||
@@ -24,13 +29,21 @@
|
||||
import { useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import ExecLogTable from './components/exec-log-table.vue';
|
||||
import ExecLogClearModal from './components/exec-log-clear-modal.vue';
|
||||
import JsonEditorModal from '@/components/view/json-editor/modal/index.vue';
|
||||
import ShellEditorModal from '@/components/view/shell-editor/modal/index.vue';
|
||||
|
||||
const render = ref(false);
|
||||
const tableRef = ref();
|
||||
const clearModal = ref();
|
||||
const jsonModal = ref();
|
||||
const shellModal = ref();
|
||||
|
||||
// 打开清理模态框
|
||||
const openClearModal = (e: any) => {
|
||||
clearModal.value.open(e);
|
||||
};
|
||||
|
||||
// 查看命令
|
||||
const viewCommand = (data: string) => {
|
||||
shellModal.value.open(data, '命令');
|
||||
@@ -41,6 +54,11 @@
|
||||
jsonModal.value.open(JSON.parse(data));
|
||||
};
|
||||
|
||||
// 清理回调
|
||||
const clearCallback = () => {
|
||||
tableRef.value.fetchTableData();
|
||||
};
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
title="执行命令"
|
||||
:width="470"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@cancel="handleClose">
|
||||
<a-spin class="full spin-wrapper" :loading="loading">
|
||||
<!-- 命令表单 -->
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
:rules="formRules">
|
||||
<!-- 执行主机 -->
|
||||
<a-form-item field="hostIdList" label="执行主机">
|
||||
<div class="selected-host">
|
||||
<!-- 已选择数量 -->
|
||||
<span class="usn" v-if="formModel.hostIdList?.length">
|
||||
已选择<span class="selected-host-count span-blue">{{ formModel.hostIdList?.length }}</span>台主机
|
||||
</span>
|
||||
<span class="usn pointer span-blue" @click="openSelectHost">
|
||||
{{ formModel.hostIdList?.length ? '重新选择' : '选择主机' }}
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<!-- 执行描述 -->
|
||||
<a-form-item field="description" label="执行描述">
|
||||
<a-input v-model="formModel.description"
|
||||
placeholder="请输入执行描述"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 超时时间 -->
|
||||
<a-form-item field="timeout"
|
||||
label="超时时间">
|
||||
<a-input-number v-model="formModel.timeout"
|
||||
placeholder="为0则不超时"
|
||||
:min="0"
|
||||
:max="100000"
|
||||
hide-button>
|
||||
<template #suffix>
|
||||
秒
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<!-- 模板命令 -->
|
||||
<a-form-item field="command"
|
||||
label="模板命令"
|
||||
:wrapper-col-props="{ span: 24 }">
|
||||
<exec-editor v-model="formModel.command"
|
||||
containerClass="command-editor"
|
||||
theme="vs-dark"
|
||||
:parameter="parameterSchema" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<!-- 命令参数 -->
|
||||
<a-divider v-if="parameterSchema.length"
|
||||
orientation="center"
|
||||
style="margin: 12px 0 26px 0;">
|
||||
命令参数
|
||||
</a-divider>
|
||||
<!-- 参数表单 -->
|
||||
<a-form v-if="parameterSchema.length"
|
||||
:model="parameterFormModel"
|
||||
ref="parameterFormRef"
|
||||
label-align="right"
|
||||
:label-col-props="{ span: 5 }"
|
||||
:wrapper-col-props="{ span: 18 }">
|
||||
<a-form-item v-for="item in parameterSchema"
|
||||
:key="item.name"
|
||||
:field="item.name as string"
|
||||
:label="item.name"
|
||||
required>
|
||||
<a-input v-model="parameterFormModel[item.name]"
|
||||
:placeholder="item.desc"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
<!-- 主机模态框 -->
|
||||
<authorized-host-modal ref="hostModal"
|
||||
@selected="setSelectedHost" />
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'execTemplateExecDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TemplateParam } from '@/components/view/exec-editor/const';
|
||||
import type { ExecTemplateQueryResponse } from '@/api/exec/exec-template';
|
||||
import type { ExecCommandRequest } from '@/api/exec/exec';
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import formRules from '../../exec-command/types/form.rules';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import ExecEditor from '@/components/view/exec-editor/index.vue';
|
||||
import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue';
|
||||
import { execCommand } from '@/api/exec/exec';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const formRef = ref<any>();
|
||||
const parameterFormRef = ref<any>();
|
||||
const hostModal = ref<any>();
|
||||
const formModel = ref<ExecCommandRequest>({});
|
||||
const parameterFormModel = ref<Record<string, any>>({});
|
||||
const parameterSchema = ref<Array<TemplateParam>>([]);
|
||||
|
||||
// 打开
|
||||
const open = (record: ExecTemplateQueryResponse) => {
|
||||
renderForm({ ...record });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: ExecTemplateQueryResponse) => {
|
||||
formModel.value = {
|
||||
description: record.name,
|
||||
timeout: record.timeout,
|
||||
command: record.command,
|
||||
hostIdList: []
|
||||
};
|
||||
if (record.parameter) {
|
||||
parameterSchema.value = JSON.parse(record.parameter);
|
||||
const params = {} as any;
|
||||
for (let param of parameterSchema.value) {
|
||||
params[param.name as keyof any] = param.default;
|
||||
}
|
||||
parameterFormModel.value = params;
|
||||
|
||||
} else {
|
||||
parameterSchema.value = [];
|
||||
parameterFormModel.value = {};
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 打开选择主机
|
||||
const openSelectHost = () => {
|
||||
hostModal.value.open(formModel.value.hostIdList);
|
||||
};
|
||||
|
||||
// 设置选中主机
|
||||
const setSelectedHost = (hosts: Array<number>) => {
|
||||
formModel.value.hostIdList = hosts;
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
let error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
error = await parameterFormRef.value?.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
await execCommand({
|
||||
...formModel.value,
|
||||
parameter: JSON.stringify(parameterFormModel.value),
|
||||
parameterSchema: JSON.stringify(parameterSchema.value),
|
||||
});
|
||||
Message.success('已开始执行');
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// 卸载关闭
|
||||
onUnmounted(handlerClear);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.spin-wrapper {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.selected-host {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&-count {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.command-editor {
|
||||
width: 100%;
|
||||
height: 44vh;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -11,7 +11,6 @@
|
||||
<a-spin class="full modal-form" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
layout="vertical"
|
||||
:rules="formRules">
|
||||
<!-- 模板名称 -->
|
||||
<a-form-item field="name" label="模板名称">
|
||||
@@ -33,17 +32,20 @@
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<!-- 模板命令 -->
|
||||
<a-form-item field="command" label="模板命令">
|
||||
<editor v-model="formModel.command"
|
||||
containerClass="command-editor"
|
||||
language="shell"
|
||||
theme="vs-dark"
|
||||
:suggestions="false" />
|
||||
<a-form-item field="command"
|
||||
label="模板命令"
|
||||
:wrapper-col-props="{ span: 24 }">
|
||||
<exec-editor v-model="formModel.command"
|
||||
containerClass="command-editor"
|
||||
theme="vs-dark"
|
||||
:parameter="parameter" />
|
||||
</a-form-item>
|
||||
<!-- 命令参数 -->
|
||||
<a-form-item field="parameter"
|
||||
class="parameter-form-item"
|
||||
label="命令参数">
|
||||
label="命令参数"
|
||||
:label-col-props="{ span: 24 }"
|
||||
:wrapper-col-props="{ span: 24 }">
|
||||
<!-- label -->
|
||||
<template #label>
|
||||
<div class="parameter-label-wrapper">
|
||||
@@ -72,8 +74,8 @@
|
||||
<span class="parameter-item-close click-icon-wrapper"
|
||||
title="移除"
|
||||
@click="removeParameter(i)">
|
||||
<icon-close />
|
||||
</span>
|
||||
<icon-close />
|
||||
</span>
|
||||
</a-input-group>
|
||||
</template>
|
||||
<!-- 无参数 -->
|
||||
@@ -95,17 +97,15 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TemplateParam } from '../types/const';
|
||||
import type { TemplateParam } from '@/components/view/exec-editor/const';
|
||||
import type { ExecTemplateUpdateRequest } from '@/api/exec/exec-template';
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import formRules from '../types/form.rules';
|
||||
import { createExecTemplate, updateExecTemplate } from '@/api/exec/exec-template';
|
||||
import { builtinsParams } from '../types/const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { language as shellLanguage } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
|
||||
import ExecEditor from '@/components/view/exec-editor/index.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
@@ -126,7 +126,6 @@
|
||||
const formRef = ref<any>();
|
||||
const formModel = ref<ExecTemplateUpdateRequest>({});
|
||||
const parameter = ref<Array<TemplateParam>>([]);
|
||||
const suggestionsDispose = ref();
|
||||
|
||||
const emits = defineEmits(['added', 'updated']);
|
||||
|
||||
@@ -154,8 +153,6 @@
|
||||
} else {
|
||||
parameter.value = [];
|
||||
}
|
||||
// 注册代码提示
|
||||
registerSuggestions();
|
||||
};
|
||||
|
||||
defineExpose({ openAdd, openUpdate });
|
||||
@@ -170,57 +167,6 @@
|
||||
parameter.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 注册代码提示
|
||||
const registerSuggestions = () => {
|
||||
if (suggestionsDispose.value) {
|
||||
return;
|
||||
}
|
||||
// 代码提示
|
||||
suggestionsDispose.value = monaco.languages.registerCompletionItemProvider('shell', {
|
||||
provideCompletionItems() {
|
||||
const suggestions: any = [];
|
||||
shellLanguage.keywords?.forEach((item: any) => {
|
||||
suggestions.push({
|
||||
label: item,
|
||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
||||
insertText: item
|
||||
});
|
||||
});
|
||||
shellLanguage.builtins?.forEach((item: any) => {
|
||||
suggestions.push({
|
||||
label: item,
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: item,
|
||||
});
|
||||
});
|
||||
// 内置参数提示
|
||||
builtinsParams.forEach(s => {
|
||||
suggestions.push({
|
||||
label: s.name,
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: `@{{ ${s.name} }}`,
|
||||
detail: s.desc || '',
|
||||
});
|
||||
});
|
||||
// 命令参数提示
|
||||
parameter.value.forEach(s => {
|
||||
if (!s.name) {
|
||||
return;
|
||||
}
|
||||
suggestions.push({
|
||||
label: s.name,
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: `@{{ ${s.name} }}`,
|
||||
detail: s.desc || '',
|
||||
});
|
||||
});
|
||||
return {
|
||||
suggestions: [...new Set(suggestions)],
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
@@ -268,9 +214,6 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
// 卸载代码提示
|
||||
suggestionsDispose.value?.dispose();
|
||||
suggestionsDispose.value = undefined;
|
||||
};
|
||||
|
||||
// 卸载关闭
|
||||
@@ -352,7 +295,7 @@
|
||||
|
||||
.command-editor {
|
||||
width: 100%;
|
||||
height: 44vh;
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -76,6 +76,12 @@
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<a-button v-permission="['asset:exec:exec-command']"
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="emits('openExec', record)">
|
||||
执行
|
||||
</a-button>
|
||||
<!-- 修改 -->
|
||||
<a-button v-permission="['asset:exec-template:update']"
|
||||
type="text"
|
||||
@@ -118,14 +124,13 @@
|
||||
import { usePagination } from '@/types/table';
|
||||
import useCopy from '@/hooks/copy';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate']);
|
||||
|
||||
const tableRenderData = ref<ExecTemplateQueryResponse[]>([]);
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openExec']);
|
||||
|
||||
const pagination = usePagination();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { copy } = useCopy();
|
||||
|
||||
const tableRenderData = ref<ExecTemplateQueryResponse[]>([]);
|
||||
const formModel = reactive<ExecTemplateQueryRequest>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<exec-template-table ref="table"
|
||||
@openAdd="() => drawer.openAdd()"
|
||||
@openUpdate="(e) => drawer.openUpdate(e)" />
|
||||
@open-exec="e => execModal.open(e)"
|
||||
@openAdd="() => drawer.openAdd()"
|
||||
@openUpdate="(e) => drawer.openUpdate(e)" />
|
||||
<!-- 添加修改模态框 -->
|
||||
<exec-template-form-drawer ref="drawer"
|
||||
@added="modalAddCallback"
|
||||
@updated="modalUpdateCallback" />
|
||||
@added="modalAddCallback"
|
||||
@updated="modalUpdateCallback" />
|
||||
<!-- 执行模态框 -->
|
||||
<exec-template-exec-drawer ref="execModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,10 +24,12 @@
|
||||
import { ref, onBeforeMount } from 'vue';
|
||||
import ExecTemplateTable from './components/exec-template-table.vue';
|
||||
import ExecTemplateFormDrawer from './components/exec-template-form-drawer.vue';
|
||||
import ExecTemplateExecDrawer from './components/exec-template-exec-drawer.vue';
|
||||
|
||||
const render = ref(false);
|
||||
const table = ref();
|
||||
const drawer = ref();
|
||||
const execModal = ref();
|
||||
|
||||
// 添加回调
|
||||
const modalAddCallback = () => {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
// 模板参数
|
||||
export interface TemplateParam {
|
||||
name?: string;
|
||||
default?: string;
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
// 内置参数
|
||||
export const builtinsParams: Array<TemplateParam> = [
|
||||
{
|
||||
name: 'hostId',
|
||||
desc: '执行主机id'
|
||||
}, {
|
||||
name: 'hostName',
|
||||
desc: '执行主机名称'
|
||||
}, {
|
||||
name: 'hostCode',
|
||||
desc: '执行主机编码'
|
||||
}, {
|
||||
name: 'hostAddress',
|
||||
desc: '执行主机地址'
|
||||
}, {
|
||||
name: 'userId',
|
||||
desc: '执行用户id'
|
||||
}, {
|
||||
name: 'username',
|
||||
desc: '执行用户名'
|
||||
}, {
|
||||
name: 'execId',
|
||||
desc: '执行记录id'
|
||||
}, {
|
||||
name: 'uuid',
|
||||
desc: '生成任务维度 uuid'
|
||||
}, {
|
||||
name: 'uuidShort',
|
||||
desc: '生成任务维度 uuid 无 \'-\''
|
||||
}, {
|
||||
name: 'hostUuid',
|
||||
desc: '生成机器维度 uuid'
|
||||
}, {
|
||||
name: 'hostUuidShort',
|
||||
desc: '生成机器维度 uuid 无 \'-\''
|
||||
}, {
|
||||
name: 'timestampMillis',
|
||||
desc: '时间戳毫秒'
|
||||
}, {
|
||||
name: 'timestamp',
|
||||
desc: '时间戳'
|
||||
}, {
|
||||
name: 'date',
|
||||
desc: '执行时间 yyyy-MM-dd'
|
||||
}, {
|
||||
name: 'datetime',
|
||||
desc: '执行时间 yyyy-MM-dd HH:mm:ss'
|
||||
},
|
||||
];
|
||||
|
||||
@@ -16,6 +16,11 @@ export const command = [{
|
||||
export const timeout = [{
|
||||
required: true,
|
||||
message: '请输入超时时间'
|
||||
}, {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100000,
|
||||
message: '超时时间需要在 0 - 100000 之间'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
|
||||
Reference in New Issue
Block a user