Browse Source

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
commit
816feb9937
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      package.json
  2. 49
      pnpm-lock.yaml
  3. 2
      src/main.ts
  4. 27
      src/modules/system/plugins/PluginDetail.vue
  5. 164
      src/modules/system/plugins/PluginList.vue

1
package.json

@ -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",

49
pnpm-lock.yaml

@ -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:

2
src/main.ts

@ -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) {

27
src/modules/system/plugins/PluginDetail.vue

@ -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>

164
src/modules/system/plugins/PluginList.vue

@ -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…
Cancel
Save