🔨 批量执行.

This commit is contained in:
lijiahangmax
2024-03-21 01:32:25 +08:00
parent eb9e184d79
commit 2a144cfa57
12 changed files with 165 additions and 102 deletions

View File

@@ -2,14 +2,16 @@
## v1.0.2 ## v1.0.2
`2024-03-` `release` `2024-03-22` `release`
* 🐞 修复 SFTP 加载失败后一直 loading * 🐞 修复 SFTP 加载失败后一直 loading
* 🐞 修复 主机终端搜索框报错
* 🐞 修复 SSH 配置未启用还可以连接 * 🐞 修复 SSH 配置未启用还可以连接
* 🐞 修复 主机配置保存后无法修改状态 * 🐞 修复 主机配置保存后无法修改状态
* 🐞 修复 添加快捷命令时编辑器无代码提示 * 🐞 修复 添加快捷命令时编辑器无代码提示
* 🔨 修改 菜单路由命名逻辑修改 * 🔨 修改 菜单路由命名逻辑修改
* 🔨 优化 前端组件命名规范化 * 🔨 优化 前端组件命名规范化
* 🔨 优化 前端 emit 命名规范化
* 🌈 新增 双击终端会话 Tab 快速复制 * 🌈 新增 双击终端会话 Tab 快速复制
* 🌈 新增 批量执行命令 * 🌈 新增 批量执行命令
* 🌈 新增 命令执行日志 * 🌈 新增 命令执行日志

View File

