🔨 执行命令.

This commit is contained in:
lijiahang
2024-03-15 19:32:22 +08:00
parent 87bbcfa845
commit 03c334a507
28 changed files with 791 additions and 202 deletions

View File

@@ -106,6 +106,10 @@ public class DataQuery<T> {
return then; return then;
} }
public DataQuery<T> limit(IPageRequest page) {
return this.last(Pager.of(page).getSql());
}
public DataQuery<T> limit(int limit) { public DataQuery<T> limit(int limit) {
return this.last(Const.LIMIT + Const.SPACE + limit); return this.last(Const.LIMIT + Const.SPACE + limit);
} }

View File

@@ -5,6 +5,7 @@ import com.orion.ops.framework.biz.operator.log.core.annotation.OperatorLog;
import com.orion.ops.framework.common.validator.group.Page; import com.orion.ops.framework.common.validator.group.Page;
import com.orion.ops.framework.log.core.annotation.IgnoreLog; import com.orion.ops.framework.log.core.annotation.IgnoreLog;
import com.orion.ops.framework.log.core.enums.IgnoreLogMode; import com.orion.ops.framework.log.core.enums.IgnoreLogMode;
import com.orion.ops.framework.security.core.utils.SecurityUtils;
import com.orion.ops.framework.web.core.annotation.RestWrapper; import com.orion.ops.framework.web.core.annotation.RestWrapper;
import com.orion.ops.module.asset.define.operator.ExecOperatorType; import com.orion.ops.module.asset.define.operator.ExecOperatorType;
import com.orion.ops.module.asset.entity.request.exec.ExecLogQueryRequest; import com.orion.ops.module.asset.entity.request.exec.ExecLogQueryRequest;
@@ -73,6 +74,16 @@ public class ExecLogController {
return execLogService.getExecLogStatus(idList); return execLogService.getExecLogStatus(idList);
} }
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/history")
@Operation(summary = "查询执行历史")
@PreAuthorize("@ss.hasAnyPermission('asset:exec-log:query', 'asset:exec:exec-command')")
public List<ExecLogVO> getExecLogHistory(@Validated(Page.class) ExecLogQueryRequest request) {
request.setSource(ExecSourceEnum.BATCH.name());
request.setUserId(SecurityUtils.getLoginUserId());
return execLogService.getExecHistory(request);
}
@OperatorLog(ExecOperatorType.DELETE_LOG) @OperatorLog(ExecOperatorType.DELETE_LOG)
@DeleteMapping("/delete") @DeleteMapping("/delete")
@Operation(summary = "删除执行日志") @Operation(summary = "删除执行日志")

View File

@@ -21,7 +21,6 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List;
/** /**
* 执行模板 api * 执行模板 api
@@ -67,14 +66,6 @@ public class ExecTemplateController {
return execTemplateService.getExecTemplateById(id); return execTemplateService.getExecTemplateById(id);
} }
@IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/list")
@Operation(summary = "查询全部执行模板")
@PreAuthorize("@ss.hasPermission('asset:exec-template:query')")
public List<ExecTemplateVO> getExecTemplateList() {
return execTemplateService.getExecTemplateListByCache();
}
@IgnoreLog(IgnoreLogMode.RET) @IgnoreLog(IgnoreLogMode.RET)
@PostMapping("/query") @PostMapping("/query")
@Operation(summary = "分页查询执行模板") @Operation(summary = "分页查询执行模板")

View File

@@ -1,7 +1,6 @@
package com.orion.ops.module.asset.convert; package com.orion.ops.module.asset.convert;
import com.orion.ops.module.asset.entity.domain.ExecTemplateDO; import com.orion.ops.module.asset.entity.domain.ExecTemplateDO;
import com.orion.ops.module.asset.entity.dto.ExecTemplateCacheDTO;
import com.orion.ops.module.asset.entity.request.exec.ExecTemplateCreateRequest; import com.orion.ops.module.asset.entity.request.exec.ExecTemplateCreateRequest;
import com.orion.ops.module.asset.entity.request.exec.ExecTemplateUpdateRequest; import com.orion.ops.module.asset.entity.request.exec.ExecTemplateUpdateRequest;
import com.orion.ops.module.asset.entity.vo.ExecTemplateVO; import com.orion.ops.module.asset.entity.vo.ExecTemplateVO;
@@ -26,8 +25,4 @@ public interface ExecTemplateConvert {
ExecTemplateVO to(ExecTemplateDO domain); ExecTemplateVO to(ExecTemplateDO domain);
ExecTemplateVO to(ExecTemplateCacheDTO cache);
ExecTemplateCacheDTO toCache(ExecTemplateDO domain);
} }

View File

@@ -1,27 +0,0 @@
package com.orion.ops.module.asset.define.cache;
import com.orion.lang.define.cache.key.CacheKeyBuilder;
import com.orion.lang.define.cache.key.CacheKeyDefine;
import com.orion.lang.define.cache.key.struct.RedisCacheStruct;
import com.orion.ops.module.asset.entity.dto.ExecTemplateCacheDTO;
import java.util.concurrent.TimeUnit;
/**
* 执行模板缓存 key
*
* @author Jiahang Li
* @version 1.0.1
* @since 2024-3-7 18:08
*/
public interface ExecTemplateCacheKeyDefine {
CacheKeyDefine EXEC_TEMPLATE = new CacheKeyBuilder()
.key("exec:template:list")
.desc("执行模板列表")
.type(ExecTemplateCacheDTO.class)
.struct(RedisCacheStruct.HASH)
.timeout(1, TimeUnit.DAYS)
.build();
}

