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
Ryan Wang 2 years ago committed by GitHub
parent c03ea64bf2
commit 816feb9937
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -45,6 +45,7 @@
"@halo-dev/components": "workspace:*", "@halo-dev/components": "workspace:*",
"@halo-dev/console-shared": "workspace:*", "@halo-dev/console-shared": "workspace:*",
"@halo-dev/richtext-editor": "0.0.0-alpha.19", "@halo-dev/richtext-editor": "0.0.0-alpha.19",
"@tanstack/vue-query": "^4.24.10",
"@tiptap/extension-character-count": "^2.0.0-beta.202", "@tiptap/extension-character-count": "^2.0.0-beta.202",
"@uppy/core": "^3.0.4", "@uppy/core": "^3.0.4",
"@uppy/dashboard": "^3.2.0", "@uppy/dashboard": "^3.2.0",

@ -22,6 +22,7 @@ importers:
'@rushstack/eslint-patch': ^1.2.0 '@rushstack/eslint-patch': ^1.2.0
'@tailwindcss/aspect-ratio': ^0.4.2 '@tailwindcss/aspect-ratio': ^0.4.2
'@tailwindcss/container-queries': ^0.1.0 '@tailwindcss/container-queries': ^0.1.0
'@tanstack/vue-query': ^4.24.10
'@tiptap/extension-character-count': ^2.0.0-beta.202 '@tiptap/extension-character-count': ^2.0.0-beta.202
'@types/jsdom': ^20.0.1 '@types/jsdom': ^20.0.1
'@types/lodash.clonedeep': 4.5.7 '@types/lodash.clonedeep': 4.5.7
@ -115,6 +116,7 @@ importers:
'@halo-dev/components': link:packages/components '@halo-dev/components': link:packages/components
'@halo-dev/console-shared': link:packages/shared '@halo-dev/console-shared': link:packages/shared
'@halo-dev/richtext-editor': 0.0.0-alpha.19_oau5uimcdfyintci7kxkcutjea '@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 '@tiptap/extension-character-count': 2.0.0-beta.202_f4ffqkgz5d3wev7su7t7l2rrua
'@uppy/core': 3.0.4 '@uppy/core': 3.0.4
'@uppy/dashboard': 3.2.0_@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 tailwindcss: 3.2.4_postcss@8.4.19
dev: true 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: /@tiptap/core/2.0.0-beta.209_ev2av5mpakmaqws3kqtlepbpdy:
resolution: {integrity: sha512-DOOzfo2XKD5Qt2oEGW33/6ugwSnvpl4WbxtlKdPadLoApk6Kja3K1Eps3pihBgIGmo4tkctkCzmj8wNWS7KeWg==} resolution: {integrity: sha512-DOOzfo2XKD5Qt2oEGW33/6ugwSnvpl4WbxtlKdPadLoApk6Kja3K1Eps3pihBgIGmo4tkctkCzmj8wNWS7KeWg==}
peerDependencies: peerDependencies:
@ -4726,7 +4755,7 @@ packages:
/axios/0.21.4_debug@4.3.2: /axios/0.21.4_debug@4.3.2:
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
dependencies: dependencies:
follow-redirects: 1.15.2_debug@4.3.2 follow-redirects: 1.15.2
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
dev: true dev: true
@ -4742,7 +4771,7 @@ packages:
/axios/0.27.2: /axios/0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
dependencies: dependencies:
follow-redirects: 1.15.2_debug@4.3.2 follow-redirects: 1.15.2
form-data: 4.0.0 form-data: 4.0.0
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
@ -5549,6 +5578,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
dev: true
/debug/4.3.4: /debug/4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
@ -6870,6 +6900,15 @@ packages:
vue-resize: 2.0.0-alpha.1_vue@3.2.45 vue-resize: 2.0.0-alpha.1_vue@3.2.45
dev: false 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: /follow-redirects/1.15.2_debug@4.3.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@ -6880,6 +6919,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
debug: 4.3.2 debug: 4.3.2
dev: true
/foreground-child/2.0.0: /foreground-child/2.0.0:
resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==}
@ -8434,6 +8474,7 @@ packages:
/ms/2.1.2: /ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/ms/2.1.3: /ms/2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -9307,6 +9348,10 @@ packages:
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
dev: true dev: true
/remove-accents/0.4.2:
resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==}
dev: false
/request-progress/3.0.0: /request-progress/3.0.0:
resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==}
dependencies: dependencies:

