🔨 执行命令.

This commit is contained in:
lijiahang
2024-03-14 19:46:05 +08:00
parent e98bace51b
commit 6bd2640af7
25 changed files with 900 additions and 181 deletions

View File

@@ -58,7 +58,7 @@
日志
</a-button>
<!-- 中断 -->
<a-popconfirm content="确认要中断命令吗?"
<a-popconfirm content="确认要中断命令吗, 删除后会中断执行?"
position="left"
type="warning"
@ok="interruptedHost(record)">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);