Merge pull request #1042 from MCSManager/abandon

Feat: select instances
This commit is contained in:
unitwk 2023-10-18 11:19:34 +08:00 committed by GitHub
commit 17b635f9c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 377 additions and 53 deletions

View File

@ -71,5 +71,6 @@ declare module 'vue' {
PlaceHolderCard: typeof import('./src/components/PlaceHolderCard.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SelectInstances: typeof import('./src/components/SelectInstances.vue')['default']
}
}

View File

@ -0,0 +1,252 @@
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import type { UserInstance, MountComponent } from "@/types";
import { t } from "@/lang/i18n";
import {
SearchOutlined,
DownOutlined,
FormOutlined,
DatabaseOutlined,
FrownOutlined
} from "@ant-design/icons-vue";
import BetweenMenus from "@/components/BetweenMenus.vue";
import { remoteInstances } from "@/services/apis";
import { remoteNodeList } from "../services/apis";
import type { NodeStatus } from "../types/index";
import { message } from "ant-design-vue";
import { computeNodeName } from "../tools/nodes";
import { throttle } from "lodash";
const props = defineProps<MountComponent>();
const open = ref(false);
const cancel = async () => {
open.value = false;
if (props.destroyComponent) props.destroyComponent(1000);
};
const operationForm = ref({
instanceName: "",
currentPage: 1,
pageSize: 10
});
const currentRemoteNode = ref<NodeStatus>();
const { execute: getNodes, state: nodes } = remoteNodeList();
const { execute: getInstances, state: instances, isLoading } = remoteInstances();
const instancesList = computed(() => {
const newInstances: UserInstance[] = [];
for (const instance of instances.value?.data || []) {
newInstances.push({
instanceUuid: instance.instanceUuid,
serviceUuid: currentRemoteNode.value?.uuid ?? "",
nickname: instance.config.nickname,
status: instance.status,
hostIp: `${currentRemoteNode.value?.ip}:${currentRemoteNode.value?.port}`
});
}
return newInstances;
});
const initNodes = async () => {
await getNodes();
if (!nodes.value?.length) {
return message.error(t("TXT_CODE_e3d96a26"));
}
if (localStorage.getItem("pageSelectedRemote")) {
currentRemoteNode.value = JSON.parse(localStorage.pageSelectedRemote);
} else {
currentRemoteNode.value = nodes.value[0];
}
};
const initInstancesData = async () => {
if (!currentRemoteNode.value) {
await initNodes();
}
try {
await getInstances({
params: {
remote_uuid: currentRemoteNode.value?.uuid ?? "",
page: operationForm.value.currentPage,
page_size: operationForm.value.pageSize,
instance_name: operationForm.value.instanceName.trim()
}
});
} catch (err) {
return message.error(t("访问远程节点异常"));
}
};
const selectedItems = ref<UserInstance[]>([]);
const columns = [
{
align: "center",
title: t("实例名称"),
dataIndex: "nickname",
key: "instanceUuid"
},
{
align: "center",
title: t("操作"),
key: "operation"
}
];
const selectItem = (item: UserInstance) => {
selectedItems.value.push(item);
};
const findItem = (item: UserInstance) => {
return selectedItems.value.some((i) => JSON.stringify(i) === JSON.stringify(item));
};
const removeItem = (item: UserInstance) => {
selectedItems.value.splice(selectedItems.value.indexOf(item), 1);
};
const submit = async () => {
if (props.emitResult) props.emitResult(selectedItems.value);
await cancel();
};
onMounted(async () => {
await initInstancesData();
open.value = true;
});
const handleQueryInstance = throttle(async () => {
await initInstancesData();
}, 600);
const handleChangeNode = async (item: NodeStatus) => {
try {
currentRemoteNode.value = item;
await initInstancesData();
localStorage.setItem("pageSelectedRemote", JSON.stringify(item));
} catch (err: any) {
console.error(err.message);
}
};
</script>
<template>
<a-modal
v-model:open="open"
centered
:mask-closable="false"
:title="t('请选择实例')"
:ok-text="t('保存')"
:cancel-text="t('取消')"
@ok="submit"
@cancel="cancel"
>
<a-typography-paragraph>
<a-typography-text type="secondary">
{{ t("利用远程主机地址与模糊查询来为此用户增加应用实例") }}
</a-typography-text>
</a-typography-paragraph>
<a-row :gutter="[24, 24]" style="height: 100%">
<a-col :span="24">
<BetweenMenus>
<template #left>
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item
v-for="item in nodes"
:key="item.uuid"
@click="handleChangeNode(item)"
>
<DatabaseOutlined v-if="item.available" />
<FrownOutlined v-else />
{{ computeNodeName(item.ip, item.available, item.remarks) }}
</a-menu-item>
<a-menu-divider />
<a-menu-item key="toNodesPage">
<FormOutlined />
{{ t("TXT_CODE_28e53fed") }}
</a-menu-item>
</a-menu>
</template>
<a-button class="mr-12">
{{
computeNodeName(
currentRemoteNode?.ip || "",
currentRemoteNode?.available || true,
currentRemoteNode?.remarks
)
}}
<DownOutlined />
</a-button>
</a-dropdown>
</template>
<template #right>
<div class="search-input">
<a-input
v-model:value="operationForm.instanceName"
:placeholder="t('TXT_CODE_ce132192')"
@press-enter="handleQueryInstance"
@change="handleQueryInstance"
>
<template #prefix>
<search-outlined />
</template>
</a-input>
</div>
</template>
</BetweenMenus>
</a-col>
<a-col :span="24">
<div v-if="instances" class="flex-between align-center">
<a-typography-text>
{{ t("已选择") }} {{ selectedItems.length }} {{ t("项") }}
</a-typography-text>
<a-pagination
v-model:current="operationForm.currentPage"
v-model:pageSize="operationForm.pageSize"
:total="instances.maxPage * operationForm.pageSize"
show-size-changer
@change="initInstancesData"
/>
</div>
</a-col>
<template v-if="instancesList">
<a-col :span="24">
<a-table
:loading="isLoading"
:data-source="instancesList"
:columns="columns"
:pagination="false"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'operation'">
<a-button v-if="findItem(record)" danger size="" @click="removeItem(record)">
{{ t("移除") }}
</a-button>
<a-button v-else size="" @click="selectItem(record)">
{{ t("选择") }}
</a-button>
</template>
</template>
</a-table>
</a-col>
</template>
</a-row>
</a-modal>
</template>
<style lang="scss" scoped>
.instance-card {
cursor: pointer;
}
.instance-card:hover {
border: 1px solid var(--color-gray-8);
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16);
}
</style>

