mirror of https://github.com/halo-dev/halo
feat: add management and view pages for extension points (#6137)
#### What type of PR is this? /area ui /kind feature /milestone 2.17.x #### What this PR does / why we need it: 添加扩展点的查看和设置页面。 <img width="1414" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/4dd4660f-540f-46b5-8250-b4f011ebaae6"> #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/3206 #### Does this PR introduce a user-facing change? ```release-note 添加系统扩展点的查看和设置页面。 ```pull/6082/head
parent
49c4a917e1
commit
5eabce7544
|
@ -81,7 +81,13 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
|
||||||
definition -> definition.getSpec().getClassName())
|
definition -> definition.getSpec().getClassName())
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
schemeManager.register(ExtensionDefinition.class);
|
schemeManager.register(ExtensionDefinition.class, indexSpecs -> {
|
||||||
|
indexSpecs.add(new IndexSpec()
|
||||||
|
.setName("spec.extensionPointName")
|
||||||
|
.setIndexFunc(simpleAttribute(ExtensionDefinition.class,
|
||||||
|
definition -> definition.getSpec().getExtensionPointName())
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
schemeManager.register(RoleBinding.class);
|
schemeManager.register(RoleBinding.class);
|
||||||
schemeManager.register(User.class, indexSpecs -> {
|
schemeManager.register(User.class, indexSpecs -> {
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { coreApiClient } from "@halo-dev/api-client";
|
||||||
|
import { IconSettings, VCard, VPageHeader } from "@halo-dev/components";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
|
import { computed, watch } from "vue";
|
||||||
|
import ExtensionDefinitionMultiInstanceView from "./components/extension-points/ExtensionDefinitionMultiInstanceView.vue";
|
||||||
|
import ExtensionDefinitionSingletonView from "./components/extension-points/ExtensionDefinitionSingletonView.vue";
|
||||||
|
|
||||||
|
const { data: extensionPointDefinitions } = useQuery({
|
||||||
|
queryKey: ["extension-point-definitions"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } =
|
||||||
|
await coreApiClient.plugin.extensionPointDefinition.listExtensionPointDefinition();
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedExtensionPointDefinitionName = useRouteQuery<string | undefined>(
|
||||||
|
"extension-point-definition-name"
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedExtensionPointDefinition = computed(() => {
|
||||||
|
return extensionPointDefinitions.value?.items.find(
|
||||||
|
(item) => item.metadata.name === selectedExtensionPointDefinitionName.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => extensionPointDefinitions.value,
|
||||||
|
(value) => {
|
||||||
|
if (value?.items.length && !selectedExtensionPointDefinitionName.value) {
|
||||||
|
selectedExtensionPointDefinitionName.value =
|
||||||
|
value?.items[0].metadata.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VPageHeader title="扩展点设置">
|
||||||
|
<template #icon>
|
||||||
|
<IconSettings class="mr-2 self-center" />
|
||||||
|
</template>
|
||||||
|
</VPageHeader>
|
||||||
|
|
||||||
|
<div class="m-0 md:m-4">
|
||||||
|
<VCard
|
||||||
|
style="height: calc(100vh - 5.5rem)"
|
||||||
|
:body-class="['h-full', '!p-0']"
|
||||||
|
>
|
||||||
|
<div class="flex h-full divide-x">
|
||||||
|
<div class="w-72 flex-none">
|
||||||
|
<div
|
||||||
|
class="sticky top-0 z-10 flex h-12 items-center border-b bg-white px-4"
|
||||||
|
>
|
||||||
|
<h2 class="font-semibold text-green-900">扩展点定义</h2>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="box-border h-full w-full divide-y divide-gray-100 overflow-auto"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="extensionPointDefinition in extensionPointDefinitions?.items"
|
||||||
|
:key="extensionPointDefinition.metadata.name"
|
||||||
|
class="relative cursor-pointer"
|
||||||
|
@click="
|
||||||
|
selectedExtensionPointDefinitionName =
|
||||||
|
extensionPointDefinition.metadata.name
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="
|
||||||
|
selectedExtensionPointDefinitionName ===
|
||||||
|
extensionPointDefinition.metadata.name
|
||||||
|
"
|
||||||
|
class="absolute inset-y-0 left-0 w-0.5 bg-primary"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="flex flex-col space-y-1.5 px-4 py-2.5 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
class="line-clamp-1 break-words text-sm font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
{{ extensionPointDefinition.spec.displayName }}
|
||||||
|
</h3>
|
||||||
|
<p class="line-clamp-2 text-xs text-gray-600">
|
||||||
|
{{ extensionPointDefinition.spec.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="flex min-w-0 flex-1 shrink flex-col overflow-auto">
|
||||||
|
<div
|
||||||
|
class="sticky top-0 z-10 flex h-12 items-center space-x-3 border-b bg-white px-4"
|
||||||
|
>
|
||||||
|
<h2 class="font-semibold text-green-900">
|
||||||
|
{{ selectedExtensionPointDefinition?.spec.displayName }}
|
||||||
|
</h2>
|
||||||
|
<small class="line-clamp-1 text-gray-600">
|
||||||
|
{{ selectedExtensionPointDefinition?.spec.description }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<ExtensionDefinitionSingletonView
|
||||||
|
v-if="selectedExtensionPointDefinition?.spec.type === 'SINGLETON'"
|
||||||
|
:extension-point-definition="selectedExtensionPointDefinition"
|
||||||
|
/>
|
||||||
|
<ExtensionDefinitionMultiInstanceView
|
||||||
|
v-else-if="
|
||||||
|
selectedExtensionPointDefinition?.spec.type === 'MULTI_INSTANCE'
|
||||||
|
"
|
||||||
|
:extension-point-definition="selectedExtensionPointDefinition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,77 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
coreApiClient,
|
||||||
|
type ExtensionDefinition,
|
||||||
|
type Plugin,
|
||||||
|
} from "@halo-dev/api-client";
|
||||||
|
import {
|
||||||
|
IconSettings,
|
||||||
|
VAvatar,
|
||||||
|
VEntity,
|
||||||
|
VEntityField,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{ extensionDefinition: ExtensionDefinition }>(),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: plugins } = useQuery<Plugin[]>({
|
||||||
|
queryKey: ["extension-definition-related-plugins"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await coreApiClient.plugin.plugin.listPlugin({
|
||||||
|
page: 0,
|
||||||
|
size: 0,
|
||||||
|
});
|
||||||
|
return data.items;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchedPlugin = computed(() => {
|
||||||
|
return plugins.value?.find(
|
||||||
|
(plugin) =>
|
||||||
|
plugin.metadata.name ===
|
||||||
|
props.extensionDefinition.metadata.labels?.["plugin.halo.run/plugin-name"]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VEntity>
|
||||||
|
<template v-if="$slots['selection-indicator']" #checkbox>
|
||||||
|
<slot name="selection-indicator" />
|
||||||
|
</template>
|
||||||
|
<template #start>
|
||||||
|
<VEntityField>
|
||||||
|
<template #description>
|
||||||
|
<VAvatar
|
||||||
|
:alt="extensionDefinition.spec.displayName"
|
||||||
|
:src="extensionDefinition.spec.icon || matchedPlugin?.status?.logo"
|
||||||
|
size="md"
|
||||||
|
></VAvatar>
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
|
<VEntityField
|
||||||
|
:title="extensionDefinition.spec.displayName"
|
||||||
|
:description="extensionDefinition.spec.description"
|
||||||
|
></VEntityField>
|
||||||
|
</template>
|
||||||
|
<template v-if="matchedPlugin" #end>
|
||||||
|
<VEntityField>
|
||||||
|
<template #description>
|
||||||
|
<RouterLink
|
||||||
|
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 },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<IconSettings />
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
|
</template>
|
||||||
|
</VEntity>
|
||||||
|
</template>
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ExtensionPointDefinition } from "@halo-dev/api-client";
|
||||||
|
import { VEmpty } from "@halo-dev/components";
|
||||||
|
import { toRefs } from "vue";
|
||||||
|
import { useExtensionDefinitionFetch } from "../../composables/use-extension-definition-fetch";
|
||||||
|
import ExtensionDefinitionListItem from "./ExtensionDefinitionListItem.vue";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{ extensionPointDefinition?: ExtensionPointDefinition }>(),
|
||||||
|
{ extensionPointDefinition: undefined }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { extensionPointDefinition } = toRefs(props);
|
||||||
|
|
||||||
|
const { data: extensionDefinitions, isLoading } = useExtensionDefinitionFetch(
|
||||||
|
extensionPointDefinition
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<VLoading v-if="isLoading"></VLoading>
|
||||||
|
<Transition
|
||||||
|
v-else-if="!extensionDefinitions?.items.length"
|
||||||
|
appear
|
||||||
|
name="fade"
|
||||||
|
>
|
||||||
|
<VEmpty title="当前没有扩展点实现"></VEmpty>
|
||||||
|
</Transition>
|
||||||
|
<Transition v-else name="fade" appear>
|
||||||
|
<ul
|
||||||
|
class="box-border h-full w-full divide-y divide-gray-100 overflow-hidden rounded-base border"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="item in extensionDefinitions?.items"
|
||||||
|
:key="item.metadata.name"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
<ExtensionDefinitionListItem :extension-definition="item" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,141 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
coreApiClient,
|
||||||
|
type ExtensionPointDefinition,
|
||||||
|
} from "@halo-dev/api-client";
|
||||||
|
import { Toast, VEmpty, VLoading } from "@halo-dev/components";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||||
|
import { computed, ref, toRefs } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useExtensionDefinitionFetch } from "../../composables/use-extension-definition-fetch";
|
||||||
|
import ExtensionDefinitionListItem from "./ExtensionDefinitionListItem.vue";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const Q_KEY = (name?: string) => ["extension-point-value", name];
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{ extensionPointDefinition?: ExtensionPointDefinition }>(),
|
||||||
|
{ extensionPointDefinition: undefined }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { extensionPointDefinition } = toRefs(props);
|
||||||
|
|
||||||
|
const { data: extensionDefinitions, isLoading } = useExtensionDefinitionFetch(
|
||||||
|
extensionPointDefinition
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: value } = useQuery({
|
||||||
|
queryKey: Q_KEY(extensionPointDefinition.value?.metadata.name),
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!extensionPointDefinition.value) return null;
|
||||||
|
|
||||||
|
const { data } = await coreApiClient.configMap.getConfigMap({
|
||||||
|
name: "system",
|
||||||
|
});
|
||||||
|
|
||||||
|
const extensionPointEnabled = JSON.parse(
|
||||||
|
data.data?.["extensionPointEnabled"] || "{}"
|
||||||
|
);
|
||||||
|
|
||||||
|
const extensionPointValue =
|
||||||
|
extensionPointEnabled[extensionPointDefinition.value?.metadata.name];
|
||||||
|
|
||||||
|
// check is array
|
||||||
|
if (Array.isArray(extensionPointValue)) {
|
||||||
|
return extensionPointValue[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
enabled: computed(() => !!extensionPointDefinition.value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSubmitting = ref(false);
|
||||||
|
|
||||||
|
async function onExtensionChange(e: Event) {
|
||||||
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
|
||||||
|
if (!extensionPointDefinition.value) return;
|
||||||
|
|
||||||
|
isSubmitting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: configMap } = await coreApiClient.configMap.getConfigMap({
|
||||||
|
name: "system",
|
||||||
|
});
|
||||||
|
|
||||||
|
const extensionPointEnabled = JSON.parse(
|
||||||
|
configMap.data?.["extensionPointEnabled"] || "{}"
|
||||||
|
);
|
||||||
|
|
||||||
|
extensionPointEnabled[extensionPointDefinition.value?.metadata.name] = [
|
||||||
|
value,
|
||||||
|
];
|
||||||
|
|
||||||
|
await coreApiClient.configMap.patchConfigMap({
|
||||||
|
name: "system",
|
||||||
|
jsonPatchInner: [
|
||||||
|
{
|
||||||
|
op: "add",
|
||||||
|
path: "/data/extensionPointEnabled",
|
||||||
|
value: JSON.stringify(extensionPointEnabled),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success(t("core.common.toast.save_success"));
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: Q_KEY(extensionPointDefinition.value?.metadata.name),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Toast.error(t("core.common.toast.save_failed_and_retry"));
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4">
|
||||||
|
<VLoading v-if="isLoading"></VLoading>
|
||||||
|
<Transition
|
||||||
|
v-else-if="!extensionDefinitions?.items.length"
|
||||||
|
appear
|
||||||
|
name="fade"
|
||||||
|
>
|
||||||
|
<VEmpty title="当前没有扩展点实现"></VEmpty>
|
||||||
|
</Transition>
|
||||||
|
<Transition v-else name="fade" appear>
|
||||||
|
<ul
|
||||||
|
class="box-border h-full w-full divide-y divide-gray-100 overflow-hidden rounded-base border"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="item in extensionDefinitions?.items"
|
||||||
|
:key="item.metadata.name"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="cursor-pointer transition-all"
|
||||||
|
:class="{ 'pointer-events-none opacity-50': isSubmitting }"
|
||||||
|
>
|
||||||
|
<ExtensionDefinitionListItem :extension-definition="item">
|
||||||
|
<template #selection-indicator>
|
||||||
|
<input
|
||||||
|
:value="item.metadata.name"
|
||||||
|
type="radio"
|
||||||
|
name="activated-extension"
|
||||||
|
:checked="item.metadata.name === value"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
@change="onExtensionChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ExtensionDefinitionListItem>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { ExtensionPointDefinition } from "@halo-dev/api-client";
|
||||||
|
import { coreApiClient } from "@halo-dev/api-client";
|
||||||
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
|
import { computed, type Ref } from "vue";
|
||||||
|
|
||||||
|
export function useExtensionDefinitionFetch(
|
||||||
|
extensionPointDefinition: Ref<ExtensionPointDefinition | undefined>
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["extension-definitions", extensionPointDefinition],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } =
|
||||||
|
await coreApiClient.plugin.extensionDefinition.listExtensionDefinition({
|
||||||
|
fieldSelector: [
|
||||||
|
`spec.extensionPointName=${extensionPointDefinition.value?.metadata.name}`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: computed(() => !!extensionPointDefinition.value),
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
import BasicLayout from "@console/layouts/BasicLayout.vue";
|
import BasicLayout from "@console/layouts/BasicLayout.vue";
|
||||||
import { IconPlug } from "@halo-dev/components";
|
import { IconPlug, IconSettings } from "@halo-dev/components";
|
||||||
import { definePlugin } from "@halo-dev/console-shared";
|
import { definePlugin } from "@halo-dev/console-shared";
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
import PluginDetail from "./PluginDetail.vue";
|
import PluginDetail from "./PluginDetail.vue";
|
||||||
|
import PluginExtensionPointSettings from "./PluginExtensionPointSettings.vue";
|
||||||
import PluginList from "./PluginList.vue";
|
import PluginList from "./PluginList.vue";
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
|
@ -29,6 +31,19 @@ export default definePlugin({
|
||||||
name: "Plugins",
|
name: "Plugins",
|
||||||
component: PluginList,
|
component: PluginList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "extension-point-settings",
|
||||||
|
name: "PluginExtensionPointSettings",
|
||||||
|
component: PluginExtensionPointSettings,
|
||||||
|
meta: {
|
||||||
|
title: "扩展点设置",
|
||||||
|
hideFooter: true,
|
||||||
|
menu: {
|
||||||
|
name: "扩展点设置",
|
||||||
|
icon: markRaw(IconSettings),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ":name",
|
path: ":name",
|
||||||
name: "PluginDetail",
|
name: "PluginDetail",
|
||||||
|
@ -39,6 +54,6 @@ export default definePlugin({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
} as RouteRecordRaw,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
@ -452,3 +452,4 @@ export {
|
||||||
defaultPublicApiClient as publicApiClient,
|
defaultPublicApiClient as publicApiClient,
|
||||||
defaultUcApiClient as ucApiClient
|
defaultUcApiClient as ucApiClient
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue