mirror of https://github.com/halo-dev/halo-admin
refactor: use tanstack query to refactor plugins fetching (#876)
#### What type of PR is this? /kind improvement #### What this PR does / why we need it: 使用 [TanStack Query](https://github.com/TanStack/query) 重构插件管理列表的数据请求相关逻辑。 #### Which issue(s) this PR fixes: Ref https://github.com/halo-dev/halo/issues/3360 #### Special notes for your reviewer: 测试方式: 1. 需要 `pnpm install` 2. 插件管理页面,安装若干插件。 3. 测试分页、条件筛选等逻辑是否正常。 #### Does this PR introduce a user-facing change? ```release-note None ```pull/868/head^2
parent
c03ea64bf2
commit
816feb9937
|
@ -45,6 +45,7 @@
|
|||
"@halo-dev/components": "workspace:*",
|
||||
"@halo-dev/console-shared": "workspace:*",
|
||||
"@halo-dev/richtext-editor": "0.0.0-alpha.19",
|
||||
"@tanstack/vue-query": "^4.24.10",
|
||||
"@tiptap/extension-character-count": "^2.0.0-beta.202",
|
||||
"@uppy/core": "^3.0.4",
|
||||
"@uppy/dashboard": "^3.2.0",
|
||||
|
|
|
@ -22,6 +22,7 @@ importers:
|
|||
'@rushstack/eslint-patch': ^1.2.0
|
||||
'@tailwindcss/aspect-ratio': ^0.4.2
|
||||
'@tailwindcss/container-queries': ^0.1.0
|
||||
'@tanstack/vue-query': ^4.24.10
|
||||
'@tiptap/extension-character-count': ^2.0.0-beta.202
|
||||
'@types/jsdom': ^20.0.1
|
||||
'@types/lodash.clonedeep': 4.5.7
|
||||
|
@ -115,6 +116,7 @@ importers:
|
|||
'@halo-dev/components': link:packages/components
|
||||
'@halo-dev/console-shared': link:packages/shared
|
||||
'@halo-dev/richtext-editor': 0.0.0-alpha.19_oau5uimcdfyintci7kxkcutjea
|
||||
'@tanstack/vue-query': 4.24.10_vue@3.2.45
|
||||
'@tiptap/extension-character-count': 2.0.0-beta.202_f4ffqkgz5d3wev7su7t7l2rrua
|
||||
'@uppy/core': 3.0.4
|
||||
'@uppy/dashboard': 3.2.0_@uppy+core@3.0.4
|
||||
|
@ -3168,6 +3170,33 @@ packages:
|
|||
tailwindcss: 3.2.4_postcss@8.4.19
|
||||
dev: true
|
||||
|
||||
/@tanstack/match-sorter-utils/8.7.6:
|
||||
resolution: {integrity: sha512-2AMpRiA6QivHOUiBpQAVxjiHAA68Ei23ZUMNaRJrN6omWiSFLoYrxGcT6BXtuzp0Jw4h6HZCmGGIM/gbwebO2A==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
remove-accents: 0.4.2
|
||||
dev: false
|
||||
|
||||
/@tanstack/query-core/4.24.10:
|
||||
resolution: {integrity: sha512-2QywqXEAGBIUoTdgn1lAB4/C8QEqwXHj2jrCLeYTk2xVGtLiPEUD8jcMoeB2noclbiW2mMt4+Fq7fZStuz3wAQ==}
|
||||
dev: false
|
||||
|
||||
/@tanstack/vue-query/4.24.10_vue@3.2.45:
|
||||
resolution: {integrity: sha512-K9mtij3WpQquySsaNhyN5ZQT3oO6Y69J+OT2/NYQYvYCI1AL1S5/sQ1n2FtpyslgrJn9khSerFB7icbYcbcubA==}
|
||||
peerDependencies:
|
||||
'@vue/composition-api': ^1.1.2
|
||||
vue: ^2.5.0 || ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@tanstack/match-sorter-utils': 8.7.6
|
||||
'@tanstack/query-core': 4.24.10
|
||||
'@vue/devtools-api': 6.4.5
|
||||
vue: 3.2.45
|
||||
vue-demi: 0.13.11_vue@3.2.45
|
||||
dev: false
|
||||
|
||||
/@tiptap/core/2.0.0-beta.209_ev2av5mpakmaqws3kqtlepbpdy:
|
||||
resolution: {integrity: sha512-DOOzfo2XKD5Qt2oEGW33/6ugwSnvpl4WbxtlKdPadLoApk6Kja3K1Eps3pihBgIGmo4tkctkCzmj8wNWS7KeWg==}
|
||||
peerDependencies:
|
||||
|
@ -4726,7 +4755,7 @@ packages:
|
|||
/axios/0.21.4_debug@4.3.2:
|
||||
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.2_debug@4.3.2
|
||||
follow-redirects: 1.15.2
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: true
|
||||
|
@ -4742,7 +4771,7 @@ packages:
|
|||
/axios/0.27.2:
|
||||
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.2_debug@4.3.2
|
||||
follow-redirects: 1.15.2
|
||||
form-data: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
@ -5549,6 +5578,7 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/debug/4.3.4:
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
|
@ -6870,6 +6900,15 @@ packages:
|
|||
vue-resize: 2.0.0-alpha.1_vue@3.2.45
|
||||
dev: false
|
||||
|
||||
/follow-redirects/1.15.2:
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
/follow-redirects/1.15.2_debug@4.3.2:
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
@ -6880,6 +6919,7 @@ packages:
|
|||
optional: true
|
||||
dependencies:
|
||||
debug: 4.3.2
|
||||
dev: true
|
||||
|
||||
/foreground-child/2.0.0:
|
||||
resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==}
|
||||
|
@ -8434,6 +8474,7 @@ packages:
|
|||
|
||||
/ms/2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
dev: true
|
||||
|
||||
/ms/2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
@ -9307,6 +9348,10 @@ packages:
|
|||
engines: {node: '>= 0.10'}
|
||||
dev: true
|
||||
|
||||
/remove-accents/0.4.2:
|
||||
resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==}
|
||||
dev: false
|
||||
|
||||
/request-progress/3.0.0:
|
||||
resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==}
|
||||
dependencies:
|
||||
|
|
|
@ -21,6 +21,7 @@ import { useSystemStatesStore } from "./stores/system-states";
|
|||
import { useUserStore } from "./stores/user";
|
||||
import { useSystemConfigMapStore } from "./stores/system-configmap";
|
||||
import i18n from "./locales";
|
||||
import { VueQueryPlugin } from "@tanstack/vue-query";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
|
@ -28,6 +29,7 @@ setupComponents(app);
|
|||
|
||||
app.use(createPinia());
|
||||
app.use(i18n);
|
||||
app.use(VueQueryPlugin);
|
||||
|
||||
function registerModule(pluginModule: PluginModule, core: boolean) {
|
||||
if (pluginModule.components) {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
import { VSwitch, VTag } from "@halo-dev/components";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, inject, ref, watchEffect } from "vue";
|
||||
import { computed, inject } from "vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { Plugin, Role } from "@halo-dev/api-client";
|
||||
import { pluginLabels } from "@/constants/labels";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { usePluginLifeCycle } from "./composables/use-plugin";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
|
||||
const plugin = inject<Ref<Plugin | undefined>>("plugin");
|
||||
const { changeStatus, isStarted } = usePluginLifeCycle(plugin);
|
||||
|
@ -17,24 +18,22 @@ interface RoleTemplateGroup {
|
|||
roles: Role[];
|
||||
}
|
||||
|
||||
const pluginRoleTemplates = ref<Role[]>([]);
|
||||
|
||||
const handleFetchRoles = async () => {
|
||||
try {
|
||||
const { data: pluginRoleTemplates } = useQuery({
|
||||
queryKey: ["plugin-roles", plugin?.value?.metadata.name],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.extension.role.listv1alpha1Role({
|
||||
page: 0,
|
||||
size: 0,
|
||||
labelSelector: [`${pluginLabels.NAME}=${plugin?.value?.metadata.name}`],
|
||||
});
|
||||
pluginRoleTemplates.value = data.items;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return data.items;
|
||||
},
|
||||
});
|
||||
|
||||
const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
||||
const groups: RoleTemplateGroup[] = [];
|
||||
pluginRoleTemplates.value.forEach((role) => {
|
||||
pluginRoleTemplates.value?.forEach((role) => {
|
||||
const group = groups.find(
|
||||
(group) =>
|
||||
group.module === role.metadata.annotations?.[rbacAnnotations.MODULE]
|
||||
|
@ -50,12 +49,6 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
|
|||
});
|
||||
return groups;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (plugin?.value) {
|
||||
handleFetchRoles();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -14,95 +14,15 @@ import {
|
|||
} from "@halo-dev/components";
|
||||
import PluginListItem from "./components/PluginListItem.vue";
|
||||
import PluginUploadModal from "./components/PluginUploadModal.vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { PluginList } from "@halo-dev/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { onBeforeRouteLeave } from "vue-router";
|
||||
import FilterTag from "@/components/filter/FilterTag.vue";
|
||||
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
|
||||
import { getNode } from "@formkit/core";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type { Plugin } from "@halo-dev/api-client";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const plugins = ref<PluginList>({
|
||||
page: 1,
|
||||
size: 20,
|
||||
total: 0,
|
||||
items: [],
|
||||
first: true,
|
||||
last: false,
|
||||
hasNext: false,
|
||||
hasPrevious: false,
|
||||
totalPages: 0,
|
||||
});
|
||||
const loading = ref(false);
|
||||
const pluginInstall = ref(false);
|
||||
const keyword = ref("");
|
||||
const refreshInterval = ref();
|
||||
|
||||
const handleFetchPlugins = async (options?: {
|
||||
mute?: boolean;
|
||||
page?: number;
|
||||
}) => {
|
||||
try {
|
||||
clearInterval(refreshInterval.value);
|
||||
|
||||
if (!options?.mute) {
|
||||
loading.value = true;
|
||||
}
|
||||
|
||||
if (options?.page) {
|
||||
plugins.value.page = options.page;
|
||||
}
|
||||
|
||||
const { data } = await apiClient.plugin.listPlugins({
|
||||
page: plugins.value.page,
|
||||
size: plugins.value.size,
|
||||
keyword: keyword.value,
|
||||
enabled: selectedEnabledItem.value?.value,
|
||||
sort: [selectedSortItem.value?.value].filter(
|
||||
(item) => !!item
|
||||
) as string[],
|
||||
});
|
||||
|
||||
plugins.value = data;
|
||||
|
||||
const deletedPlugins = plugins.value.items.filter(
|
||||
(plugin) => !!plugin.metadata.deletionTimestamp
|
||||
);
|
||||
|
||||
if (deletedPlugins.length) {
|
||||
refreshInterval.value = setInterval(() => {
|
||||
handleFetchPlugins({ mute: true });
|
||||
}, 3000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch plugins", e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
clearInterval(refreshInterval.value);
|
||||
});
|
||||
|
||||
const handlePaginationChange = ({
|
||||
page,
|
||||
size,
|
||||
}: {
|
||||
page: number;
|
||||
size: number;
|
||||
}) => {
|
||||
plugins.value.page = page;
|
||||
plugins.value.size = size;
|
||||
handleFetchPlugins();
|
||||
};
|
||||
|
||||
onMounted(handleFetchPlugins);
|
||||
|
||||
// Filters
|
||||
interface EnabledItem {
|
||||
label: string;
|
||||
value?: boolean;
|
||||
|
@ -113,6 +33,16 @@ interface SortItem {
|
|||
value: string;
|
||||
}
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const pluginInstall = ref(false);
|
||||
|
||||
const keyword = ref("");
|
||||
const page = ref(1);
|
||||
const size = ref(20);
|
||||
const total = ref(0);
|
||||
|
||||
// Filters
|
||||
const EnabledItems: EnabledItem[] = [
|
||||
{
|
||||
label: "全部",
|
||||
|
@ -144,12 +74,12 @@ const selectedSortItem = ref<SortItem>();
|
|||
|
||||
function handleEnabledItemChange(enabledItem: EnabledItem) {
|
||||
selectedEnabledItem.value = enabledItem;
|
||||
handleFetchPlugins({ page: 1 });
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
function handleSortItemChange(sortItem?: SortItem) {
|
||||
selectedSortItem.value = sortItem;
|
||||
handleFetchPlugins({ page: 1 });
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
function handleKeywordChange() {
|
||||
|
@ -157,12 +87,12 @@ function handleKeywordChange() {
|
|||
if (keywordNode) {
|
||||
keyword.value = keywordNode._value as string;
|
||||
}
|
||||
handleFetchPlugins({ page: 1 });
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
function handleClearKeyword() {
|
||||
keyword.value = "";
|
||||
handleFetchPlugins({ page: 1 });
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
const hasFilters = computed(() => {
|
||||
|
@ -177,14 +107,47 @@ function handleClearFilters() {
|
|||
selectedEnabledItem.value = undefined;
|
||||
selectedSortItem.value = undefined;
|
||||
keyword.value = "";
|
||||
handleFetchPlugins({ page: 1 });
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
|
||||
queryKey: [
|
||||
"plugins",
|
||||
page,
|
||||
size,
|
||||
keyword,
|
||||
selectedEnabledItem,
|
||||
selectedSortItem,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.plugin.listPlugins({
|
||||
page: page.value,
|
||||
size: size.value,
|
||||
keyword: keyword.value,
|
||||
enabled: selectedEnabledItem.value?.value,
|
||||
sort: [selectedSortItem.value?.value].filter(Boolean) as string[],
|
||||
});
|
||||
|
||||
total.value = data.total;
|
||||
|
||||
return data.items;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
keepPreviousData: true,
|
||||
refetchInterval: (data) => {
|
||||
const deletingPlugins = data?.filter(
|
||||
(plugin) => !!plugin.metadata.deletionTimestamp
|
||||
);
|
||||
|
||||
return deletingPlugins?.length ? 3000 : false;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<PluginUploadModal
|
||||
v-if="currentUserHasPermission(['system:plugins:manage'])"
|
||||
v-model:visible="pluginInstall"
|
||||
@close="handleFetchPlugins()"
|
||||
@close="refetch()"
|
||||
/>
|
||||
|
||||
<VPageHeader title="插件">
|
||||
|
@ -302,11 +265,11 @@ function handleClearFilters() {
|
|||
<div class="flex flex-row gap-2">
|
||||
<div
|
||||
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
||||
@click="handleFetchPlugins()"
|
||||
@click="refetch()"
|
||||
>
|
||||
<IconRefreshLine
|
||||
v-tooltip="`刷新`"
|
||||
:class="{ 'animate-spin text-gray-900': loading }"
|
||||
:class="{ 'animate-spin text-gray-900': isFetching }"
|
||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
|
@ -317,16 +280,16 @@ function handleClearFilters() {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<VLoading v-if="loading" />
|
||||
<VLoading v-if="isLoading" />
|
||||
|
||||
<Transition v-else-if="!plugins.total" appear name="fade">
|
||||
<Transition v-else-if="!data?.length" appear name="fade">
|
||||
<VEmpty
|
||||
message="当前没有已安装的插件,你可以尝试刷新或者安装新插件"
|
||||
title="当前没有已安装的插件"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="handleFetchPlugins()">刷新</VButton>
|
||||
<VButton :loading="isFetching" @click="refetch()">刷新</VButton>
|
||||
<VButton
|
||||
v-permission="['system:plugins:manage']"
|
||||
type="secondary"
|
||||
|
@ -347,8 +310,8 @@ function handleClearFilters() {
|
|||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
role="list"
|
||||
>
|
||||
<li v-for="(plugin, index) in plugins.items" :key="index">
|
||||
<PluginListItem :plugin="plugin" @reload="handleFetchPlugins()" />
|
||||
<li v-for="(plugin, index) in data" :key="index">
|
||||
<PluginListItem :plugin="plugin" @reload="refetch()" />
|
||||
</li>
|
||||
</ul>
|
||||
</Transition>
|
||||
|
@ -356,11 +319,10 @@ function handleClearFilters() {
|
|||
<template #footer>
|
||||
<div class="bg-white sm:flex sm:items-center sm:justify-end">
|
||||
<VPagination
|
||||
:page="plugins.page"
|
||||
:size="plugins.size"
|
||||
:total="plugins.total"
|
||||
:size-options="[20, 30, 50, 100]"
|
||||
@change="handlePaginationChange"
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:total="total"
|
||||
:size-options="[10, 20, 30, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue