Merge pull request #1060 from MCSManager/abandon

Feat: schedule page
This commit is contained in:
unitwk 2023-11-13 19:36:33 +08:00 committed by GitHub
commit 64a7a2727e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 739 additions and 31 deletions

View File

@ -51,6 +51,7 @@ declare module 'vue' {
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypography: typeof import('ant-design-vue/es')['Typography']
ATypographyParagraph: typeof import('ant-design-vue/es')['TypographyParagraph']

View File

@ -32,6 +32,7 @@ import UserStatusBlock from "@/widgets/UserStatusBlock.vue";
import UserInstanceList from "@/widgets/UserInstanceList.vue";
import ImageManager from "@/widgets/imageManager/index.vue";
import NewImage from "@/widgets/imageManager/NewImage.vue";
import Schedule from "@/widgets/instance/Schedule.vue";
import { NEW_CARD_TYPE } from "../types/index";
import { ROLE } from "./router";
@ -67,7 +68,8 @@ export const LAYOUT_CARD_TYPES: { [key: string]: any } = {
UserStatusBlock,
UserInstanceList,
ImageManager,
NewImage
NewImage,
Schedule
};
export interface NewCardItem extends LayoutCard {

View File

@ -103,6 +103,14 @@ let originRouterConfig: RouterConfig[] = [
]
}
]
},
{
path: "/instances/schedule",
name: t("计划任务"),
component: LayoutContainer,
meta: {
permission: ROLE.USER
}
}
]
},

View File

@ -1,5 +1,11 @@
import { useDefineApi } from "@/stores/useDefineApi";
import type { InstanceDetail, NewInstanceForm, QuickStartTemplate } from "@/types";
import type {
InstanceDetail,
NewInstanceForm,
QuickStartTemplate,
Schedule,
NewScheduleTask
} from "@/types";
import type { IGlobalInstanceConfig } from "../../../../common/global";
import type { InstanceMoreDetail } from "@/hooks/useInstance";
@ -67,6 +73,53 @@ export const stopInstance = useDefineApi<
method: "GET"
});
export const restartInstance = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
},
{
instanceUuid: string;
}
>({
url: "/api/protected_instance/restart",
method: "GET"
});
export const killInstance = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
};
},
{
instanceUuid: string;
}
>({
url: "/api/protected_instance/kill",
method: "GET"
});
export const updateInstance = useDefineApi<
{
params: {
uuid: string;
remote_uuid: string;
task_name: string;
};
data: {
time: number;
};
},
boolean
>({
url: "/api/protected_instance/asynchronous",
method: "POST"
});
// 更新实例设置(普通用户)
export const updateInstanceConfig = useDefineApi<
{
@ -343,3 +396,47 @@ export const batchDelete = useDefineApi<
method: "DELETE",
url: "/api/instance"
});
// 获取计划任务
export const scheduleList = useDefineApi<
{
params: {
remote_uuid: string;
uuid: string;
};
},
Schedule[]
>({
method: "GET",
url: "/api/protected_schedule"
});
// 删除计划任务
export const scheduleDelete = useDefineApi<
{
params: {
remote_uuid: string;
uuid: string;
task_name: string;
};
},
boolean
>({
method: "DELETE",
url: "/api/protected_schedule"
});
// 创建计划任务
export const scheduleCreate = useDefineApi<
{
params: {
remote_uuid: string;
uuid: string;
};
data: NewScheduleTask;
},
boolean
>({
url: "/api/protected_schedule",
method: "POST"
});

View File