View File

@@ -0,0 +1,35 @@
package com.orion.ops.module.asset.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 命令执行参数 schema 对象
*
* @author Jiahang Li
* @version 1.0.0
* @since 2024/3/15 14:50
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "ExecParameterSchemaDTO", description = "命令执行参数 schema 对象")
public class ExecParameterSchemaDTO {
@Schema(description = "参数名称")
private String name;
@Schema(description = "参数描述")
private String desc;
@Schema(description = "默认值")
private Object defaultValue;
@Schema(description = "")
private Object value;
}

View File

@@ -1,43 +0,0 @@
package com.orion.ops.module.asset.entity.dto;
import com.orion.lang.define.cache.key.model.LongCacheIdModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 执行模板 缓存对象
*
* @author Jiahang Li
* @version 1.0.1
* @since 2024-3-7 18:08
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "ExecTemplateCacheDTO", description = "执行模板 缓存对象")
public class ExecTemplateCacheDTO implements LongCacheIdModel, Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
private Long id;
@Schema(description = "名称")
private String name;
@Schema(description = "命令")
private String command;
@Schema(description = "超时时间秒 0不超时")
private Integer timeout;
@Schema(description = "参数")
private String parameter;
}

View File

@@ -34,10 +34,6 @@ public class ExecCommandRequest {
@Schema(description = "执行命令") @Schema(description = "执行命令")
private String command; private String command;
@NotBlank
@Schema(description = "执行参数")
private String parameter;
@NotBlank @NotBlank
@Schema(description = "参数 schema") @Schema(description = "参数 schema")
private String parameterSchema; private String parameterSchema;

View File

@@ -8,6 +8,7 @@ import lombok.NoArgsConstructor;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* 批量执行日志 视图响应对象 * 批量执行日志 视图响应对象
@@ -55,4 +56,7 @@ public class ExecLogVO implements Serializable {
@Schema(description = "执行完成时间") @Schema(description = "执行完成时间")
private Date finishTime; private Date finishTime;
@Schema(description = "执行主机id")
private List<Long> hostIdList;
} }

View File