@@ -77,7 +77,7 @@ export function getExecLogPage(request: ExecLogQueryRequest) {
* 查询执行记录 * 查询执行记录
*/ */
export function getExecLog(id: number) { export function getExecLog(id: number) {
return axios.get<ExecLogQueryResponse>('/asset/exec-log/query', { params: { id } }); return axios.get<ExecLogQueryResponse>('/asset/exec-log/get', { params: { id } });
} }
/** /**

View File

@@ -3,17 +3,21 @@
title-align="start" title-align="start"
title="执行日志" title="执行日志"
width="94%" width="94%"
:top="80" :top="40"
:body-style="{ padding: '0' }" :body-style="{ padding: '0', height: 'calc(100vh - 140px)', overflow: 'hidden' }"
:align-center="false" :align-center="false"
:draggable="true" :draggable="true"
:mask-closable="false" :mask-closable="false"
:unmount-on-close="true" :unmount-on-close="true"
:footer="false" :footer="false"
@close="handleClose"> @close="handleClose">
<a-spin class="modal-body" :loading="loading"> <a-spin v-if="visible"
<!-- 日志面板 --> class="modal-body"
<exec-log-panel ref="log" :visible-back="false" /> :loading="loading">
<div class="panel-wrapper">
<!-- 日志面板 -->
<exec-log-panel ref="log" :visible-back="false" />
</div>
</a-spin> </a-spin>
</a-modal> </a-modal>
</template> </template>
@@ -36,7 +40,7 @@
const log = ref(); const log = ref();
// TODO 测试卸载 // TODO 卸载
// 打开 // 打开
const open = async (id: number) => { const open = async (id: number) => {
@@ -46,16 +50,18 @@
// 获取执行日志 // 获取执行日志
const { data } = await getExecLog(id); const { data } = await getExecLog(id);
// 打开日志 // 打开日志
nextTick(() => { await nextTick(() => {
log.value.open(data); log.value.open(data);
}); });
} catch (e) { } catch (e) {
} finally {
setVisible(false); setVisible(false);
} finally {
setLoading(false); setLoading(false);
} }
}; };
defineExpose({ open });
// 关闭回调 // 关闭回调
const handleClose = () => { const handleClose = () => {
handleClear(); handleClear();
@@ -65,6 +71,7 @@
const handleClear = () => { const handleClear = () => {
setLoading(false); setLoading(false);
setVisible(false); setVisible(false);
console.log('clear');
}; };
</script> </script>
@@ -72,6 +79,39 @@
<style lang="less" scoped> <style lang="less" scoped>
.modal-body { .modal-body {
width: 100%; width: 100%;
height: calc(100vh - 140px); height: 100%;
position: relative;
padding: 0 12px 12px 12px;
} }
.panel-wrapper {
width: 100%;
height: 100%;
position: relative;
}
:deep(.exec-host-container) {
padding: 0;
.host-header {
height: 38px;
margin: 0;
h3 {
height: 100%;
display: flex;
align-items: center;
}
}
.exec-host-items {
width: 100%;
height: calc(100% - 38px);
}
}
:deep(.log-header) {
padding: 0;
}
</style> </style>

View File

@@ -2,7 +2,7 @@
<!-- 执行主机 --> <!-- 执行主机 -->
<div class="container"> <div class="container">
<!-- 表头 --> <!-- 表头 -->
<div class="panel-header"> <div class="host-header">
<h3>执行主机</h3> <h3>执行主机</h3>
<!-- 操作 --> <!-- 操作 -->
<a-button v-if="visibleBack" <a-button v-if="visibleBack"
@@ -12,7 +12,7 @@
</a-button> </a-button>
</div> </div>
<!-- 主机列表 --> <!-- 主机列表 -->
<div class="exec-host-container"> <div class="exec-host-items">
<div v-for="item in hosts" <div v-for="item in hosts"
:key="item.id" :key="item.id"
class="exec-host-item" class="exec-host-item"
@@ -57,8 +57,27 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.exec-host-container {
margin-top: 4px; .host-header {
width: 100%;
height: 28px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: flex-start;
h3, > span {
margin: 0;
overflow: hidden;
white-space: nowrap;
}
h3 {
color: var(--color-text-1);
}
}
.exec-host-items {
position: absolute; position: absolute;
width: calc(100% - 32px); width: calc(100% - 32px);
height: calc(100% - 68px); height: calc(100% - 68px);

View File

@@ -1,17 +1,18 @@
<template> <template>
<div class="log-panel-container" v-if="command"> <div class="log-panel-container" v-if="execLog && appender">
<!-- 执行主机 --> <!-- 执行主机 -->
<exec-host class="exec-host-container" <exec-host class="exec-host-container"
:visibleBack="visibleBack" :visibleBack="visibleBack"
:current="currentHostExecId" :current="currentHostExecId"
:hosts="command.hosts" :hosts="execLog.hosts"
@selected="selectedHost" @selected="selectedHost"
@back="emits('back')" /> @back="emits('back')" />
<!-- 日志容器 --> <!-- 日志容器 -->
<log-view ref="logView" <log-view ref="logViewRef"
class="log-view-container" class="log-view-container"
:current="currentHostExecId" :current="currentHostExecId"
:command="command" /> :hosts="execLog.hosts"
:appender="appender" />
</div> </div>
</template> </template>
@@ -23,11 +24,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ExecLogQueryResponse } from '@/api/exec/exec-log'; import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
import type { ILogAppender } from './const';
import { onUnmounted, ref, nextTick } from 'vue'; import { onUnmounted, ref, nextTick } from 'vue';
import { getExecLogStatus } from '@/api/exec/exec-log'; import { getExecLogStatus } from '@/api/exec/exec-log';
import { execHostStatus, execStatus } from './const'; import { execHostStatus, execStatus } from './const';
import ExecHost from './exec-host.vue'; import ExecHost from './exec-host.vue';
import LogView from './log-view.vue'; import LogView from './log-view.vue';
import LogAppender from '@/components/exec/log/panel/log-appender';
const props = defineProps<{ const props = defineProps<{
visibleBack: boolean visibleBack: boolean
@@ -35,15 +38,17 @@
const emits = defineEmits(['back']); const emits = defineEmits(['back']);
const logView = ref(); const logViewRef = ref();
const currentHostExecId = ref(); const currentHostExecId = ref();
const statusIntervalId = ref(); const statusIntervalId = ref();
const finishIntervalId = ref(); const finishIntervalId = ref();
const command = ref<ExecLogQueryResponse>(); const execLog = ref<ExecLogQueryResponse>();
const appender = ref<ILogAppender>();
// 打开 // 打开
const open = (record: ExecLogQueryResponse) => { const open = (record: ExecLogQueryResponse) => {
command.value = record; appender.value = new LogAppender({ execId: record.id });
execLog.value = record;
currentHostExecId.value = record.hosts[0].id; currentHostExecId.value = record.hosts[0].id;
// 定时查询执行状态 // 定时查询执行状态
if (record.status === execStatus.WAITING || if (record.status === execStatus.WAITING ||
@@ -55,24 +60,24 @@
} }
// 打开日志 // 打开日志
nextTick(() => { nextTick(() => {
logView.value?.open(); logViewRef.value?.open();
}); });
}; };
// 加载状态 // 加载状态
const fetchTaskStatus = async () => { const fetchTaskStatus = async () => {
if (!command.value) { if (!execLog.value) {
return; return;
} }
// 加载状态 // 加载状态
const { data: { logList, hostList } } = await getExecLogStatus([command.value.id]); const { data: { logList, hostList } } = await getExecLogStatus([execLog.value.id]);
if (logList.length) { if (logList.length) {
command.value.status = logList[0].status; execLog.value.status = logList[0].status;
command.value.startTime = logList[0].startTime; execLog.value.startTime = logList[0].startTime;
command.value.finishTime = logList[0].finishTime; execLog.value.finishTime = logList[0].finishTime;
} }
// 设置主机状态 // 设置主机状态
for (let host of command.value.hosts) { for (let host of execLog.value.hosts) {
const hostStatus = hostList.find(s => s.id === host.id); const hostStatus = hostList.find(s => s.id === host.id);
if (hostStatus) { if (hostStatus) {
host.status = hostStatus.status; host.status = hostStatus.status;
@@ -85,15 +90,15 @@
} }
} }
// 已完成跳过 // 已完成跳过
if (command.value.status === execStatus.COMPLETED || if (execLog.value.status === execStatus.COMPLETED ||
command.value.status === execStatus.FAILED) { execLog.value.status === execStatus.FAILED) {
closeClient(); closeClient();
} }
}; };
// 设置完成时间 // 设置完成时间
const setTaskFinishTime = () => { const setTaskFinishTime = () => {
const hosts = command.value?.hosts; const hosts = execLog.value?.hosts;
if (!hosts) { if (!hosts) {
return; return;
} }
@@ -118,18 +123,20 @@
// 关闭连接 // 关闭连接
const closeClient = () => { const closeClient = () => {
// 关闭日志
logView.value?.closeClient();
// 清理轮询 // 清理轮询
clearAllInterval(); clearAllInterval();
// 关闭 client
appender.value?.closeClient();
}; };
// 清理并且关闭 // 清理并且关闭
const closeAll = () => { const closeAll = () => {
// 关闭日志 // TODO
logView.value?.closeAll(); console.log('closeAll');
// 清理轮询 // 清理轮询
clearAllInterval(); clearAllInterval();
// 关闭 appender
appender.value?.close();
}; };
// 清理轮询 // 清理轮询

View File

@@ -235,10 +235,10 @@ export default class LogAppender implements ILogAppender {
closeView(): void { closeView(): void {
// 关闭 terminal // 关闭 terminal
Object.values(this.appenderRel).forEach(s => { Object.values(this.appenderRel).forEach(s => {
s.terminal?.dispose();
if (s.addons) { if (s.addons) {
Object.values(s.addons).forEach(s => s.dispose()); Object.values(s.addons).forEach(s => s.dispose());
} }
s.terminal?.dispose();
}); });
// 移除自适应事件 // 移除自适应事件
removeEventListen(window, 'resize', this.fitAllFn); removeEventListen(window, 'resize', this.fitAllFn);
@@ -246,8 +246,14 @@ export default class LogAppender implements ILogAppender {
// 关闭 // 关闭
close(): void { close(): void {
this.closeClient(); try {
this.closeView(); this.closeClient();
this.closeView();
} catch (ex) {
// TODO
console.log('errr');
console.error(ex);
}
} }
// 处理消息 // 处理消息

View File

@@ -211,7 +211,7 @@
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@header-height: 40px; @header-height: 38px;
.log-header { .log-header {
width: 100%; width: 100%;

View File

@@ -1,14 +1,12 @@
<template> <template>
<div class="container"> <div class="container">
<template v-if="appender"> <log-item class="log-item"
<log-item class="log-item" v-show="current === host.id"
v-show="current === host.id" v-for="host in hosts"
v-for="host in command.hosts" :key="host.id"
:key="host.id" :ref="addRef as unknown as VNodeRef"
:ref="addRef as unknown as VNodeRef" :host="host"
:host="host" :appender="appender" />
:appender="appender as ILogAppender" />
</template>
</div> </div>
</template> </template>
@@ -20,25 +18,24 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNodeRef } from 'vue'; import type { VNodeRef } from 'vue';
import type { ExecLogQueryResponse } from '@/api/exec/exec-log'; import type { ExecHostLogQueryResponse } from '@/api/exec/exec-log';
import type { LogDomRef, ILogAppender } from './const'; import type { LogDomRef, ILogAppender } from './const';
import { nextTick, onBeforeMount, ref, watch } from 'vue'; import { nextTick, ref, watch } from 'vue';
import LogAppender from './log-appender';
import LogItem from './log-item.vue'; import LogItem from './log-item.vue';
const props = defineProps<{ const props = defineProps<{
current: number; current: number;
command: ExecLogQueryResponse; hosts: Array<ExecHostLogQueryResponse>;
appender: ILogAppender;
}>(); }>();
const logRefs = ref<Array<LogDomRef>>([]); const logRefs = ref<Array<LogDomRef>>([]);
const appender = ref<ILogAppender>();
// 切换标签 // 切换标签
watch(() => props.current, (val) => { watch(() => props.current, (val) => {
nextTick(() => { nextTick(() => {
setTimeout(() => { setTimeout(() => {
appender.value?.setCurrent(val); props.appender?.setCurrent(val);
}, 50); }, 50);
}); });
}); });
@@ -46,26 +43,16 @@
// 打开 // 打开
const open = () => { const open = () => {
nextTick(async () => { nextTick(async () => {
if (appender.value) { // TODO
console.log(props.appender);
if (props.appender) {
// 初始化 // 初始化
await appender.value.init(logRefs.value); await props.appender.init(logRefs.value);
} }
}); });
}; };
// 关闭客户端 defineExpose({ open });
const closeClient = () => {
appender.value?.closeClient();
};
// 关闭全部
const closeAll = () => {
appender.value?.close();
logRefs.value = [];
appender.value = undefined;
};
defineExpose({ open, closeClient, closeAll });
// 添加 ref // 添加 ref
const addRef = (ref: any) => { const addRef = (ref: any) => {
@@ -81,10 +68,6 @@
}); });
}; };
onBeforeMount(() => {
appender.value = new LogAppender({ execId: props.command.id });
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -112,6 +112,7 @@
const defaultForm = (): ExecCommandRequest => { const defaultForm = (): ExecCommandRequest => {
return { return {
command: '', command: '',
timeout: 0,
}; };
}; };
@@ -220,8 +221,8 @@
@command-gap: @form-width + @history-width + 32px; @command-gap: @form-width + @history-width + 32px;
.exec-container { .exec-container {
width: calc(100% - 32px); width: 100%;
height: calc(100% - 32px); height: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
position: absolute; position: absolute;
@@ -265,4 +266,23 @@
} }
} }
:deep(.panel-header) {
width: 100%;
height: 28px;
margin-bottom: 4px;
display: flex;
justify-content: space-between;
align-items: flex-start;
h3, > span {
margin: 0;
overflow: hidden;
white-space: nowrap;
}
h3 {
color: var(--color-text-1);
}
}
</style> </style>

View File

@@ -1,13 +1,15 @@
<template> <template>
<div class="layout-container full"> <div class="layout-container full">
<!-- 执行面板 --> <!-- 执行面板 -->
<exec-panel v-show="!logVisible" <div v-show="!logVisible" class="panel-wrapper">
@submit="openLog" /> <exec-panel @submit="openLog" />
</div>
<!-- 执行日志 --> <!-- 执行日志 -->
<exec-log-panel v-if="logVisible" <div v-if="logVisible" class="panel-wrapper">
ref="log" <exec-log-panel ref="log"
:visibleBack="true" :visibleBack="true"
@back="setLogVisible(false)" /> @back="setLogVisible(false)" />
</div>
</div> </div>
</template> </template>
@@ -48,23 +50,10 @@
<style lang="less" scoped> <style lang="less" scoped>
:deep(.panel-header) { .panel-wrapper {
width: 100%; width: 100%;
height: 28px; height: 100%;
margin-bottom: 4px; position: relative;
display: flex;
justify-content: space-between;
align-items: flex-start;
h3, > span {
margin: 0;
overflow: hidden;
white-space: nowrap;
}
h3 {
color: var(--color-text-1);
}
} }
</style> </style>

View File

@@ -163,7 +163,7 @@
<a-button v-permission="['asset:exec:exec-command']" <a-button v-permission="['asset:exec:exec-command']"
type="text" type="text"
size="mini" size="mini"
@click="() => emits('viewLog', record.id, $event.ctrlKey)"> @click="(e) => emits('viewLog', record.id, e.ctrlKey)">
日志 日志
</a-button> </a-button>
<!-- 中断 --> <!-- 中断 -->

View File

@@ -31,9 +31,6 @@
import ExecTemplateExecDrawer from './components/exec-template-exec-drawer.vue'; import ExecTemplateExecDrawer from './components/exec-template-exec-drawer.vue';
import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue'; import AuthorizedHostModal from '@/components/asset/host/authorized-host-modal/index.vue';
// TODO TEST 选择主机
// TODO openAdd openUpdate 脊柱
const render = ref(false); const render = ref(false);
const table = ref(); const table = ref();
const drawer = ref(); const drawer = ref();