mirror of https://github.com/halo-dev/halo
Refactor plugin installation modal to support extend (#4461)
#### What type of PR is this? /area console /kind feature /milestone 2.9.x #### What this PR does / why we need it: 重构插件安装的界面,以支持扩展。 文档:https://github.com/halo-dev/halo/pull/4461/files?short_path=fe4adc6#diff-fe4adc66005d24150478d0919c5f330ee695c2b5657ee7baed56929830c7eb90 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/4432 #### Special notes for your reviewer: 需要测试插件的本地上传和远程下载的安装的更新。 #### Does this PR introduce a user-facing change? ```release-note 重构 Console 端插件安装界面,支持通过插件扩展选项卡。 ```pull/4482/head
parent
7603b21dd2
commit
63bbd4fa81
|
@ -0,0 +1,54 @@
|
||||||
|
# 插件安装界面选项卡扩展点
|
||||||
|
|
||||||
|
## 原由
|
||||||
|
|
||||||
|
目前 Halo 原生支持本地上传和远程下载的方式安装插件,此扩展点用于扩展插件安装界面的选项卡,以支持更多的安装方式。
|
||||||
|
|
||||||
|
## 定义方式
|
||||||
|
|
||||||
|
> 此示例为添加一个安装选项卡用于从 GitHub 上下载插件。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { definePlugin } from "@halo-dev/console-shared";
|
||||||
|
import { markRaw } from "vue";
|
||||||
|
import GitHubDownloadTab from "./components/GitHubDownloadTab.vue";
|
||||||
|
|
||||||
|
export default definePlugin({
|
||||||
|
extensionPoints: {
|
||||||
|
"plugin:installation:tabs:create": () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "github",
|
||||||
|
label: "GitHub",
|
||||||
|
component: markRaw(GitHubDownload),
|
||||||
|
props: {
|
||||||
|
foo: "bar",
|
||||||
|
},
|
||||||
|
priority: 30,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
扩展点类型:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
"plugin:installation:tabs:create"?: () =>
|
||||||
|
| PluginInstallationTab[]
|
||||||
|
| Promise<PluginInstallationTab[]>;
|
||||||
|
```
|
||||||
|
|
||||||
|
`PluginInstallationTab`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface PluginInstallationTab {
|
||||||
|
id: string; // 选项卡的唯一标识
|
||||||
|
label: string; // 选项卡的名称
|
||||||
|
component: Raw<Component>; // 选项卡面板的组件
|
||||||
|
props?: Record<string, unknown>; // 选项卡组件的 props
|
||||||
|
permissions?: string[]; // 权限
|
||||||
|
priority: number; // 优先级
|
||||||
|
}
|
||||||
|
```
|
|
@ -7,4 +7,5 @@ export * from "./states/editor";
|
||||||
export * from "./states/plugin-tab";
|
export * from "./states/plugin-tab";
|
||||||
export * from "./states/comment-subject-ref";
|
export * from "./states/comment-subject-ref";
|
||||||
export * from "./states/backup";
|
export * from "./states/backup";
|
||||||
|
export * from "./states/plugin-installation-tabs";
|
||||||
export * from "./states/entity";
|
export * from "./states/entity";
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { Component, Raw } from "vue";
|
||||||
|
|
||||||
|
export interface PluginInstallationTab {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
component: Raw<Component>;
|
||||||
|
props?: Record<string, unknown>;
|
||||||
|
permissions?: string[];
|
||||||
|
priority: number;
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import type { EditorProvider, PluginTab } from "..";
|
||||||
import type { AnyExtension } from "@tiptap/vue-3";
|
import type { AnyExtension } from "@tiptap/vue-3";
|
||||||
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
|
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
|
||||||
import type { BackupTab } from "@/states/backup";
|
import type { BackupTab } from "@/states/backup";
|
||||||
|
import type { PluginInstallationTab } from "@/states/plugin-installation-tabs";
|
||||||
import type { EntityDropdownItem } from "@/states/entity";
|
import type { EntityDropdownItem } from "@/states/entity";
|
||||||
import type { ListedPost, Plugin } from "@halo-dev/api-client";
|
import type { ListedPost, Plugin } from "@halo-dev/api-client";
|
||||||
|
|
||||||
|
@ -34,6 +35,10 @@ export interface ExtensionPoint {
|
||||||
|
|
||||||
"backup:tabs:create"?: () => BackupTab[] | Promise<BackupTab[]>;
|
"backup:tabs:create"?: () => BackupTab[] | Promise<BackupTab[]>;
|
||||||
|
|
||||||
|
"plugin:installation:tabs:create"?: () =>
|
||||||
|
| PluginInstallationTab[]
|
||||||
|
| Promise<PluginInstallationTab[]>;
|
||||||
|
|
||||||
"post:list-item:operation:create"?: () =>
|
"post:list-item:operation:create"?: () =>
|
||||||
| EntityDropdownItem<ListedPost>[]
|
| EntityDropdownItem<ListedPost>[]
|
||||||
| Promise<EntityDropdownItem<ListedPost>[]>;
|
| Promise<EntityDropdownItem<ListedPost>[]>;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
Dialog,
|
Dialog,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import PluginListItem from "./components/PluginListItem.vue";
|
import PluginListItem from "./components/PluginListItem.vue";
|
||||||
import PluginUploadModal from "./components/PluginUploadModal.vue";
|
import PluginInstallationModal from "./components/PluginInstallationModal.vue";
|
||||||
import { computed, ref, onMounted } from "vue";
|
import { computed, ref, onMounted } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
|
@ -27,12 +27,12 @@ const { t } = useI18n();
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
const pluginUploadModal = ref(false);
|
const pluginInstallationModal = ref(false);
|
||||||
const pluginToUpgrade = ref<Plugin>();
|
const pluginToUpgrade = ref<Plugin>();
|
||||||
|
|
||||||
function handleOpenUploadModal(plugin?: Plugin) {
|
function handleOpenUploadModal(plugin?: Plugin) {
|
||||||
pluginToUpgrade.value = plugin;
|
pluginToUpgrade.value = plugin;
|
||||||
pluginUploadModal.value = true;
|
pluginInstallationModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
|
@ -115,10 +115,10 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<PluginUploadModal
|
<PluginInstallationModal
|
||||||
v-if="currentUserHasPermission(['system:plugins:manage'])"
|
v-if="currentUserHasPermission(['system:plugins:manage'])"
|
||||||
v-model:visible="pluginUploadModal"
|
v-model:visible="pluginInstallationModal"
|
||||||
:upgrade-plugin="pluginToUpgrade"
|
:plugin-to-upgrade="pluginToUpgrade"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VPageHeader :title="$t('core.plugin.title')">
|
<VPageHeader :title="$t('core.plugin.title')">
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { VModal, VButton, VTabbar } from "@halo-dev/components";
|
||||||
|
import type { Plugin } from "@halo-dev/api-client";
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
import { provide } from "vue";
|
||||||
|
import { toRefs } from "vue";
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
import LocalUpload from "./installation-tabs/LocalUpload.vue";
|
||||||
|
import RemoteDownload from "./installation-tabs/RemoteDownload.vue";
|
||||||
|
import { markRaw } from "vue";
|
||||||
|
import type {
|
||||||
|
PluginInstallationTab,
|
||||||
|
PluginModule,
|
||||||
|
} from "@halo-dev/console-shared";
|
||||||
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
pluginToUpgrade?: Plugin;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
visible: false,
|
||||||
|
pluginToUpgrade: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:visible", visible: boolean): void;
|
||||||
|
(event: "close"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { pluginToUpgrade } = toRefs(props);
|
||||||
|
provide<Ref<Plugin | undefined>>("pluginToUpgrade", pluginToUpgrade);
|
||||||
|
|
||||||
|
const tabs = ref<PluginInstallationTab[]>([
|
||||||
|
{
|
||||||
|
id: "local",
|
||||||
|
label: t("core.plugin.upload_modal.tabs.local"),
|
||||||
|
component: markRaw(LocalUpload),
|
||||||
|
priority: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "remote",
|
||||||
|
label: t("core.plugin.upload_modal.tabs.remote.title"),
|
||||||
|
component: markRaw(RemoteDownload),
|
||||||
|
priority: 20,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const activeTabId = ref();
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
return props.pluginToUpgrade
|
||||||
|
? t("core.plugin.upload_modal.titles.upgrade", {
|
||||||
|
display_name: props.pluginToUpgrade.spec.displayName,
|
||||||
|
})
|
||||||
|
: t("core.plugin.upload_modal.titles.install");
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleVisibleChange = (visible: boolean) => {
|
||||||
|
emit("update:visible", visible);
|
||||||
|
if (!visible) {
|
||||||
|
emit("close");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle remote download url from route
|
||||||
|
const routeRemoteDownloadUrl = useRouteQuery<string | null>(
|
||||||
|
"remote-download-url"
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible && routeRemoteDownloadUrl.value) {
|
||||||
|
activeTabId.value = "remote";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { pluginModules } = usePluginModuleStore();
|
||||||
|
onMounted(() => {
|
||||||
|
pluginModules.forEach((pluginModule: PluginModule) => {
|
||||||
|
const { extensionPoints } = pluginModule;
|
||||||
|
if (!extensionPoints?.["plugin:installation:tabs:create"]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = extensionPoints[
|
||||||
|
"plugin:installation:tabs:create"
|
||||||
|
]() as PluginInstallationTab[];
|
||||||
|
|
||||||
|
tabs.value.push(...items);
|
||||||
|
});
|
||||||
|
|
||||||
|
tabs.value.sort((a, b) => {
|
||||||
|
return a.priority - b.priority;
|
||||||
|
});
|
||||||
|
|
||||||
|
activeTabId.value = tabs.value[0].id;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VModal
|
||||||
|
:visible="visible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:centered="true"
|
||||||
|
:width="920"
|
||||||
|
height="calc(100vh - 20px)"
|
||||||
|
@update:visible="handleVisibleChange"
|
||||||
|
>
|
||||||
|
<VTabbar
|
||||||
|
v-model:active-id="activeTabId"
|
||||||
|
:items="
|
||||||
|
tabs.map((tab) => {
|
||||||
|
return { label: tab.label, id: tab.id };
|
||||||
|
})
|
||||||
|
"
|
||||||
|
type="outline"
|
||||||
|
/>
|
||||||
|
<div class="mt-2">
|
||||||
|
<template v-for="tab in tabs" :key="tab.id">
|
||||||
|
<component
|
||||||
|
:is="tab.component"
|
||||||
|
v-bind="tab.props"
|
||||||
|
v-if="tab.id === activeTabId"
|
||||||
|
@close-modal="handleVisibleChange(false)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<VButton @click="handleVisibleChange(false)">关闭</VButton>
|
||||||
|
</template>
|
||||||
|
</VModal>
|
||||||
|
</template>
|
|
@ -1,298 +0,0 @@
|
||||||
<script lang="ts" setup>
|
|
||||||
import {
|
|
||||||
VModal,
|
|
||||||
Dialog,
|
|
||||||
Toast,
|
|
||||||
VTabs,
|
|
||||||
VTabItem,
|
|
||||||
VButton,
|
|
||||||
} from "@halo-dev/components";
|
|
||||||
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
|
||||||
import { apiClient } from "@/utils/api-client";
|
|
||||||
import type { Plugin } from "@halo-dev/api-client";
|
|
||||||
import { computed, ref, watch, nextTick } from "vue";
|
|
||||||
import type { SuccessResponse, ErrorResponse, UppyFile } from "@uppy/core";
|
|
||||||
import { useI18n } from "vue-i18n";
|
|
||||||
import { useQueryClient } from "@tanstack/vue-query";
|
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
|
||||||
import { submitForm } from "@formkit/core";
|
|
||||||
import AppDownloadAlert from "@/components/common/AppDownloadAlert.vue";
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
visible: boolean;
|
|
||||||
upgradePlugin?: Plugin;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
visible: false,
|
|
||||||
upgradePlugin: undefined,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(event: "update:visible", visible: boolean): void;
|
|
||||||
(event: "close"): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const uploadVisible = ref(false);
|
|
||||||
|
|
||||||
const modalTitle = computed(() => {
|
|
||||||
return props.upgradePlugin
|
|
||||||
? t("core.plugin.upload_modal.titles.upgrade", {
|
|
||||||
display_name: props.upgradePlugin.spec.displayName,
|
|
||||||
})
|
|
||||||
: t("core.plugin.upload_modal.titles.install");
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleVisibleChange = (visible: boolean) => {
|
|
||||||
emit("update:visible", visible);
|
|
||||||
if (!visible) {
|
|
||||||
emit("close");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoint = computed(() => {
|
|
||||||
if (props.upgradePlugin) {
|
|
||||||
return `/apis/api.console.halo.run/v1alpha1/plugins/${props.upgradePlugin.metadata.name}/upgrade`;
|
|
||||||
}
|
|
||||||
return "/apis/api.console.halo.run/v1alpha1/plugins/install";
|
|
||||||
});
|
|
||||||
|
|
||||||
const onUploaded = async (response: SuccessResponse) => {
|
|
||||||
if (props.upgradePlugin) {
|
|
||||||
Toast.success(t("core.common.toast.upgrade_success"));
|
|
||||||
window.location.reload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVisibleChange(false);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["plugins"] });
|
|
||||||
|
|
||||||
handleShowActiveModalAfterInstall(response.body as Plugin);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleShowActiveModalAfterInstall = (plugin: Plugin) => {
|
|
||||||
Dialog.success({
|
|
||||||
title: t("core.plugin.upload_modal.operations.active_after_install.title"),
|
|
||||||
description: t(
|
|
||||||
"core.plugin.upload_modal.operations.active_after_install.description"
|
|
||||||
),
|
|
||||||
confirmText: t("core.common.buttons.confirm"),
|
|
||||||
cancelText: t("core.common.buttons.cancel"),
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
const { data: pluginToUpdate } =
|
|
||||||
await apiClient.extension.plugin.getpluginHaloRunV1alpha1Plugin({
|
|
||||||
name: plugin.metadata.name,
|
|
||||||
});
|
|
||||||
pluginToUpdate.spec.enabled = true;
|
|
||||||
|
|
||||||
await apiClient.extension.plugin.updatepluginHaloRunV1alpha1Plugin({
|
|
||||||
name: pluginToUpdate.metadata.name,
|
|
||||||
plugin: pluginToUpdate,
|
|
||||||
});
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
interface PluginInstallationErrorResponse {
|
|
||||||
detail: string;
|
|
||||||
instance: string;
|
|
||||||
pluginName: string;
|
|
||||||
requestId: string;
|
|
||||||
status: number;
|
|
||||||
timestamp: string;
|
|
||||||
title: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PLUGIN_ALREADY_EXISTS_TYPE =
|
|
||||||
"https://halo.run/probs/plugin-alreay-exists";
|
|
||||||
|
|
||||||
const onError = (file: UppyFile<unknown>, response: ErrorResponse) => {
|
|
||||||
const body = response.body as PluginInstallationErrorResponse;
|
|
||||||
|
|
||||||
if (body.type === PLUGIN_ALREADY_EXISTS_TYPE) {
|
|
||||||
handleCatchExistsException(body, file.data as File);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCatchExistsException = async (
|
|
||||||
error: PluginInstallationErrorResponse,
|
|
||||||
file?: File
|
|
||||||
) => {
|
|
||||||
Dialog.info({
|
|
||||||
title: t(
|
|
||||||
"core.plugin.upload_modal.operations.existed_during_installation.title"
|
|
||||||
),
|
|
||||||
description: t(
|
|
||||||
"core.plugin.upload_modal.operations.existed_during_installation.description"
|
|
||||||
),
|
|
||||||
confirmText: t("core.common.buttons.confirm"),
|
|
||||||
cancelText: t("core.common.buttons.cancel"),
|
|
||||||
onConfirm: async () => {
|
|
||||||
if (activeTabId.value === "local") {
|
|
||||||
await apiClient.plugin.upgradePlugin({
|
|
||||||
name: error.pluginName,
|
|
||||||
file: file,
|
|
||||||
});
|
|
||||||
} else if (activeTabId.value === "remote") {
|
|
||||||
await apiClient.plugin.upgradePluginFromUri({
|
|
||||||
name: error.pluginName,
|
|
||||||
upgradeFromUriRequest: {
|
|
||||||
uri: remoteDownloadUrl.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error("Unknown tab id");
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.success(t("core.common.toast.upgrade_success"));
|
|
||||||
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.visible,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
uploadVisible.value = true;
|
|
||||||
} else {
|
|
||||||
const uploadVisibleTimer = setTimeout(() => {
|
|
||||||
uploadVisible.value = false;
|
|
||||||
clearTimeout(uploadVisibleTimer);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// remote download
|
|
||||||
const activeTabId = ref("local");
|
|
||||||
const remoteDownloadUrl = ref("");
|
|
||||||
const downloading = ref(false);
|
|
||||||
|
|
||||||
const handleDownloadPlugin = async () => {
|
|
||||||
try {
|
|
||||||
downloading.value = true;
|
|
||||||
if (props.upgradePlugin) {
|
|
||||||
await apiClient.plugin.upgradePluginFromUri({
|
|
||||||
name: props.upgradePlugin.metadata.name,
|
|
||||||
upgradeFromUriRequest: {
|
|
||||||
uri: remoteDownloadUrl.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Toast.success(t("core.common.toast.upgrade_success"));
|
|
||||||
window.location.reload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: plugin } = await apiClient.plugin.installPluginFromUri({
|
|
||||||
installFromUriRequest: {
|
|
||||||
uri: remoteDownloadUrl.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
handleVisibleChange(false);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["plugins"] });
|
|
||||||
|
|
||||||
handleShowActiveModalAfterInstall(plugin);
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
} catch (error: any) {
|
|
||||||
const data = error?.response.data as PluginInstallationErrorResponse;
|
|
||||||
if (data?.type === PLUGIN_ALREADY_EXISTS_TYPE) {
|
|
||||||
handleCatchExistsException(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("Failed to download plugin", error);
|
|
||||||
} finally {
|
|
||||||
routeRemoteDownloadUrl.value = null;
|
|
||||||
downloading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// handle remote download url from route
|
|
||||||
const routeRemoteDownloadUrl = useRouteQuery<string | null>(
|
|
||||||
"remote-download-url"
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => props.visible,
|
|
||||||
(visible) => {
|
|
||||||
if (routeRemoteDownloadUrl.value && visible) {
|
|
||||||
activeTabId.value = "remote";
|
|
||||||
remoteDownloadUrl.value = routeRemoteDownloadUrl.value;
|
|
||||||
nextTick(() => {
|
|
||||||
submitForm("plugin-remote-download-form");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<VModal
|
|
||||||
:visible="visible"
|
|
||||||
:width="600"
|
|
||||||
:title="modalTitle"
|
|
||||||
:centered="false"
|
|
||||||
@update:visible="handleVisibleChange"
|
|
||||||
>
|
|
||||||
<VTabs v-model:active-id="activeTabId" type="outline" class="!rounded-none">
|
|
||||||
<VTabItem id="local" :label="$t('core.plugin.upload_modal.tabs.local')">
|
|
||||||
<div class="pb-3">
|
|
||||||
<AppDownloadAlert />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UppyUpload
|
|
||||||
v-if="uploadVisible"
|
|
||||||
:restrictions="{
|
|
||||||
maxNumberOfFiles: 1,
|
|
||||||
allowedFileTypes: ['.jar'],
|
|
||||||
}"
|
|
||||||
:endpoint="endpoint"
|
|
||||||
auto-proceed
|
|
||||||
@uploaded="onUploaded"
|
|
||||||
@error="onError"
|
|
||||||
/>
|
|
||||||
</VTabItem>
|
|
||||||
<VTabItem
|
|
||||||
id="remote"
|
|
||||||
:label="$t('core.plugin.upload_modal.tabs.remote.title')"
|
|
||||||
>
|
|
||||||
<FormKit
|
|
||||||
id="plugin-remote-download-form"
|
|
||||||
name="plugin-remote-download-form"
|
|
||||||
type="form"
|
|
||||||
:preserve="true"
|
|
||||||
@submit="handleDownloadPlugin"
|
|
||||||
>
|
|
||||||
<FormKit
|
|
||||||
v-model="remoteDownloadUrl"
|
|
||||||
:label="$t('core.plugin.upload_modal.tabs.remote.fields.url')"
|
|
||||||
type="text"
|
|
||||||
></FormKit>
|
|
||||||
</FormKit>
|
|
||||||
|
|
||||||
<div class="pt-5">
|
|
||||||
<VButton
|
|
||||||
:loading="downloading"
|
|
||||||
type="secondary"
|
|
||||||
@click="$formkit.submit('plugin-remote-download-form')"
|
|
||||||
>
|
|
||||||
{{ $t("core.common.buttons.download") }}
|
|
||||||
</VButton>
|
|
||||||
</div>
|
|
||||||
</VTabItem>
|
|
||||||
</VTabs>
|
|
||||||
</VModal>
|
|
||||||
</template>
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { type Ref, inject, ref } from "vue";
|
||||||
|
import type { Plugin } from "@halo-dev/api-client";
|
||||||
|
import { Dialog, Toast } from "@halo-dev/components";
|
||||||
|
import type { SuccessResponse } from "@uppy/core";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
|
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { UppyFile } from "@uppy/core";
|
||||||
|
import type { ErrorResponse } from "@uppy/core";
|
||||||
|
import type { PluginInstallationErrorResponse } from "../../types";
|
||||||
|
import { PLUGIN_ALREADY_EXISTS_TYPE } from "../../constants";
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
import AppDownloadAlert from "@/components/common/AppDownloadAlert.vue";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "close-modal"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const pluginToUpgrade = inject<Ref<Plugin | undefined>>(
|
||||||
|
"pluginToUpgrade",
|
||||||
|
ref()
|
||||||
|
);
|
||||||
|
|
||||||
|
const endpoint = computed(() => {
|
||||||
|
if (pluginToUpgrade.value) {
|
||||||
|
return `/apis/api.console.halo.run/v1alpha1/plugins/${pluginToUpgrade.value.metadata.name}/upgrade`;
|
||||||
|
}
|
||||||
|
return "/apis/api.console.halo.run/v1alpha1/plugins/install";
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUploaded = async (response: SuccessResponse) => {
|
||||||
|
if (pluginToUpgrade.value) {
|
||||||
|
Toast.success(t("core.common.toast.upgrade_success"));
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("close-modal");
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["plugins"] });
|
||||||
|
|
||||||
|
handleShowActiveModalAfterInstall(response.body as Plugin);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError = (file: UppyFile<unknown>, response: ErrorResponse) => {
|
||||||
|
const body = response.body as PluginInstallationErrorResponse;
|
||||||
|
|
||||||
|
if (body.type === PLUGIN_ALREADY_EXISTS_TYPE) {
|
||||||
|
handleCatchExistsException(body, file.data as File);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowActiveModalAfterInstall = (plugin: Plugin) => {
|
||||||
|
Dialog.success({
|
||||||
|
title: t("core.plugin.upload_modal.operations.active_after_install.title"),
|
||||||
|
description: t(
|
||||||
|
"core.plugin.upload_modal.operations.active_after_install.description"
|
||||||
|
),
|
||||||
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
const { data: pluginToUpdate } =
|
||||||
|
await apiClient.extension.plugin.getpluginHaloRunV1alpha1Plugin({
|
||||||
|
name: plugin.metadata.name,
|
||||||
|
});
|
||||||
|
pluginToUpdate.spec.enabled = true;
|
||||||
|
|
||||||
|
await apiClient.extension.plugin.updatepluginHaloRunV1alpha1Plugin({
|
||||||
|
name: pluginToUpdate.metadata.name,
|
||||||
|
plugin: pluginToUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCatchExistsException = async (
|
||||||
|
error: PluginInstallationErrorResponse,
|
||||||
|
file?: File
|
||||||
|
) => {
|
||||||
|
Dialog.info({
|
||||||
|
title: t(
|
||||||
|
"core.plugin.upload_modal.operations.existed_during_installation.title"
|
||||||
|
),
|
||||||
|
description: t(
|
||||||
|
"core.plugin.upload_modal.operations.existed_during_installation.description"
|
||||||
|
),
|
||||||
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
onConfirm: async () => {
|
||||||
|
await apiClient.plugin.upgradePlugin({
|
||||||
|
name: error.pluginName,
|
||||||
|
file: file,
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success(t("core.common.toast.upgrade_success"));
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-3">
|
||||||
|
<AppDownloadAlert />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UppyUpload
|
||||||
|
:restrictions="{
|
||||||
|
maxNumberOfFiles: 1,
|
||||||
|
allowedFileTypes: ['.jar'],
|
||||||
|
}"
|
||||||
|
:endpoint="endpoint"
|
||||||
|
width="100%"
|
||||||
|
auto-proceed
|
||||||
|
@uploaded="onUploaded"
|
||||||
|
@error="onError"
|
||||||
|
/>
|
||||||
|
</template>
|
|
@ -0,0 +1,169 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { apiClient } from "@/utils/api-client";
|
||||||
|
import { Dialog, Toast, VButton } from "@halo-dev/components";
|
||||||
|
import type { Plugin } from "@halo-dev/api-client";
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
import { inject } from "vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
|
import type { PluginInstallationErrorResponse } from "../../types";
|
||||||
|
import { PLUGIN_ALREADY_EXISTS_TYPE } from "../../constants";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
import { submitForm } from "@formkit/core";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "close-modal"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const pluginToUpgrade = inject<Ref<Plugin | undefined>>(
|
||||||
|
"pluginToUpgrade",
|
||||||
|
ref()
|
||||||
|
);
|
||||||
|
|
||||||
|
const remoteDownloadUrl = ref("");
|
||||||
|
const downloading = ref(false);
|
||||||
|
|
||||||
|
const handleDownloadPlugin = async () => {
|
||||||
|
try {
|
||||||
|
downloading.value = true;
|
||||||
|
if (pluginToUpgrade.value) {
|
||||||
|
await apiClient.plugin.upgradePluginFromUri({
|
||||||
|
name: pluginToUpgrade.value.metadata.name,
|
||||||
|
upgradeFromUriRequest: {
|
||||||
|
uri: remoteDownloadUrl.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success(t("core.common.toast.upgrade_success"));
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: plugin } = await apiClient.plugin.installPluginFromUri({
|
||||||
|
installFromUriRequest: {
|
||||||
|
uri: remoteDownloadUrl.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
emit("close-modal");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["plugins"] });
|
||||||
|
|
||||||
|
handleShowActiveModalAfterInstall(plugin);
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
} catch (error: any) {
|
||||||
|
const data = error?.response.data as PluginInstallationErrorResponse;
|
||||||
|
if (data?.type === PLUGIN_ALREADY_EXISTS_TYPE) {
|
||||||
|
handleCatchExistsException(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Failed to download plugin", error);
|
||||||
|
} finally {
|
||||||
|
routeRemoteDownloadUrl.value = null;
|
||||||
|
downloading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowActiveModalAfterInstall = (plugin: Plugin) => {
|
||||||
|
Dialog.success({
|
||||||
|
title: t("core.plugin.upload_modal.operations.active_after_install.title"),
|
||||||
|
description: t(
|
||||||
|
"core.plugin.upload_modal.operations.active_after_install.description"
|
||||||
|
),
|
||||||
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
const { data: pluginToUpdate } =
|
||||||
|
await apiClient.extension.plugin.getpluginHaloRunV1alpha1Plugin({
|
||||||
|
name: plugin.metadata.name,
|
||||||
|
});
|
||||||
|
pluginToUpdate.spec.enabled = true;
|
||||||
|
|
||||||
|
await apiClient.extension.plugin.updatepluginHaloRunV1alpha1Plugin({
|
||||||
|
name: pluginToUpdate.metadata.name,
|
||||||
|
plugin: pluginToUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCatchExistsException = async (
|
||||||
|
error: PluginInstallationErrorResponse
|
||||||
|
) => {
|
||||||
|
Dialog.info({
|
||||||
|
title: t(
|
||||||
|
"core.plugin.upload_modal.operations.existed_during_installation.title"
|
||||||
|
),
|
||||||
|
description: t(
|
||||||
|
"core.plugin.upload_modal.operations.existed_during_installation.description"
|
||||||
|
),
|
||||||
|
confirmText: t("core.common.buttons.confirm"),
|
||||||
|
cancelText: t("core.common.buttons.cancel"),
|
||||||
|
onConfirm: async () => {
|
||||||
|
await apiClient.plugin.upgradePluginFromUri({
|
||||||
|
name: error.pluginName,
|
||||||
|
upgradeFromUriRequest: {
|
||||||
|
uri: remoteDownloadUrl.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success(t("core.common.toast.upgrade_success"));
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle remote download url from route
|
||||||
|
const routeRemoteDownloadUrl = useRouteQuery<string | null>(
|
||||||
|
"remote-download-url"
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (routeRemoteDownloadUrl.value) {
|
||||||
|
remoteDownloadUrl.value = routeRemoteDownloadUrl.value;
|
||||||
|
nextTick(() => {
|
||||||
|
submitForm("plugin-remote-download-form");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormKit
|
||||||
|
id="plugin-remote-download-form"
|
||||||
|
name="plugin-remote-download-form"
|
||||||
|
type="form"
|
||||||
|
:preserve="true"
|
||||||
|
@submit="handleDownloadPlugin"
|
||||||
|
>
|
||||||
|
<FormKit
|
||||||
|
v-model="remoteDownloadUrl"
|
||||||
|
:label="$t('core.plugin.upload_modal.tabs.remote.fields.url')"
|
||||||
|
type="text"
|
||||||
|
validation="required"
|
||||||
|
></FormKit>
|
||||||
|
</FormKit>
|
||||||
|
|
||||||
|
<div class="pt-5">
|
||||||
|
<VButton
|
||||||
|
:loading="downloading"
|
||||||
|
type="secondary"
|
||||||
|
@click="$formkit.submit('plugin-remote-download-form')"
|
||||||
|
>
|
||||||
|
{{ $t("core.common.buttons.download") }}
|
||||||
|
</VButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const PLUGIN_ALREADY_EXISTS_TYPE =
|
||||||
|
"https://halo.run/probs/plugin-alreay-exists";
|
|
@ -0,0 +1,10 @@
|
||||||
|
export interface PluginInstallationErrorResponse {
|
||||||
|
detail: string;
|
||||||
|
instance: string;
|
||||||
|
pluginName: string;
|
||||||
|
requestId: string;
|
||||||
|
status: number;
|
||||||
|
timestamp: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
}
|
Loading…
Reference in New Issue