@ -21,6 +21,7 @@ import { useSystemStatesStore } from "./stores/system-states";
import { useUserStore } from "./stores/user"; import { useUserStore } from "./stores/user";
import { useSystemConfigMapStore } from "./stores/system-configmap"; import { useSystemConfigMapStore } from "./stores/system-configmap";
import i18n from "./locales"; import i18n from "./locales";
import { VueQueryPlugin } from "@tanstack/vue-query";
const app = createApp(App); const app = createApp(App);
@ -28,6 +29,7 @@ setupComponents(app);
app.use(createPinia()); app.use(createPinia());
app.use(i18n); app.use(i18n);
app.use(VueQueryPlugin);
function registerModule(pluginModule: PluginModule, core: boolean) { function registerModule(pluginModule: PluginModule, core: boolean) {
if (pluginModule.components) { if (pluginModule.components) {

@ -1,13 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { VSwitch, VTag } from "@halo-dev/components"; import { VSwitch, VTag } from "@halo-dev/components";
import type { Ref } from "vue"; import type { Ref } from "vue";
import { computed, inject, ref, watchEffect } from "vue"; import { computed, inject } from "vue";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import type { Plugin, Role } from "@halo-dev/api-client"; import type { Plugin, Role } from "@halo-dev/api-client";
import { pluginLabels } from "@/constants/labels"; import { pluginLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import { usePluginLifeCycle } from "./composables/use-plugin"; import { usePluginLifeCycle } from "./composables/use-plugin";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { useQuery } from "@tanstack/vue-query";
const plugin = inject<Ref<Plugin | undefined>>("plugin"); const plugin = inject<Ref<Plugin | undefined>>("plugin");
const { changeStatus, isStarted } = usePluginLifeCycle(plugin); const { changeStatus, isStarted } = usePluginLifeCycle(plugin);
@ -17,24 +18,22 @@ interface RoleTemplateGroup {
roles: Role[]; roles: Role[];
} }
const pluginRoleTemplates = ref<Role[]>([]); const { data: pluginRoleTemplates } = useQuery({
queryKey: ["plugin-roles", plugin?.value?.metadata.name],
const handleFetchRoles = async () => { queryFn: async () => {
try {
const { data } = await apiClient.extension.role.listv1alpha1Role({ const { data } = await apiClient.extension.role.listv1alpha1Role({
page: 0, page: 0,
size: 0, size: 0,
labelSelector: [`${pluginLabels.NAME}=${plugin?.value?.metadata.name}`], labelSelector: [`${pluginLabels.NAME}=${plugin?.value?.metadata.name}`],
}); });
pluginRoleTemplates.value = data.items;
} catch (e) { return data.items;
console.error(e); },
} });
};
const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => { const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
const groups: RoleTemplateGroup[] = []; const groups: RoleTemplateGroup[] = [];
pluginRoleTemplates.value.forEach((role) => { pluginRoleTemplates.value?.forEach((role) => {
const group = groups.find( const group = groups.find(
(group) => (group) =>
group.module === role.metadata.annotations?.[rbacAnnotations.MODULE] group.module === role.metadata.annotations?.[rbacAnnotations.MODULE]
@ -50,12 +49,6 @@ const pluginRoleTemplateGroups = computed<RoleTemplateGroup[]>(() => {
}); });
return groups; return groups;
}); });
watchEffect(() => {
if (plugin?.value) {
handleFetchRoles();
}
});
</script> </script>
<template> <template>

@ -14,95 +14,15 @@ import {
} from "@halo-dev/components"; } from "@halo-dev/components";
import PluginListItem from "./components/PluginListItem.vue"; import PluginListItem from "./components/PluginListItem.vue";
import PluginUploadModal from "./components/PluginUploadModal.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 { apiClient } from "@/utils/api-client";
import type { PluginList } from "@halo-dev/api-client";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import { onBeforeRouteLeave } from "vue-router";
import FilterTag from "@/components/filter/FilterTag.vue"; import FilterTag from "@/components/filter/FilterTag.vue";
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue"; import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core"; 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 { interface EnabledItem {
label: string; label: string;
value?: boolean; value?: boolean;
@ -113,6 +33,16 @@ interface SortItem {
value: string; 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[] = [ const EnabledItems: EnabledItem[] = [
{ {
label: "全部", label: "全部",
@ -144,12 +74,12 @@ const selectedSortItem = ref<SortItem>();
function handleEnabledItemChange(enabledItem: EnabledItem) { function handleEnabledItemChange(enabledItem: EnabledItem) {
selectedEnabledItem.value = enabledItem; selectedEnabledItem.value = enabledItem;
handleFetchPlugins({ page: 1 }); page.value = 1;
} }
function handleSortItemChange(sortItem?: SortItem) { function handleSortItemChange(sortItem?: SortItem) {
selectedSortItem.value = sortItem; selectedSortItem.value = sortItem;
handleFetchPlugins({ page: 1 }); page.value = 1;
} }
function handleKeywordChange() { function handleKeywordChange() {
@ -157,12 +87,12 @@ function handleKeywordChange() {
if (keywordNode) { if (keywordNode) {
keyword.value = keywordNode._value as string; keyword.value = keywordNode._value as string;
} }
handleFetchPlugins({ page: 1 }); page.value = 1;
} }
function handleClearKeyword() { function handleClearKeyword() {
keyword.value = ""; keyword.value = "";
handleFetchPlugins({ page: 1 }); page.value = 1;
} }
const hasFilters = computed(() => { const hasFilters = computed(() => {
@ -177,14 +107,47 @@ function handleClearFilters() {
selectedEnabledItem.value = undefined; selectedEnabledItem.value = undefined;
selectedSortItem.value = undefined; selectedSortItem.value = undefined;
keyword.value = ""; 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> </script>
<template> <template>
<PluginUploadModal <PluginUploadModal
v-if="currentUserHasPermission(['system:plugins:manage'])" v-if="currentUserHasPermission(['system:plugins:manage'])"
v-model:visible="pluginInstall" v-model:visible="pluginInstall"
@close="handleFetchPlugins()" @close="refetch()"
/> />
<VPageHeader title="插件"> <VPageHeader title="插件">
@ -302,11 +265,11 @@ function handleClearFilters() {
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<div <div
class="group cursor-pointer rounded p-1 hover:bg-gray-200" class="group cursor-pointer rounded p-1 hover:bg-gray-200"
@click="handleFetchPlugins()" @click="refetch()"
> >
<IconRefreshLine <IconRefreshLine
v-tooltip="`刷新`" 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" class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
/> />
</div> </div>
@ -317,16 +280,16 @@ function handleClearFilters() {
</div> </div>
</template> </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 <VEmpty
message="当前没有已安装的插件,你可以尝试刷新或者安装新插件" message="当前没有已安装的插件,你可以尝试刷新或者安装新插件"
title="当前没有已安装的插件" title="当前没有已安装的插件"
> >
<template #actions> <template #actions>
<VSpace> <VSpace>
<VButton @click="handleFetchPlugins()"></VButton> <VButton :loading="isFetching" @click="refetch()"></VButton>
<VButton <VButton
v-permission="['system:plugins:manage']" v-permission="['system:plugins:manage']"
type="secondary" type="secondary"
@ -347,8 +310,8 @@ function handleClearFilters() {
class="box-border h-full w-full divide-y divide-gray-100" class="box-border h-full w-full divide-y divide-gray-100"
role="list" role="list"
> >
<li v-for="(plugin, index) in plugins.items" :key="index"> <li v-for="(plugin, index) in data" :key="index">
<PluginListItem :plugin="plugin" @reload="handleFetchPlugins()" /> <PluginListItem :plugin="plugin" @reload="refetch()" />
</li> </li>
</ul> </ul>
</Transition> </Transition>
@ -356,11 +319,10 @@ function handleClearFilters() {
<template #footer> <template #footer>
<div class="bg-white sm:flex sm:items-center sm:justify-end"> <div class="bg-white sm:flex sm:items-center sm:justify-end">
<VPagination <VPagination
:page="plugins.page" v-model:page="page"
:size="plugins.size" v-model:size="size"
:total="plugins.total" :total="total"
:size-options="[20, 30, 50, 100]" :size-options="[10, 20, 30, 50, 100]"
@change="handlePaginationChange"
/> />
</div> </div>
</template> </template>

Loading…
Cancel
Save