@@ -24,6 +24,14 @@ public interface ExecLogService {
*/ */
DataGrid<ExecLogVO> getExecLogPage(ExecLogQueryRequest request); DataGrid<ExecLogVO> getExecLogPage(ExecLogQueryRequest request);
/**
* 获取执行历史
*
* @param request request
* @return history
*/
List<ExecLogVO> getExecHistory(ExecLogQueryRequest request);
/** /**
* 获取执行日志状态 * 获取执行日志状态
* *

View File

@@ -6,8 +6,6 @@ import com.orion.ops.module.asset.entity.request.exec.ExecTemplateQueryRequest;
import com.orion.ops.module.asset.entity.request.exec.ExecTemplateUpdateRequest; import com.orion.ops.module.asset.entity.request.exec.ExecTemplateUpdateRequest;
import com.orion.ops.module.asset.entity.vo.ExecTemplateVO; import com.orion.ops.module.asset.entity.vo.ExecTemplateVO;
import java.util.List;
/** /**
* 执行模板 服务类 * 执行模板 服务类
* *
@@ -41,13 +39,6 @@ public interface ExecTemplateService {
*/ */
ExecTemplateVO getExecTemplateById(Long id); ExecTemplateVO getExecTemplateById(Long id);
/**
* 通过缓存查询执行模板
*
* @return rows
*/
List<ExecTemplateVO> getExecTemplateListByCache();
/** /**
* 分页查询执行模板 * 分页查询执行模板
* *

View File

@@ -28,6 +28,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -64,6 +65,41 @@ public class ExecLogServiceImpl implements ExecLogService {
.dataGrid(ExecLogConvert.MAPPER::to); .dataGrid(ExecLogConvert.MAPPER::to);
} }
@Override
public List<ExecLogVO> getExecHistory(ExecLogQueryRequest request) {
// 查询执行记录
List<ExecLogVO> logs = execLogDAO.of()
.createWrapper()
.eq(ExecLogDO::getSource, request.getSource())
.eq(ExecLogDO::getUserId, request.getUserId())
.groupBy(ExecLogDO::getDescription)
.orderByDesc(ExecLogDO::getId)
.then()
.limit(request)
.list(ExecLogConvert.MAPPER::to);
if (logs.isEmpty()) {
return logs;
}
List<Long> logIdList = logs.stream()
.map(ExecLogVO::getId)
.collect(Collectors.toList());
// 设置执行主机id
Map<Long, List<Long>> hostIdRel = execHostLogDAO.of()
.createWrapper()
.in(ExecHostLogDO::getLogId, logIdList)
.then()
.stream()
.collect(Collectors.groupingBy(
ExecHostLogDO::getLogId,
Collectors.mapping(
ExecHostLogDO::getHostId,
Collectors.toList()
)
));
logs.forEach(s -> s.setHostIdList(hostIdRel.get(s.getId())));
return logs;
}
@Override @Override
public ExecLogStatusVO getExecLogStatus(List<Long> idList) { public ExecLogStatusVO getExecLogStatus(List<Long> idList) {
// 查询执行状态 // 查询执行状态

View File

@@ -1,10 +1,11 @@
package com.orion.ops.module.asset.service.impl; package com.orion.ops.module.asset.service.impl;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.orion.lang.function.Functions;
import com.orion.lang.id.UUIds; import com.orion.lang.id.UUIds;
import com.orion.lang.utils.Strings; import com.orion.lang.utils.Strings;
import com.orion.lang.utils.collect.Lists;
import com.orion.lang.utils.collect.Maps; import com.orion.lang.utils.collect.Maps;
import com.orion.lang.utils.json.matcher.NoMatchStrategy; import com.orion.lang.utils.json.matcher.NoMatchStrategy;
import com.orion.lang.utils.json.matcher.ReplacementFormatter; import com.orion.lang.utils.json.matcher.ReplacementFormatter;
@@ -23,6 +24,7 @@ import com.orion.ops.module.asset.dao.HostDAO;
import com.orion.ops.module.asset.entity.domain.ExecHostLogDO; import com.orion.ops.module.asset.entity.domain.ExecHostLogDO;
import com.orion.ops.module.asset.entity.domain.ExecLogDO; import com.orion.ops.module.asset.entity.domain.ExecLogDO;
import com.orion.ops.module.asset.entity.domain.HostDO; import com.orion.ops.module.asset.entity.domain.HostDO;
import com.orion.ops.module.asset.entity.dto.ExecParameterSchemaDTO;
import com.orion.ops.module.asset.entity.request.exec.ExecCommandRequest; import com.orion.ops.module.asset.entity.request.exec.ExecCommandRequest;
import com.orion.ops.module.asset.entity.vo.ExecCommandHostVO; import com.orion.ops.module.asset.entity.vo.ExecCommandHostVO;
import com.orion.ops.module.asset.entity.vo.ExecCommandVO; import com.orion.ops.module.asset.entity.vo.ExecCommandVO;
@@ -109,7 +111,7 @@ public class ExecServiceImpl implements ExecService {
execLogDAO.insert(execLog); execLogDAO.insert(execLog);
Long execId = execLog.getId(); Long execId = execLog.getId();
// 获取内置参数 // 获取内置参数
Map<String, Object> builtinsParams = this.getBaseBuiltinsParams(user, execId, request.getParameter()); Map<String, Object> builtinsParams = this.getBaseBuiltinsParams(user, execId, request.getParameterSchema());
// 设置主机日志 // 设置主机日志
List<ExecHostLogDO> execHostLogs = hosts.stream() List<ExecHostLogDO> execHostLogs = hosts.stream()
.map(s -> { .map(s -> {
@@ -159,7 +161,6 @@ public class ExecServiceImpl implements ExecService {
.description(execLog.getDescription()) .description(execLog.getDescription())
.timeout(execLog.getTimeout()) .timeout(execLog.getTimeout())
.command(execLog.getCommand()) .command(execLog.getCommand())
.parameter(hostLogs.get(0).getParameter())
.parameterSchema(execLog.getParameterSchema()) .parameterSchema(execLog.getParameterSchema())
.hostIdList(hostIdList) .hostIdList(hostIdList)
.build(); .build();
@@ -298,19 +299,37 @@ public class ExecServiceImpl implements ExecService {
} }
/** /**
* 获取基础内置参数 * 提取参数
* *
* @param user user * @param parameterSchema parameterSchema
* @param execId execId
* @return params * @return params
*/ */
private Map<String, Object> getBaseBuiltinsParams(LoginUser user, Long execId, String inputParam) { private Map<String, Object> extraSchemaParams(String parameterSchema) {
List<ExecParameterSchemaDTO> schemaList = JSON.parseArray(parameterSchema, ExecParameterSchemaDTO.class);
if (Lists.isEmpty(schemaList)) {
return Maps.newMap();
}
// 解析参数
return schemaList.stream()
.collect(Collectors.toMap(ExecParameterSchemaDTO::getName,
ExecParameterSchemaDTO::getValue,
Functions.right()));
}
/**
* 获取基础内置参数
*
* @param user user
* @param execId execId
* @param parameterSchema parameterSchema
* @return params
*/
private Map<String, Object> getBaseBuiltinsParams(LoginUser user, Long execId, String parameterSchema) {
String uuid = UUIds.random(); String uuid = UUIds.random();
Date date = new Date(); Date date = new Date();
// 输入参数 // 输入参数
JSONObject inputParams = JSON.parseObject(inputParam); Map<String, Object> params = this.extraSchemaParams(parameterSchema);
// 内置参数 // 添加内置参数
Map<String, Object> params = Maps.newMap(inputParams);
params.put("userId", user.getId()); params.put("userId", user.getId());
params.put("username", user.getId()); params.put("username", user.getId());
params.put("execId", execId); params.put("execId", execId);

View File

@@ -5,13 +5,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.orion.lang.define.wrapper.DataGrid; import com.orion.lang.define.wrapper.DataGrid;
import com.orion.ops.framework.common.constant.ErrorMessage; import com.orion.ops.framework.common.constant.ErrorMessage;
import com.orion.ops.framework.common.utils.Valid; import com.orion.ops.framework.common.utils.Valid;
import com.orion.ops.framework.redis.core.utils.RedisMaps;
import com.orion.ops.framework.redis.core.utils.barrier.CacheBarriers;
import com.orion.ops.module.asset.convert.ExecTemplateConvert; import com.orion.ops.module.asset.convert.ExecTemplateConvert;
import com.orion.ops.module.asset.dao.ExecTemplateDAO; import com.orion.ops.module.asset.dao.ExecTemplateDAO;
import com.orion.ops.module.asset.define.cache.ExecTemplateCacheKeyDefine;
import com.orion.ops.module.asset.entity.domain.ExecTemplateDO; import com.orion.ops.module.asset.entity.domain.ExecTemplateDO;
import com.orion.ops.module.asset.entity.dto.ExecTemplateCacheDTO;
import com.orion.ops.module.asset.entity.request.exec.ExecTemplateCreateRequest; import com.orion.ops.module.asset.entity.request.exec.ExecTemplateCreateRequest;
import com.orion.ops.module.asset.entity.request.exec.ExecTemplateQueryRequest; import com.orion.ops.module.asset.entity.request.exec.ExecTemplateQueryRequest;
import com.orion.ops.module.asset.entity.request.exec.ExecTemplateUpdateRequest; import com.orion.ops.module.asset.entity.request.exec.ExecTemplateUpdateRequest;
@@ -21,9 +17,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/** /**
* 执行模板 服务实现类 * 执行模板 服务实现类
@@ -50,8 +43,6 @@ public class ExecTemplateServiceImpl implements ExecTemplateService {
int effect = execTemplateDAO.insert(record); int effect = execTemplateDAO.insert(record);
Long id = record.getId(); Long id = record.getId();
log.info("ExecTemplateService-createExecTemplate id: {}, effect: {}", id, effect); log.info("ExecTemplateService-createExecTemplate id: {}, effect: {}", id, effect);
// 删除缓存
RedisMaps.delete(ExecTemplateCacheKeyDefine.EXEC_TEMPLATE);
return id; return id;
} }
@@ -69,8 +60,6 @@ public class ExecTemplateServiceImpl implements ExecTemplateService {
// 更新 // 更新
int effect = execTemplateDAO.updateById(updateRecord); int effect = execTemplateDAO.updateById(updateRecord);
log.info("ExecTemplateService-updateExecTemplateById effect: {}", effect); log.info("ExecTemplateService-updateExecTemplateById effect: {}", effect);
// 删除缓存
RedisMaps.delete(ExecTemplateCacheKeyDefine.EXEC_TEMPLATE);
return effect; return effect;
} }
@@ -83,27 +72,6 @@ public class ExecTemplateServiceImpl implements ExecTemplateService {
return ExecTemplateConvert.MAPPER.to(record); return ExecTemplateConvert.MAPPER.to(record);
} }
@Override
public List<ExecTemplateVO> getExecTemplateListByCache() {
// 查询缓存
List<ExecTemplateCacheDTO> list = RedisMaps.valuesJson(ExecTemplateCacheKeyDefine.EXEC_TEMPLATE);
if (list.isEmpty()) {
// 查询数据库
list = execTemplateDAO.of().list(ExecTemplateConvert.MAPPER::toCache);
// 设置屏障 防止穿透
CacheBarriers.checkBarrier(list, ExecTemplateCacheDTO::new);
// 设置缓存
RedisMaps.putAllJson(ExecTemplateCacheKeyDefine.EXEC_TEMPLATE, s -> s.getId().toString(), list);
}
// 删除屏障
CacheBarriers.removeBarrier(list);
// 转换
return list.stream()
.map(ExecTemplateConvert.MAPPER::to)
.sorted(Comparator.comparing(ExecTemplateVO::getId).reversed())
.collect(Collectors.toList());
}
@Override @Override
public DataGrid<ExecTemplateVO> getExecTemplatePage(ExecTemplateQueryRequest request) { public DataGrid<ExecTemplateVO> getExecTemplatePage(ExecTemplateQueryRequest request) {
// 条件 // 条件
@@ -123,8 +91,6 @@ public class ExecTemplateServiceImpl implements ExecTemplateService {
// 删除 // 删除
int effect = execTemplateDAO.deleteById(id); int effect = execTemplateDAO.deleteById(id);
log.info("ExecTemplateService-deleteExecTemplateById id: {}, effect: {}", id, effect); log.info("ExecTemplateService-deleteExecTemplateById id: {}, effect: {}", id, effect);
// 删除缓存
RedisMaps.delete(ExecTemplateCacheKeyDefine.EXEC_TEMPLATE, id);
return effect; return effect;
} }

View File

@@ -28,6 +28,7 @@ export interface ExecLogQueryResponse extends TableData, ExecLogQueryExtraRespon
status: string; status: string;
startTime: number; startTime: number;
finishTime: number; finishTime: number;
hostIdList: Array<number>;
} }
/** /**
@@ -88,6 +89,13 @@ export function getExecLogStatus(idList: Array<number>) {
}); });
} }
/**
* 查询历史执行记录
*/
export function getExecLogHistory(limit: number) {
return axios.get<Array<ExecLogQueryResponse>>('/asset/exec-log/history', { params: { page: 1, limit } });
}
/** /**
* 删除执行记录 * 删除执行记录
*/ */

View File

@@ -64,13 +64,6 @@ export function getExecTemplate(id: number) {
return axios.get<ExecTemplateQueryResponse>('/asset/exec-template/get', { params: { id } }); return axios.get<ExecTemplateQueryResponse>('/asset/exec-template/get', { params: { id } });
} }
/**
* 查询全部执行模板
*/
export function getExecTemplateList() {
return axios.get<Array<ExecTemplateQueryResponse>>('/asset/exec-template/list');
}
/** /**
* 分页查询执行模板 * 分页查询执行模板
*/ */

View File

@@ -8,7 +8,6 @@ export interface ExecCommandRequest {
description?: string; description?: string;
timeout?: number; timeout?: number;
command?: string; command?: string;
parameter?: string;
parameterSchema?: string; parameterSchema?: string;
hostIdList?: number[]; hostIdList?: number[];
} }

View File

@@ -0,0 +1,165 @@
<template>
<a-modal v-model:visible="visible"
title-align="start"
title="执行模板"
width="86%"
:top="80"
:body-style="{padding: '0 8px'}"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:footer="false"
@close="handleClose">
<!-- 搜索 -->
<a-card class="general-card table-search-card"
style="margin-bottom: 0;">
<query-header :model="formModel"
label-align="left"
@submit="fetchTableData"
@reset="fetchTableData"
@keyup.enter="() => fetchTableData()">
<!-- id -->
<a-form-item field="id" label="id">
<a-input-number v-model="formModel.id"
placeholder="请输入id"
allow-clear
hide-button />
</a-form-item>
<!-- 模板名称 -->
<a-form-item field="name" label="模板名称">
<a-input v-model="formModel.name"
placeholder="请输入模板名称"
allow-clear />
</a-form-item>
<!-- 模板命令 -->
<a-form-item field="command" label="模板命令">
<a-input v-model="formModel.command"
placeholder="请输入模板命令"
allow-clear />
</a-form-item>
</query-header>
</a-card>
<!-- 表格 -->
<a-card class="general-card table-card">
<!-- table -->
<a-table row-key="id"
ref="tableRef"
:loading="loading"
:columns="columns"
:data="tableRenderData"
:pagination="pagination"
:scroll="{ x: '100%', y: '60vh' }"
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
@page-size-change="(size) => fetchTableData(1, size)"
:bordered="false">
<!-- 模板名称 -->
<template #name="{ record }">
<span class="span-blue">{{ record.name }}</span>
</template>
<!-- 模板命令 -->
<template #command="{ record }">
<span class="copy-left" @click="copy(record.command, '已复制')">
<icon-copy />
</span>
<span :title="record.command">{{ record.command }}</span>
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 选择 -->
<a-button type="text"
size="mini"
@click="selectedTemplate(record)">
选择
</a-button>
</div>
</template>
</a-table>
</a-card>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'execTemplateModal'
};
</script>
<script lang="ts" setup>
import type { ExecTemplateQueryRequest, ExecTemplateQueryResponse } from '@/api/exec/exec-template';
import { reactive, ref } from 'vue';
import { usePagination } from '@/types/table';
import useVisible from '@/hooks/visible';
import useLoading from '@/hooks/loading';
import useCopy from '@/hooks/copy';
import columns from './table.columns';
import { getExecTemplatePage } from '@/api/exec/exec-template';
const emits = defineEmits(['selected']);
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const { copy } = useCopy();
const pagination = usePagination();
const tableRenderData = ref<ExecTemplateQueryResponse[]>([]);
const formModel = reactive<ExecTemplateQueryRequest>({
id: undefined,
name: undefined,
command: undefined,
});
// 打开
const open = () => {
setVisible(true);
// 加载数据
if (!tableRenderData.value.length) {
fetchTableData();
}
};
defineExpose({ open });
// 选择模板
const selectedTemplate = (record: ExecTemplateQueryResponse) => {
emits('selected', record);
handleClose();
};
// 加载数据
const doFetchTableData = async (request: ExecTemplateQueryRequest) => {
try {
setLoading(true);
const { data } = await getExecTemplatePage(request);
tableRenderData.value = data.rows;
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;
} catch (e) {
} finally {
setLoading(false);
}
};
// 切换页码
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
doFetchTableData({ page, limit, ...form });
};
// 关闭回调
const handleClose = () => {
handleClear();
};
// 清空
const handleClear = () => {
setLoading(false);
setVisible(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,43 @@
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import { dateFormat } from '@/utils';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 70,
align: 'left',
fixed: 'left',
}, {
title: '模板名称',
dataIndex: 'name',
slotName: 'name',
align: 'left',
width: 200,
ellipsis: true,
}, {
title: '模板命令',
dataIndex: 'command',
slotName: 'command',
align: 'left',
ellipsis: true,
}, {
title: '修改时间',
dataIndex: 'updateTime',
slotName: 'updateTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.updateTime));
},
}, {
title: '操作',
slotName: 'handle',
width: 80,
align: 'center',
fixed: 'right',
},
] as TableColumnData[];
export default columns;

