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())
|
||||
));
|
||||
});
|
||||
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(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 { IconPlug } from "@halo-dev/components";
|
||||
import { IconPlug, IconSettings } from "@halo-dev/components";
|
||||
import { definePlugin } from "@halo-dev/console-shared";
|
||||
import { markRaw } from "vue";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import PluginDetail from "./PluginDetail.vue";
|
||||
import PluginExtensionPointSettings from "./PluginExtensionPointSettings.vue";
|
||||
import PluginList from "./PluginList.vue";
|
||||
|
||||
export default definePlugin({
|
||||
|
@ -29,6 +31,19 @@ export default definePlugin({
|
|||
name: "Plugins",
|
||||
component: PluginList,
|
||||
},
|
||||
{
|
||||
path: "extension-point-settings",
|
||||
name: "PluginExtensionPointSettings",
|
||||
component: PluginExtensionPointSettings,
|
||||
meta: {
|
||||
title: "扩展点设置",
|
||||
hideFooter: true,
|
||||
menu: {
|
||||
name: "扩展点设置",
|
||||
icon: markRaw(IconSettings),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ":name",
|
||||
name: "PluginDetail",
|
||||
|
@ -39,6 +54,6 @@ export default definePlugin({
|
|||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as RouteRecordRaw,
|
||||
],
|
||||
});
|
||||
|
|
|
@ -452,3 +452,4 @@ export {
|
|||
defaultPublicApiClient as publicApiClient,
|
||||
defaultUcApiClient as ucApiClient
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue