Skip to content

Commit

Permalink
feat: add plugin detail modal as global component (#6233)
Browse files Browse the repository at this point in the history
#### 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 #6232

#### Does this PR introduce a user-facing change?

```release-note
为 UI 添加通用的插件设置弹窗,以供插件主动调用
```
  • Loading branch information
ruibaby authored Jul 1, 2024
1 parent 8a61a39 commit 2aaf64a
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 136 deletions.
120 changes: 10 additions & 110 deletions ui/console-src/modules/system/plugins/PluginDetail.vue
Original file line number Diff line number Diff line change
@@ -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);
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())];
}
},
});
const { plugin, setting, activeTab, tabs } = usePluginDetailTabs(
name as Ref<string | undefined>,
true
);
provide<Ref<string>>("activeTab", activeTab);
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">
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>(),
Expand All @@ -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" />
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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[]>(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
<script lang="ts" setup>
// core libs
import { computed, inject, ref, type Ref } from "vue";
// hooks
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";
// components
import { Toast, VButton } from "@halo-dev/components";
// types
import StickyBlock from "@/components/sticky-block/StickyBlock.vue";
import type { ConfigMap, Plugin, Setting } from "@halo-dev/api-client";
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();
Expand Down
Loading

0 comments on commit 2aaf64a

Please sign in to comment.