mirror of https://github.com/halo-dev/halo
feat: add plugin detail modal as global component (#6233)
#### What type of PR is this? /area ui /kind feature /milestone 2.17.x #### What this PR does / why we need it: 添加 PluginDetailModal,用于打开插件的设置界面。并在扩展设置页面适配以测试。 <img width="1643" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/4bb38ab1-ed51-4437-8202-ccaf9f79cb41"> #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/6232 #### Does this PR introduce a user-facing change? ```release-note 为 UI 添加通用的插件设置弹窗,以供插件主动调用 ```pull/6242/head
parent
8a61a39be3
commit
2aaf64aa34
|
@ -1,123 +1,23 @@
|
|||
<script lang="ts" setup>
|
||||
// core libs
|
||||
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
|
||||
import { computed, provide, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
// libs
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
// components
|
||||
import type { Plugin, Setting } from "@halo-dev/api-client";
|
||||
import { VAvatar, VCard, VPageHeader, VTabbar } from "@halo-dev/components";
|
||||
|
||||
// types
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import type { Plugin, Setting, SettingForm } from "@halo-dev/api-client";
|
||||
import type { PluginTab } from "@halo-dev/console-shared";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import type { Ref } from "vue";
|
||||
import { markRaw } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import DetailTab from "./tabs/Detail.vue";
|
||||
import SettingTab from "./tabs/Setting.vue";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialTabs = ref<PluginTab[]>([
|
||||
{
|
||||
id: "detail",
|
||||
label: t("core.plugin.tabs.detail"),
|
||||
component: markRaw(DetailTab),
|
||||
},
|
||||
]);
|
||||
import { provide, toRefs } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { usePluginDetailTabs } from "./composables/use-plugin";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const tabs = ref<PluginTab[]>(cloneDeep(initialTabs.value));
|
||||
const activeTab = useRouteQuery<string>("tab", tabs.value[0].id);
|
||||
const { name } = toRefs(route.params);
|
||||
|
||||
const { plugin, setting, activeTab, tabs } = usePluginDetailTabs(
|
||||
name as Ref<string | undefined>,
|
||||
true
|
||||
);
|
||||
|
||||
provide<Ref<string>>("activeTab", activeTab);
|
||||
|
||||
const { data: plugin } = useQuery({
|
||||
queryKey: ["plugin", route.params.name],
|
||||
queryFn: async () => {
|
||||
const { data } = await coreApiClient.plugin.plugin.getPlugin({
|
||||
name: route.params.name as string,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
async onSuccess(data) {
|
||||
if (
|
||||
!data.spec.settingName ||
|
||||
!currentUserHasPermission(["system:plugins:manage"])
|
||||
) {
|
||||
tabs.value = [...initialTabs.value, ...(await getTabsFromExtensions())];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
provide<Ref<Plugin | undefined>>("plugin", plugin);
|
||||
|
||||
const { data: setting } = useQuery({
|
||||
queryKey: ["plugin-setting", plugin],
|
||||
queryFn: async () => {
|
||||
const { data } = await consoleApiClient.plugin.plugin.fetchPluginSetting({
|
||||
name: plugin.value?.metadata.name as string,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: computed(() => {
|
||||
return (
|
||||
!!plugin.value &&
|
||||
!!plugin.value.spec.settingName &&
|
||||
currentUserHasPermission(["system:plugins:manage"])
|
||||
);
|
||||
}),
|
||||
async onSuccess(data) {
|
||||
if (data) {
|
||||
const { forms } = data.spec;
|
||||
tabs.value = [
|
||||
...initialTabs.value,
|
||||
...(await getTabsFromExtensions()),
|
||||
...forms.map((item: SettingForm) => {
|
||||
return {
|
||||
id: item.group,
|
||||
label: item.label || "",
|
||||
component: markRaw(SettingTab),
|
||||
};
|
||||
}),
|
||||
] as PluginTab[];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
provide<Ref<Setting | undefined>>("setting", setting);
|
||||
|
||||
async function getTabsFromExtensions() {
|
||||
const { pluginModuleMap } = usePluginModuleStore();
|
||||
|
||||
const currentPluginModule = pluginModuleMap[route.params.name as string];
|
||||
|
||||
if (!currentPluginModule) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const callbackFunction =
|
||||
currentPluginModule?.extensionPoints?.["plugin:self:tabs:create"];
|
||||
|
||||
if (typeof callbackFunction !== "function") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pluginTabs = await callbackFunction();
|
||||
|
||||
return pluginTabs.filter((tab) => {
|
||||
return currentUserHasPermission(tab.permissions);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<VPageHeader :title="plugin?.spec?.displayName">
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Plugin, Setting } from "@halo-dev/api-client";
|
||||
import { IconLink, VButton, VModal, VTabbar } from "@halo-dev/components";
|
||||
import { provide, ref, toRefs, type Ref } from "vue";
|
||||
import { usePluginDetailTabs } from "../composables/use-plugin";
|
||||
|
||||
const props = withDefaults(defineProps<{ name: string }>(), {});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "close"): void;
|
||||
}>();
|
||||
|
||||
const modal = ref<InstanceType<typeof VModal> | null>(null);
|
||||
|
||||
const { name } = toRefs(props);
|
||||
|
||||
const { plugin, setting, tabs, activeTab } = usePluginDetailTabs(name, false);
|
||||
|
||||
provide<Ref<string>>("activeTab", activeTab);
|
||||
provide<Ref<Plugin | undefined>>("plugin", plugin);
|
||||
provide<Ref<Setting | undefined>>("setting", setting);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VModal
|
||||
ref="modal"
|
||||
:title="plugin?.spec.displayName"
|
||||
:centered="true"
|
||||
:width="920"
|
||||
height="calc(100vh - 20px)"
|
||||
mount-to-body
|
||||
@close="emit('close')"
|
||||
>
|
||||
<template #actions>
|
||||
<span>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'PluginDetail',
|
||||
params: { name },
|
||||
}"
|
||||
>
|
||||
<IconLink />
|
||||
</RouterLink>
|
||||
</span>
|
||||
</template>
|
||||
<VTabbar
|
||||
v-model:active-id="activeTab"
|
||||
:items="
|
||||
tabs.map((tab) => {
|
||||
return { label: tab.label, id: tab.id };
|
||||
})
|
||||
"
|
||||
type="outline"
|
||||
/>
|
||||
<div class="-m-4 mt-2">
|
||||
<template v-for="tab in tabs" :key="tab.id">
|
||||
<component :is="tab.component" v-if="activeTab === tab.id" />
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<VButton @click="modal?.close()">
|
||||
{{ $t("core.common.buttons.close") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VModal>
|
||||
</template>
|
|
@ -11,7 +11,8 @@ import {
|
|||
VEntityField,
|
||||
} from "@halo-dev/components";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { computed } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import PluginDetailModal from "../PluginDetailModal.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ extensionDefinition: ExtensionDefinition }>(),
|
||||
|
@ -36,9 +37,16 @@ const matchedPlugin = computed(() => {
|
|||
props.extensionDefinition.metadata.labels?.["plugin.halo.run/plugin-name"]
|
||||
);
|
||||
});
|
||||
|
||||
const pluginDetailModalVisible = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PluginDetailModal
|
||||
v-if="pluginDetailModalVisible && matchedPlugin"
|
||||
:name="matchedPlugin.metadata.name"
|
||||
@close="pluginDetailModalVisible = false"
|
||||
/>
|
||||
<VEntity>
|
||||
<template v-if="$slots['selection-indicator']" #checkbox>
|
||||
<slot name="selection-indicator" />
|
||||
|
@ -61,15 +69,12 @@ const matchedPlugin = computed(() => {
|
|||
<template v-if="matchedPlugin" #end>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<RouterLink
|
||||
<div
|
||||
class="cursor-pointer rounded p-1 text-gray-600 transition-all hover:text-blue-600 group-hover:bg-gray-200/60"
|
||||
:to="{
|
||||
name: 'PluginDetail',
|
||||
params: { name: matchedPlugin?.metadata.name },
|
||||
}"
|
||||
@click.prevent="pluginDetailModalVisible = true"
|
||||
>
|
||||
<IconSettings />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
|
|
|
@ -131,6 +131,7 @@ async function onExtensionChange(e: Event) {
|
|||
<label
|
||||
class="cursor-pointer transition-all"
|
||||
:class="{ 'pointer-events-none opacity-50': isSubmitting }"
|
||||
@click.stop
|
||||
>
|
||||
<ExtensionDefinitionListItem :extension-definition="item">
|
||||
<template #selection-indicator>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { pluginLabels, roleLabels } from "@/constants/labels";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import {
|
||||
PluginStatusPhaseEnum,
|
||||
coreApiClient,
|
||||
|
@ -18,8 +19,10 @@ import {
|
|||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import PluginConditionsModal from "../components/PluginConditionsModal.vue";
|
||||
import { usePluginLifeCycle } from "../composables/use-plugin";
|
||||
import { usePluginLifeCycle } from "../../composables/use-plugin";
|
||||
import PluginConditionsModal from "../PluginConditionsModal.vue";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const plugin = inject<Ref<Plugin | undefined>>("plugin");
|
||||
const { changeStatus, changingStatus } = usePluginLifeCycle(plugin);
|
||||
|
@ -45,7 +48,11 @@ const { data: pluginRoleTemplates } = useQuery({
|
|||
return data.items;
|
||||
},
|
||||
cacheTime: 0,
|
||||
enabled: computed(() => !!plugin?.value?.metadata.name),
|
||||
enabled: computed(
|
||||
() =>
|
||||
!!plugin?.value?.metadata.name &&
|
||||
currentUserHasPermission(["system:roles:view"])
|
||||
),
|
||||
});
|
||||
|
||||
const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|
@ -1,19 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
// core libs
|
||||
import { computed, inject, ref, type Ref } from "vue";
|
||||
|
||||
// hooks
|
||||
import { useSettingFormConvert } from "@console/composables/use-setting-form";
|
||||
import { consoleApiClient } from "@halo-dev/api-client";
|
||||
|
||||
// components
|
||||
import { Toast, VButton } from "@halo-dev/components";
|
||||
|
||||
// types
|
||||
import StickyBlock from "@/components/sticky-block/StickyBlock.vue";
|
||||
import { useSettingFormConvert } from "@console/composables/use-setting-form";
|
||||
import type { ConfigMap, Plugin, Setting } from "@halo-dev/api-client";
|
||||
import { consoleApiClient } from "@halo-dev/api-client";
|
||||
import { Toast, VButton } from "@halo-dev/components";
|
||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { toRaw } from "vue";
|
||||
import { computed, inject, ref, toRaw, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
|
@ -1,15 +1,21 @@
|
|||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import {
|
||||
PluginStatusPhaseEnum,
|
||||
consoleApiClient,
|
||||
coreApiClient,
|
||||
type Plugin,
|
||||
type SettingForm,
|
||||
} from "@halo-dev/api-client";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
import { Dialog, Toast } from "@halo-dev/components";
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import type { PluginTab } from "@halo-dev/console-shared";
|
||||
import { useMutation, useQuery } from "@tanstack/vue-query";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import { computed, markRaw, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import DetailTab from "../components/tabs/Detail.vue";
|
||||
import SettingTab from "../components/tabs/Setting.vue";
|
||||
|
||||
interface usePluginLifeCycleReturn {
|
||||
isStarted: ComputedRef<boolean | undefined>;
|
||||
|
@ -270,3 +276,105 @@ export function usePluginBatchOperations(names: Ref<string[]>) {
|
|||
|
||||
return { handleUninstallInBatch, handleChangeStatusInBatch };
|
||||
}
|
||||
|
||||
export function usePluginDetailTabs(
|
||||
pluginName: Ref<string | undefined>,
|
||||
recordsActiveTab: boolean
|
||||
) {
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
||||
const initialTabs = [
|
||||
{
|
||||
id: "detail",
|
||||
label: t("core.plugin.tabs.detail"),
|
||||
component: markRaw(DetailTab),
|
||||
},
|
||||
];
|
||||
|
||||
const tabs = ref<PluginTab[]>(initialTabs);
|
||||
const activeTab = recordsActiveTab
|
||||
? useRouteQuery<string>("tab", tabs.value[0].id)
|
||||
: ref(tabs.value[0].id);
|
||||
|
||||
const { data: plugin } = useQuery({
|
||||
queryKey: ["plugin", pluginName],
|
||||
queryFn: async () => {
|
||||
const { data } = await coreApiClient.plugin.plugin.getPlugin({
|
||||
name: pluginName.value as string,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
async onSuccess(data) {
|
||||
if (
|
||||
!data.spec.settingName ||
|
||||
!currentUserHasPermission(["system:plugins:manage"])
|
||||
) {
|
||||
tabs.value = [...initialTabs, ...(await getTabsFromExtensions())];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { data: setting } = useQuery({
|
||||
queryKey: ["plugin-setting", plugin],
|
||||
queryFn: async () => {
|
||||
const { data } = await consoleApiClient.plugin.plugin.fetchPluginSetting({
|
||||
name: plugin.value?.metadata.name as string,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
enabled: computed(() => {
|
||||
return (
|
||||
!!plugin.value &&
|
||||
!!plugin.value.spec.settingName &&
|
||||
currentUserHasPermission(["system:plugins:manage"])
|
||||
);
|
||||
}),
|
||||
async onSuccess(data) {
|
||||
if (data) {
|
||||
const { forms } = data.spec;
|
||||
tabs.value = [
|
||||
...initialTabs,
|
||||
...(await getTabsFromExtensions()),
|
||||
...forms.map((item: SettingForm) => {
|
||||
return {
|
||||
id: item.group,
|
||||
label: item.label || "",
|
||||
component: markRaw(SettingTab),
|
||||
};
|
||||
}),
|
||||
] as PluginTab[];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function getTabsFromExtensions() {
|
||||
const { pluginModuleMap } = usePluginModuleStore();
|
||||
|
||||
const currentPluginModule = pluginModuleMap[pluginName.value as string];
|
||||
|
||||
if (!currentPluginModule) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const callbackFunction =
|
||||
currentPluginModule?.extensionPoints?.["plugin:self:tabs:create"];
|
||||
|
||||
if (typeof callbackFunction !== "function") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pluginTabs = await callbackFunction();
|
||||
|
||||
return pluginTabs.filter((tab) => {
|
||||
return currentUserHasPermission(tab.permissions);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
plugin,
|
||||
setting,
|
||||
tabs,
|
||||
activeTab,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,9 +6,12 @@ import type { RouteRecordRaw } from "vue-router";
|
|||
import PluginDetail from "./PluginDetail.vue";
|
||||
import PluginExtensionPointSettings from "./PluginExtensionPointSettings.vue";
|
||||
import PluginList from "./PluginList.vue";
|
||||
import PluginDetailModal from "./components/PluginDetailModal.vue";
|
||||
|
||||
export default definePlugin({
|
||||
components: {},
|
||||
components: {
|
||||
PluginDetailModal,
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
path: "/plugins",
|
||||
|
|
Loading…
Reference in New Issue