@ -80,3 +80,23 @@ ENV LC_ALL=zh_CN.UTF-8
ENV TZ=Asia/Shanghai
WORKDIR /workspace
`;
export const ScheduleAction = {
command: t("发送命令"),
stop: t("停止实例"),
start: t("开启实例"),
restart: t("重启实例"),
kill: t("终止实例")
};
export const ScheduleType = {
1: t("间隔时间性任务"),
2: t("周期时间性任务"),
3: t("指定时间性任务")
};
export enum ScheduleCreateType {
INTERVAL = "1",
CYCLE = "2",
SPECIFY = "3"
}

View File

@ -218,3 +218,21 @@ export interface MountComponent {
destroyComponent: Function;
emitResult: Function;
}
export interface Schedule {
instanceUuid: string;
name: string;
count: number;
time: string;
action: string;
payload: string;
type: number;
}
export interface NewScheduleTask {
name: String;
count: String;
time: String;
action: String;
type: String;
}

View File

@ -3,7 +3,7 @@ import { ref, computed } from "vue";
import type { LayoutCard } from "@/types";
import { arrayFilter } from "../../tools/array";
import { t } from "@/lang/i18n";
import { ArrowRightOutlined, CloudServerOutlined } from "@ant-design/icons-vue";
import { ArrowRightOutlined, CloudServerOutlined, FieldTimeOutlined } from "@ant-design/icons-vue";
import InnerCard from "@/components/InnerCard.vue";
import { LayoutCardHeight } from "../../config/originLayoutConfig";
import { useAppRouters } from "@/hooks/useAppRouters";
@ -80,9 +80,17 @@ const btns = computed(() =>
},
{
title: t("TXT_CODE_b7d026f8"),
icon: CloudServerOutlined,
icon: FieldTimeOutlined,
condition: () => !isGlobalTerminal.value,
click: () => {}
click: () => {
toPage({
path: "/instances/schedule",
query: {
instanceId,
daemonId
}
});
}
},
//
// {

View File

@ -0,0 +1,254 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { t } from "@/lang/i18n";
import { message } from "ant-design-vue";
import { DeleteOutlined, FieldTimeOutlined } from "@ant-design/icons-vue";
import CardPanel from "@/components/CardPanel.vue";
import BetweenMenus from "@/components/BetweenMenus.vue";
import { useLayoutCardTools } from "@/hooks/useCardTools";
import { useAppRouters } from "@/hooks/useAppRouters";
import { scheduleList, scheduleDelete } from "@/services/apis/instance";
import type { LayoutCard, Schedule } from "@/types/index";
import { ScheduleAction, ScheduleType, ScheduleCreateType } from "@/types/const";
import NewSchedule from "@/widgets/instance/dialogs/NewSchedule.vue";
const props = defineProps<{
card: LayoutCard;
}>();
const { getMetaOrRouteValue } = useLayoutCardTools(props.card);
const instanceId = getMetaOrRouteValue("instanceId");
const daemonId = getMetaOrRouteValue("daemonId");
const { toPage } = useAppRouters();
const newScheduleDialog = ref<InstanceType<typeof NewSchedule>>();
const { state, execute, isLoading } = scheduleList();
const getScheduleList = async () => {
try {
await execute({
params: {
remote_uuid: daemonId ?? "",
uuid: instanceId ?? ""
}
});
} catch (err: any) {
console.error(err);
message.error(err.message);
}
};
const deleteSchedule = async (name: string) => {
const { execute, state } = scheduleDelete();
try {
await execute({
params: {
remote_uuid: daemonId ?? "",
uuid: instanceId ?? "",
task_name: name
}
});
if (state.value) {
message.success(t("删除成功"));
await getScheduleList();
}
} catch (err: any) {
console.error(err);
message.error(err.message);
}
};
const rendTime = (text: string, schedule: Schedule) => {
switch (schedule.type.toString()) {
case ScheduleCreateType.INTERVAL: {
const time = Number(text);
let s = time;
let m = 0;
let h = 0;
while (s >= 60) {
s -= 60;
m += 1;
}
while (m >= 60) {
m -= 60;
h += 1;
}
return `${t("每隔")} ${h} ${t("小时")} ${m} ${t("分钟")} ${s} ${t("秒")}`;
}
case ScheduleCreateType.CYCLE: {
const time = text;
const timeArr = time.split(" ");
const h = timeArr[2];
const m = timeArr[1];
const s = timeArr[0];
const w = timeArr[5];
return `${t("每星期")} ${w} ${t("的")} ${h}:${m}:${s}`;
}
case ScheduleCreateType.SPECIFY: {
const time = text;
const timeArr = time.split(" ");
const h = timeArr[2];
const m = timeArr[1];
const s = timeArr[0];
const dd = timeArr[3];
const mm = timeArr[4];
return `${mm} ${t("月")} ${dd} ${t("日")} ${h}:${m}:${s}`;
}
default:
return "Unknown Time";
}
};
const columns = [
{
align: "center",
title: t("任务名"),
dataIndex: "name",
key: "name"
},
{
align: "center",
title: t("有效载荷"),
dataIndex: "payload",
key: "payload"
},
{
align: "center",
title: t("剩余次数"),
dataIndex: "count",
key: "count",
minWidth: "80px",
customRender: (e: { text: number }) => (e.text > 0 ? e.text : t("无限"))
},
{
align: "center",
title: t("动作"),
dataIndex: "action",
key: "action",
minWidth: "180px",
customRender: (e: { text: "command" | "stop" | "start" | "restart" | "kill" }) =>
ScheduleAction[e.text]
},
{
align: "center",
title: t("类型"),
dataIndex: "type",
key: "type",
minWidth: "180px",
customRender: (e: { text: 1 | 2 | 3 }) => ScheduleType[e.text]
},
{
align: "center",
title: t("触发时间"),
dataIndex: "time",
key: "time",
minWidth: "240px",
customRender: (e: { text: string; record: Schedule }) => rendTime(e.text, e.record)
},
{
align: "center",
title: t("TXT_CODE_fe731dfc"),
key: "actions",
minWidth: "180px"
}
];
const refresh = async () => {
await getScheduleList();
message.success(t("已刷新"));
};
const toConsole = () => {
toPage({
path: "/instances/terminal",
query: {
daemonId,
instanceId
}
});
};
onMounted(async () => {
getScheduleList();
});
</script>
<template>
<div style="height: 100%" class="container">
<a-row :gutter="[24, 24]" style="height: 100%">
<a-col :span="24">
<BetweenMenus>
<template #left>
<a-typography-title class="mb-0" :level="4">
<FieldTimeOutlined />
{{ card.title }}
</a-typography-title>
</template>
<template #right>
<a-button class="mr-10" @click="toConsole()">
{{ t("返回") }}
</a-button>
<a-button class="mr-10" @click="refresh">
{{ t("刷新") }}
</a-button>
<a-button type="primary" @click="newScheduleDialog?.openDialog()">
{{ t("新增") }}
</a-button>
</template>
</BetweenMenus>
</a-col>
<a-col :span="24">
<CardPanel style="height: 100%">
<template #body>
<a-spin :spinning="isLoading">
<a-table
:data-source="state"
:columns="columns"
:scroll="{ x: 'max-content' }"
:pagination="{
pageSize: 15
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'actions'">
<a-popconfirm
:title="t('你确定要删除这条计划任务吗?')"
@confirm="deleteSchedule(record.name)"
>
<a-button size="">
{{ t("删除") }}
<DeleteOutlined />
</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</a-spin>
</template>
</CardPanel>
</a-col>
</a-row>
</div>
<NewSchedule
ref="newScheduleDialog"
:daemon-id="daemonId ?? ''"
:instance-id="instanceId ?? ''"
@get-schedule-list="getScheduleList()"
/>
</template>
<style lang="scss" scoped>
.search-input {
transition: all 0.4s;
text-align: center;
width: 50%;
&:hover {
width: 100%;
}
@media (max-width: 992px) {
width: 100% !important;
}
}
</style>

View File

@ -18,7 +18,13 @@ import { onMounted, computed, ref } from "vue";
import { useLayoutCardTools } from "@/hooks/useCardTools";
import { getRandomId } from "../../tools/randId";
import IconBtn from "@/components/IconBtn.vue";
import { openInstance, stopInstance } from "@/services/apis/instance";
import {
openInstance,
stopInstance,
restartInstance,
killInstance,
updateInstance
} from "@/services/apis/instance";
import { CloseOutlined } from "@ant-design/icons-vue";
import { GLOBAL_INSTANCE_NAME } from "../../config/const";
import { INSTANCE_STATUS_TEXT } from "../../hooks/useInstance";
@ -70,7 +76,8 @@ const quickOperations = computed(() =>
}
});
},
props: {}
props: {},
condition: () => isStopped.value
},
{
title: t("TXT_CODE_b1dedda3"),
@ -85,34 +92,59 @@ const quickOperations = computed(() =>
},
props: {
danger: true
}
},
condition: () => isRunning.value
}
])
);
const instanceOperations = arrayFilter([
{
title: t("TXT_CODE_47dcfa5"),
icon: ReconciliationOutlined,
click: () => {
console.log(3);
const instanceOperations = computed(() =>
arrayFilter([
{
title: t("TXT_CODE_47dcfa5"),
icon: ReconciliationOutlined,
click: () => {
restartInstance().execute({
params: {
uuid: instanceId || "",
remote_uuid: daemonId || ""
}
});
},
condition: () => isRunning.value
},
{
title: t("TXT_CODE_7b67813a"),
icon: CloseOutlined,
click: () => {
killInstance().execute({
params: {
uuid: instanceId || "",
remote_uuid: daemonId || ""
}
});
},
condition: () => isRunning.value
},
{
title: t("TXT_CODE_40ca4f2"),
icon: CloudDownloadOutlined,
click: () => {
updateInstance().execute({
params: {
uuid: instanceId || "",
remote_uuid: daemonId || "",
task_name: "update"
},
data: {
time: new Date().getTime()
}
});
},
condition: () => isStopped.value
}
},
{
title: t("TXT_CODE_7b67813a"),
icon: CloseOutlined,
click: () => {
console.log(3);
}
},
{
title: t("TXT_CODE_40ca4f2"),
icon: CloudDownloadOutlined,
click: () => {
console.log(4);
}
}
]);
])
);
const handleSendCommand = () => {
sendCommand(commandInputValue.value);
@ -240,7 +272,7 @@ const innerTerminalType = viewType === "inner";
</template>
</BetweenMenus>
</div>
<a-spin :spinning="!isConnect" tip="正在连接终端中...">
<a-spin :spinning="!isConnect" :tip="t('正在连接终端中...')">
<div v-if="!containerState.isDesignMode" class="console-wrapper">
<div class="terminal-wrapper global-card-container-shadow">
<div class="terminal-container">

View File

@ -0,0 +1,246 @@
<script setup lang="ts">
import { ref, reactive } from "vue";
import { t } from "@/lang/i18n";
import { message, notification } from "ant-design-vue";
import { ScheduleAction, ScheduleType, ScheduleCreateType } from "@/types/const";
import type { NewScheduleTask } from "@/types";
import { scheduleCreate } from "@/services/apis/instance";
import { useScreen } from "@/hooks/useScreen";
import type { Dayjs } from "dayjs";
import _ from "lodash";
const props = defineProps<{
daemonId: string;
instanceId: string;
}>();
const emit = defineEmits(["getScheduleList"]);
const open = ref(false);
const openDialog = () => {
open.value = true;
};
const { isPhone } = useScreen();
const weeks = [
{ label: t("周一"), value: 1 },
{ label: t("周二"), value: 2 },
{ label: t("周三"), value: 3 },
{ label: t("周四"), value: 4 },
{ label: t("周五"), value: 5 },
{ label: t("周六"), value: 6 },
{ label: t("周日"), value: 7 }
];
interface NewTask extends NewScheduleTask {
payload: string;
weekend: string[];
cycle: string[];
objTime: string;
}
const newTaskOrigin: NewTask = {
name: "",
action: "command",
count: "",
type: ScheduleCreateType.INTERVAL,
time: "",
payload: "",
weekend: [],
cycle: ["0", "0", "0"],
objTime: ""
};
let newTask = reactive<NewTask>(_.cloneDeep(newTaskOrigin));
const createTaskTypeInterval = async () => {
const arr = newTask.cycle;
let ps = Number(arr[0]);
let pm = Number(arr[1]);
let ph = Number(arr[2]);
const rs = ps + pm * 60 + ph * 60 * 60;
newTask.time = rs.toString();
await createRequest();
};
const createTaskTypeCycle = async () => {
const weekend = newTask.weekend;
if (newTask.objTime === "") throw new Error(t("请选择时间"));
if (weekend.length === 0) throw new Error(t("请选择星期"));
const time = new Date(newTask.objTime);
const h = time.getHours();
const m = time.getMinutes();
const s = time.getSeconds();
newTask.time = `${s} ${m} ${h} * * ${weekend.join(",")}`;
await createRequest();
};
const createTaskTypeSpecify = async () => {
if (newTask.objTime === "") throw new Error(t("请选择时间"));
const time = newTask.objTime as unknown as Dayjs;
const mm = time.month() + 1;
const dd = time.date();
const h = time.hour();
const m = time.minute();
const s = time.second();
newTask.time = `${s} ${m} ${h} ${dd} ${mm} *`;
await createRequest();
};
const { state, isLoading, execute } = scheduleCreate();
const createRequest = async () => {
try {
if (!newTask.count) newTask.count = "-1";
await execute({
params: {
remote_uuid: props.daemonId,
uuid: props.instanceId
},
data: newTask
});
if (state.value) {
emit("getScheduleList");
notification.success({
message: t("创建成功")
});
newTask = reactive(_.cloneDeep(newTaskOrigin));
open.value = false;
}
} catch (err: any) {
console.error(err);
message.error(err.message);
}
};
const submit = async () => {
try {
if (newTask.type === ScheduleCreateType.INTERVAL) await createTaskTypeInterval();
if (newTask.type === ScheduleCreateType.CYCLE) await createTaskTypeCycle();
if (newTask.type === ScheduleCreateType.SPECIFY) await createTaskTypeSpecify();
} catch (err: any) {
return message.error(err.message);
}
};
defineExpose({
openDialog
});
</script>
<template>
<a-modal
v-model:open="open"
centered
:mask-closable="false"
:title="t('新增计划任务')"
:confirm-loading="isLoading"
:destroy-on-close="true"
:ok-text="t('保存')"
@ok="submit"
>
<a-form-item>
<a-typography-title :level="5">{{ t("计划任务名字") }}</a-typography-title>
<a-typography-paragraph>
<a-typography-text type="secondary">
{{ t("必填,且必须唯一") }}
</a-typography-text>
</a-typography-paragraph>
<a-input v-model:value="newTask.name" />
</a-form-item>
<a-form-item>
<a-typography-title :level="5">{{ t("任务动作 / 类型") }}</a-typography-title>
<a-row :gutter="20">
<a-col :xs="24" :md="12" :offset="0" :class="{ 'mb-10': isPhone }">
<a-select
v-model:value="newTask.action"
:placeholder="t('请选择')"
:dropdown-match-select-width="false"
>
<a-select-option v-for="(action, i) in ScheduleAction" :key="i" :value="i">
{{ action }}
</a-select-option>
</a-select>
</a-col>
<a-col :xs="24" :md="12" :offset="0">
<a-select
v-model:value="newTask.type"
:placeholder="t('请选择')"
:dropdown-match-select-width="false"
>
<a-select-option v-for="(type, i) in ScheduleType" :key="i" :value="i">
{{ type }}
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-form-item>
<a-form-item v-if="newTask.type === ScheduleCreateType.INTERVAL">
<a-typography-paragraph>
<a-typography-text>
{{ t("每隔一定时间将执行一次,具体间隔可以仔细设置") }}
</a-typography-text>
</a-typography-paragraph>
<a-row :gutter="20">
<a-col :xs="24" :md="6" :offset="0" :class="{ 'mb-10': isPhone }">
<a-input
v-model:value="newTask.cycle[2]"
:placeholder="t('不可为空,请写 0 代表每隔 0 时')"
:addon-after="t('时')"
/>
</a-col>
<a-col :xs="24" :md="6" :offset="0" :class="{ 'mb-10': isPhone }">
<a-input
v-model:value="newTask.cycle[1]"
:placeholder="t('不可为空,请写 0 代表每隔 0 时')"
:addon-after="t('分')"
/>
</a-col>
<a-col :xs="24" :md="6" :offset="0" :class="{ 'mb-10': isPhone }">
<a-input
v-model:value="newTask.cycle[0]"
:placeholder="t('不可为空,请写 0 代表每隔 0 时')"
:addon-after="t('秒')"
/>
</a-col>
<a-col :xs="24" :md="6" :offset="0">
<a-input v-model:value="newTask.count" :placeholder="t('执行次数,留空无限')" />
</a-col>
</a-row>
</a-form-item>
<div v-if="newTask.type === ScheduleCreateType.CYCLE">
<a-form-item>
<a-typography-title :level="5">{{ t("触发时间") }}</a-typography-title>
<a-time-picker
v-model:value="newTask.objTime"
size="large"
:placeholder="$t('具体时间点')"
class="w-100"
/>
</a-form-item>
<a-form-item>
<a-checkbox-group v-model:value="newTask.weekend" :options="weeks" />
</a-form-item>
<a-form-item>
<a-typography-title :level="5">{{ t("执行次数") }}</a-typography-title>
<a-input v-model:value="newTask.count" :placeholder="t('留空无限')" />
</a-form-item>
</div>
<a-form-item v-if="newTask.type === ScheduleCreateType.SPECIFY">
<a-typography-title :level="5">{{ t("请选择日期和时间") }}</a-typography-title>
<a-date-picker v-model:value="newTask.objTime" show-time size="large" class="w-100" />
</a-form-item>
<a-form-item>
<a-typography-title :level="5">{{ t("任务有效负载") }}</a-typography-title>
<a-typography-paragraph>
<a-typography-text type="secondary">
{{ t("比如命令,文件名或其他参数等") }}
</a-typography-text>
</a-typography-paragraph>
<a-input v-model:value="newTask.payload" />
</a-form-item>
</a-modal>
</template>

View File

@ -256,6 +256,28 @@ function getDefaultFrontendLayoutConfig(): IPageLayoutConfig[] {
}
]
},
{
page: "/instances/schedule",
items: [
{
id: getRandomId(),
meta: {},
type: "Schedule",
title: t("计划任务"),
width: 12,
height: LayoutCardHeight.AUTO,
disableDelete: true
},
{
id: getRandomId(),
meta: {},
type: "EmptyCard",
title: "",
width: 12,
height: LayoutCardHeight.MINI
}
]
},
{
page: "/users",
items: [