View File

@ -0,0 +1,32 @@
import { createApp, type Component } from "vue";
import { sleep } from "@/tools/commom";
export function useMountComponent() {
let isOpen = false;
const mount = <T>(component: Component) => {
if (isOpen) return;
isOpen = true;
return new Promise<T>((resolve, reject) => {
const div = document.createElement("div");
document.body.appendChild(div);
const app = createApp(component, {
async destroyComponent(delay = 0) {
await sleep(delay);
app.unmount();
div.remove();
isOpen = false;
},
emitResult(data: T) {
isOpen = false;
resolve(data);
}
});
app.mount(div);
});
};
return {
mount
};
}

View File

@ -0,0 +1,8 @@
import { useMountComponent } from "./useMountComponent";
import SelectInstances from "@/components/SelectInstances.vue";
import type { UserInstance } from "@/types";
export function useSelectInstances() {
const { mount } = useMountComponent();
return mount<UserInstance>(SelectInstances);
}

View File

@ -1,3 +1,4 @@
import { t } from "@/lang/i18n";
export const CARD_FIXED_HEIGHT = 200;
export const TERMINAL_CODE = [
@ -11,3 +12,11 @@ export const TERMINAL_CODE = [
"KS_C_5601",
"UTF-16"
];
export const INSTANCE_STATUS = {
"-1": t("状态未知"),
"0": t("已停止"),
"1": t("正在停止"),
"2": t("正在启动"),
"3": t("正在运行")
};

View File

@ -99,17 +99,11 @@ export interface UserInfo {
}
export interface UserInstance {
endTime: string;
hostIp: string;
ie: string;
instanceUuid: string;
lastDatetime: string;
nickname: string;
oe: string;
remarks: string;
serviceUuid: string;
status: number;
stopCommand: string;
}
export interface ImageInfo {
@ -185,3 +179,8 @@ export interface QuickStartTemplate {
author: string;
targetLink: string;
}
export interface MountComponent {
destroyComponent: Function;
emitResult: Function;
}

View File

@ -4,6 +4,7 @@ import { onMounted } from "vue";
import type { LayoutCard } from "@/types";
import { userInfoApi } from "@/services/apis/index";
import { useRouter } from "vue-router";
import { INSTANCE_STATUS } from "@/types/const";
defineProps<{
card: LayoutCard;
@ -13,14 +14,6 @@ const router = useRouter();
const { execute, state } = userInfoApi();
const status = {
"-1": t("状态未知"),
"0": t("已停止"),
"1": t("正在停止"),
"2": t("正在启动"),
"3": t("正在运行")
};
const columns = [
{
title: t("实例名称"),
@ -32,7 +25,7 @@ const columns = [
dataIndex: "status",
key: "status",
customRender: (e: { text: "-1" | "1" | "2" | "3" }) => {
return status[e.text] || e.text;
return INSTANCE_STATUS[e.text] || e.text;
}
},
{

View File

@ -346,7 +346,7 @@ onMounted(async () => {
</a-menu-item> -->
</a-menu>
</template>
<a-button>
<a-button size="">
{{ t("TXT_CODE_fe731dfc") }}
<DownOutlined />
</a-button>

View File

@ -10,6 +10,9 @@ import { arrayFilter } from "@/tools/array";
import { userInfoApiAdvanced } from "@/services/apis";
import { useLayoutCardTools } from "@/hooks/useCardTools";
import { updateUserInstance } from "@/services/apis";
import { useSelectInstances } from "@/hooks/useSelectInstances";
import { message } from "ant-design-vue";
import { INSTANCE_STATUS } from "@/types/const";
const props = defineProps<{
card: LayoutCard;
@ -30,19 +33,37 @@ const handleDelete = async (deletedInstance: UserInstance) => {
deletedInstance.instanceUuid == instance.instanceUuid
) {
dataSource.value.splice(valueKey, 1);
await updateUserInstance().execute({
data: {
config: {
instances: dataSource.value
},
uuid: <string>userUuid
}
});
break;
}
}
};
const assignApp = async () => {
try {
const selectedInstances = await useSelectInstances();
if (selectedInstances) dataSource.value = dataSource.value.concat(selectedInstances);
} catch (err: any) {
console.error(err);
}
};
const saveData = async () => {
try {
await updateUserInstance().execute({
data: {
config: {
instances: dataSource.value
},
uuid: <string>userUuid
}
});
return message.success(t("更新成功"));
} catch (err: any) {
console.error(err);
return message.error(err.message);
}
};
async function refreshChart() {
if (userUuid == null) {
return;
@ -97,6 +118,9 @@ const columns = computed(() => {
dataIndex: "status",
key: "status",
minWidth: "200px",
customRender: (e: { text: "-1" | "1" | "2" | "3" }) => {
return INSTANCE_STATUS[e.text] || e.text;
},
condition: () => !screen.isPhone.value
},
{
@ -112,37 +136,43 @@ const columns = computed(() => {
<template>
<div style="height: 100%" class="container">
<a-row :gutter="[24, 24]" style="height: 100%">
<div v-if="userUuid" class="h-100 w-100">
<a-col :span="24">
<BetweenMenus>
<template #left>
<a-typography-title class="mb-0" :level="4">
{{ t("TXT_CODE_e1c9a6ac") }}
</a-typography-title>
</template>
<template #right>
<a-button type="primary">{{ t("TXT_CODE_a60466a1") }}</a-button>
</template>
</BetweenMenus>
</a-col>
<a-row v-if="userUuid" :gutter="[24, 24]" style="height: 100%">
<a-col :span="24">
<BetweenMenus>
<template #left>
<a-typography-title class="mb-0" :level="4">
{{ t("TXT_CODE_e1c9a6ac") }}
</a-typography-title>
</template>
<template #right>
<a-button v-show="!screen.isPhone.value" class="mr-8" @click="refreshChart()">
{{ t("刷新") }}
</a-button>
<a-button class="mr-8" type="primary" ghost @click="saveData()">
{{ t("保存数据") }}
</a-button>
<a-button type="primary" @click="assignApp">
{{ t("TXT_CODE_a60466a1") }}
</a-button>
</template>
</BetweenMenus>
</a-col>
<a-col :span="24">
<CardPanel class="h-100">
<template #body>
<a-table :data-source="dataSource" :columns="columns">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'operation'">
<a-button danger @click="handleDelete(record)">
{{ t("TXT_CODE_ecbd7449") }}
</a-button>
</template>
<a-col :span="24">
<CardPanel class="h-100">
<template #body>
<a-table :data-source="dataSource" :columns="columns">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'operation'">
<a-button danger size="" @click="handleDelete(record)">
{{ t("TXT_CODE_ecbd7449") }}
</a-button>
</template>
</a-table>
</template>
</CardPanel>
</a-col>
</div>
</template>
</a-table>
</template>
</CardPanel>
</a-col>
</a-row>
</div>
</template>