View File

@@ -76,7 +76,6 @@
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import type { HistoryValueQueryRequest, HistoryValueQueryResponse } from '@/api/meta/history-value'; import type { HistoryValueQueryRequest, HistoryValueQueryResponse } from '@/api/meta/history-value';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
@@ -84,54 +83,9 @@
import { getHistoryValuePage } from '@/api/meta/history-value'; import { getHistoryValuePage } from '@/api/meta/history-value';
import { usePagination } from '@/types/table'; import { usePagination } from '@/types/table';
import useCopy from '@/hooks/copy'; import useCopy from '@/hooks/copy';
import { dateFormat } from '@/utils'; import columns from './table.columns';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 70,
align: 'left',
fixed: 'left',
}, {
title: '修改前',
dataIndex: 'beforeValue',
slotName: 'beforeValue',
align: 'left',
ellipsis: true,
tooltip: true,
}, {
title: '修改后',
dataIndex: 'afterValue',
slotName: 'afterValue',
align: 'left',
ellipsis: true,
tooltip: true,
}, {
title: '修改时间',
dataIndex: 'createTime',
slotName: 'createTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.createTime));
},
}, {
title: '修改人',
dataIndex: 'creator',
slotName: 'creator',
width: 80,
}, {
title: '操作',
slotName: 'handle',
width: 80,
align: 'center',
fixed: 'right',
},
] as TableColumnData[];
const props = defineProps({ const props = defineProps({
type: String, type: String,
rollback: Function rollback: Function

View File

@@ -0,0 +1,49 @@
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import { dateFormat } from '@/utils';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 70,
align: 'left',
fixed: 'left',
}, {
title: '修改前',
dataIndex: 'beforeValue',
slotName: 'beforeValue',
align: 'left',
ellipsis: true,
tooltip: true,
}, {
title: '修改后',
dataIndex: 'afterValue',
slotName: 'afterValue',
align: 'left',
ellipsis: true,
tooltip: true,
}, {
title: '修改时间',
dataIndex: 'createTime',
slotName: 'createTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.createTime));
},
}, {
title: '修改人',
dataIndex: 'creator',
slotName: 'creator',
width: 80,
}, {
title: '操作',
slotName: 'handle',
width: 80,
align: 'center',
fixed: 'right',
},
] as TableColumnData[];
export default columns;

View File

@@ -1,8 +1,9 @@
// 模板参数 // 模板参数
export interface TemplateParam { export interface TemplateParam {
name?: string; name?: string;
default?: string;
desc?: string; desc?: string;
defaultValue?: any;
value?: any;
} }
// 内置参数 // 内置参数

View File

@@ -0,0 +1,19 @@
<template>
<div>
his
</div>
</template>
<script lang="ts">
export default {
name: 'execHistory'
};
</script>
<script lang="ts" setup>
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,365 @@
<template>
<!-- 命令执行 -->
<a-spin class="exec-container" :loading="loading">
<!-- 执行参数 -->
<div class="exec-form-container">
<!-- 表头 -->
<div class="exec-form-header">
<h3>执行参数</h3>
<!-- 操作 -->
<a-button-group size="small">
<a-button @click="reset">重置</a-button>
<a-button type="primary" @click="exec">执行</a-button>
</a-button-group>
</div>
<!-- 命令表单 -->
<a-form :model="formModel"
ref="formRef"
label-align="right"
:rules="formRules">
<!-- 执行主机 -->
<a-form-item field="hostIdList"
label="执行主机"
label-col-flex="72px">
<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="执行描述"
label-col-flex="72px">
<a-input v-model="formModel.description"
placeholder="请输入执行描述"
allow-clear />
</a-form-item>
<!-- 超时时间 -->
<a-form-item field="timeout"
label="超时时间"
label-col-flex="72px">
<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>
<!-- 命令参数 -->
<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">
<a-form-item v-for="item in parameterSchema"
:key="item.name"
:field="item.name as string"
:label="item.name"
label-col-flex="72px"
required>
<a-input v-model="parameterFormModel[item.name as string]"
:placeholder="item.desc"
allow-clear />
</a-form-item>
</a-form>
</div>
<!-- 执行命令 -->
<div class="exec-command-container">
<!-- 表头 -->
<div class="exec-form-header">
<h3>执行命令</h3>
<span class="span-blue usn pointer" @click="openTemplate">从模板中选择</span>
</div>
<!-- 命令编辑器 -->
<div class="command-editor-wrapper">
<exec-editor v-model="formModel.command"
theme="vs-dark"
:parameter="parameterSchema" />
</div>
<!-- 命名提示信息 -->
<div v-pre class="command-editor-help">
使用 @{{ xxx }} 来替换参数, 输入_可以获取全部变量
</div>
</div>
<!-- 执行历史 -->
<div class="exec-history-container">
<div v-if="!historyLogs.length" class="flex-center mt16">
<a-empty description="无执行记录" />
</div>
<div v-else class="exec-history-rows">
<div v-for="record in historyLogs"
:key="record.id"
class="exec-history">
<!-- 机器数量 -->
<span class="exec-history-count">
{{ record.hostIdList?.length || 0 }}
</span>
<!-- 执行描述 -->
<span class="exec-history-desc">
{{ record.description }}
</span>
</div>
</div>
</div>
<!-- 主机模态框 -->
<authorized-host-modal ref="hostModal"
@selected="setSelectedHost" />
<!-- 命令模板模态框 -->
<exec-template-modal ref="templateModal"
@selected="setWithTemplate" />
</a-spin>
</template>
<script lang="ts">
export default {
name: 'execPanel'
};
</script>
<script lang="ts" setup>
import type { ExecCommandRequest } from '@/api/exec/exec';
import type { TemplateParam } from '@/components/view/exec-editor/const';
import type { ExecTemplateQueryResponse } from '@/api/exec/exec-template';
import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
import { onMounted, ref } from 'vue';
import formRules from '../types/form.rules';
import useLoading from '@/hooks/loading';
import { batchExecCommand } from '@/api/exec/exec';
import { historyCount } from '../types/const';
import { getExecLogHistory } from '@/api/exec/exec-log';
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 ExecTemplateModal from '@/components/exec/template/modal/index.vue';
const defaultForm = (): ExecCommandRequest => {
return {
timeout: 0
};
};
const { loading, setLoading } = useLoading();
const hostModal = ref<any>();
const templateModal = ref<any>();
const formRef = ref<any>();
const parameterFormRef = ref<any>();
const formModel = ref<ExecCommandRequest>({ ...defaultForm() });
const parameterFormModel = ref<Record<string, any>>({});
const parameterSchema = ref<Array<TemplateParam>>([]);
const historyLogs = ref<Array<ExecLogQueryResponse>>([]);
// 加载执行记录
const fetchExecHistory = async () => {
const { data } = await getExecLogHistory(historyCount);
historyLogs.value = data;
};
// 打开选择主机
const openSelectHost = () => {
hostModal.value.open(formModel.value.hostIdList);
};
// 打开模板
const openTemplate = () => {
templateModal.value.open();
};
// 设置选中主机
const setSelectedHost = (hosts: Array<number>) => {
formModel.value.hostIdList = hosts;
};
// 从执行模板设置
const setWithTemplate = (record: ExecTemplateQueryResponse) => {
formModel.value = {
...formModel.value,
command: record.command,
description: record.name,
timeout: record.timeout,
};
parameterSchema.value = record.parameter ? JSON.parse(record.parameter) : [];
parameterFormModel.value = {};
};
// 从执行日志设置
const setWithExecLog = () => {
// TODO
};
// 执行
const exec = async () => {
setLoading(true);
try {
// 验证参数
let error = await formRef.value.validate();
if (error) {
return false;
}
error = await parameterFormRef.value?.validate();
if (error) {
return false;
}
if (!formModel.value.command) {
Message.error('请输入命令');
return false;
}
// 设置 schema
for (let ps of parameterSchema.value) {
ps.value = parameterFormModel.value[ps.name as string];
}
// 执行命令
const { data } = await batchExecCommand({
...formModel.value,
parameterSchema: JSON.stringify(parameterSchema.value),
});
// TODO log history
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 重置
const reset = () => {
formModel.value = Object.assign({}, { ...defaultForm() });
parameterFormModel.value = {};
parameterSchema.value = [];
};
// 加载执行记录
onMounted(fetchExecHistory);
</script>
<style lang="less" scoped>
@form-width: 420px;
@history-width: 320px;
@command-gap: @form-width + @history-width + 32px;
.exec-container {
display: flex;
justify-content: space-between;
width: 100%;
height: 100%;
.exec-form-container {
width: @form-width;
}
.exec-command-container {
width: calc(100% - @command-gap);
}
.exec-history-container {
width: @history-width;
}
.exec-form-container, .exec-command-container, .exec-history-container {
background: var(--color-bg-2);
border-radius: 4px;
height: 100%;
padding: 16px;
position: relative;
}
.exec-form-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
height: 28px;
margin-bottom: 12px;
h3 {
margin: 0;
}
}
}
.exec-form-container {
.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;
}
}
}
.exec-command-container {
background: red;
.command-editor-wrapper {
width: 100%;
height: calc(100% - 66px);
position: relative;
}
.command-editor-help {
user-select: none;
display: flex;
margin-top: 8px;
height: 18px;
color: var(--color-text-3);
}
}
.exec-history-container {
.exec-history-rows {
overflow-y: auto;
}
.exec-history {
padding: 6px 8px;
display: flex;
justify-content: space-between;
margin-bottom: 8px;
background: var(--color-fill-2);
transition: all .2s;
user-select: none;
&:hover {
background: var(--color-fill-3);
}
&-count {
width: 24px;
height: 24px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-bg-2);
background: rgb(var(--arcoblue-6));
}
&-desc {
}
}
}
</style>

View File

@@ -1,6 +1,7 @@
<template> <template>
<div class="layout-container"> <div class="layout-container full">
<!-- 执行面板 -->
<exec-panel />
</div> </div>
</template> </template>
@@ -11,6 +12,7 @@
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import ExecPanel from './components/exec-panel.vue';
</script> </script>

View File

@@ -0,0 +1,2 @@
// 执行
export const historyCount = 20;

View File

@@ -132,7 +132,7 @@
parameterSchema.value = JSON.parse(record.parameter); parameterSchema.value = JSON.parse(record.parameter);
const params = {} as any; const params = {} as any;
for (let param of parameterSchema.value) { for (let param of parameterSchema.value) {
params[param.name as keyof any] = param.default; params[param.name as keyof any] = param.defaultValue;
} }
parameterFormModel.value = params; parameterFormModel.value = params;
@@ -167,10 +167,13 @@
if (error) { if (error) {
return false; return false;
} }
// 设置 schema
for (let ps of parameterSchema.value) {
ps.value = parameterFormModel.value[ps.name as string];
}
// 执行命令 // 执行命令
await batchExecCommand({ await batchExecCommand({
...formModel.value, ...formModel.value,
parameter: JSON.stringify(parameterFormModel.value),
parameterSchema: JSON.stringify(parameterSchema.value), parameterSchema: JSON.stringify(parameterSchema.value),
}); });
Message.success('已开始执行'); Message.success('已开始执行');

View File

@@ -65,7 +65,7 @@
placeholder="参数名称 (必填)" placeholder="参数名称 (必填)"
allow-clear /> allow-clear />
<a-input class="parameter-item-default" <a-input class="parameter-item-default"
v-model="item.default" v-model="item.defaultValue"
placeholder="默认值 (非必填)" placeholder="默认值 (非必填)"
allow-clear /> allow-clear />
<a-input class="parameter-item-description" <a-input class="parameter-item-description"