feat: show the startup status of the plugin (#5520)

#### What type of PR is this?

/area ui
/kind feature
/milestone 2.14.x

#### What this PR does / why we need it:

Show the startup status of the plugin

<img width="444" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/0f5d47c2-9d81-4cb7-b114-6b2eebe753c5">


#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/5254

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

```release-note
插件支持显示启动中的状态。
```
pull/5787/head
Ryan Wang 2024-04-25 16:21:13 +08:00 committed by GitHub
parent a635881d34
commit 5a0f735efb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 135 additions and 83 deletions

View File

@ -20,7 +20,7 @@ import { computed, onMounted, provide, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission";
import { useQuery } from "@tanstack/vue-query";
import type { Plugin } from "@halo-dev/api-client";
import { PluginStatusPhaseEnum, type Plugin } from "@halo-dev/api-client";
import { useI18n } from "vue-i18n";
import { useRouteQuery } from "@vueuse/router";
import { usePluginBatchOperations } from "./composables/use-plugin";
@ -63,11 +63,26 @@ const { data, isLoading, isFetching, refetch } = useQuery<Plugin[]>({
},
keepPreviousData: true,
refetchInterval: (data) => {
const deletingPlugins = data?.filter(
const hasDeletingData = data?.some(
(plugin) => !!plugin.metadata.deletionTimestamp
);
return deletingPlugins?.length ? 2000 : false;
if (hasDeletingData) {
return 1000;
}
const hasStartingData = data?.some(
(plugin) =>
plugin.spec.enabled &&
plugin.status?.phase !==
(PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)
);
if (hasStartingData) {
return 3000;
}
return false;
},
});

View File

@ -10,7 +10,7 @@ import {
import type { Ref } from "vue";
import { computed, inject, markRaw, ref, toRefs } from "vue";
import { usePluginLifeCycle } from "../composables/use-plugin";
import type { Plugin } from "@halo-dev/api-client";
import { type Plugin, PluginStatusPhaseEnum } from "@halo-dev/api-client";
import { formatDatetime } from "@/utils/date";
import { usePermission } from "@/utils/permission";
import { apiClient } from "@/utils/api-client";
@ -43,7 +43,7 @@ const { plugin } = toRefs(props);
const selectedNames = inject<Ref<string[]>>("selectedNames", ref([]));
const { getFailedMessage, uninstall } = usePluginLifeCycle(plugin);
const { getStatusMessage, uninstall } = usePluginLifeCycle(plugin);
const pluginUpgradeModalVisible = ref(false);
@ -146,86 +146,105 @@ const { operationItems } = useOperationItemExtensionPoint<Plugin>(
const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
"plugin:list-item:field:create",
plugin,
computed((): EntityFieldItem[] => [
{
position: "start",
priority: 10,
component: markRaw(LogoField),
props: {
plugin: props.plugin,
},
},
{
position: "start",
priority: 20,
component: markRaw(VEntityField),
props: {
title: props.plugin.spec.displayName,
description: props.plugin.spec.description,
route: {
name: "PluginDetail",
params: { name: props.plugin.metadata.name },
computed((): EntityFieldItem[] => {
const { enabled } = props.plugin.spec || {};
const { phase } = props.plugin.status || {};
const shouldHideStatusDot =
!enabled || (enabled && phase === PluginStatusPhaseEnum.Started);
const getStatusDotState = () => {
if (
enabled &&
phase !==
(PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)
) {
return "default";
}
return "error";
};
return [
{
position: "start",
priority: 10,
component: markRaw(LogoField),
props: {
plugin: props.plugin,
},
},
},
{
position: "end",
priority: 10,
component: markRaw(StatusDotField),
props: {
tooltip: getFailedMessage(),
state: "error",
animate: true,
{
position: "start",
priority: 20,
component: markRaw(VEntityField),
props: {
title: props.plugin.spec.displayName,
description: props.plugin.spec.description,
route: {
name: "PluginDetail",
params: { name: props.plugin.metadata.name },
},
},
},
hidden: props.plugin.status?.phase !== "FAILED",
},
{
position: "end",
priority: 20,
component: markRaw(StatusDotField),
props: {
tooltip: t("core.common.status.deleting"),
state: "warning",
animate: true,
{
position: "end",
priority: 10,
component: markRaw(StatusDotField),
props: {
tooltip: getStatusMessage(),
state: getStatusDotState(),
animate: true,
},
hidden: shouldHideStatusDot,
},
hidden: !props.plugin.metadata.deletionTimestamp,
},
{
position: "end",
priority: 30,
component: markRaw(AuthorField),
props: {
plugin: props.plugin,
{
position: "end",
priority: 20,
component: markRaw(StatusDotField),
props: {
tooltip: t("core.common.status.deleting"),
state: "warning",
animate: true,
},
hidden: !props.plugin.metadata.deletionTimestamp,
},
hidden: !props.plugin.spec.author,
},
{
position: "end",
priority: 40,
component: markRaw(VEntityField),
props: {
description: props.plugin.spec.version,
{
position: "end",
priority: 30,
component: markRaw(AuthorField),
props: {
plugin: props.plugin,
},
hidden: !props.plugin.spec.author,
},
},
{
position: "end",
priority: 50,
component: markRaw(VEntityField),
props: {
description: formatDatetime(props.plugin.metadata.creationTimestamp),
{
position: "end",
priority: 40,
component: markRaw(VEntityField),
props: {
description: props.plugin.spec.version,
},
},
hidden: !props.plugin.metadata.creationTimestamp,
},
{
position: "end",
priority: 60,
component: markRaw(SwitchField),
props: {
plugin: props.plugin,
{
position: "end",
priority: 50,
component: markRaw(VEntityField),
props: {
description: formatDatetime(props.plugin.metadata.creationTimestamp),
},
hidden: !props.plugin.metadata.creationTimestamp,
},
permissions: ["system:plugins:manage"],
},
])
{
position: "end",
priority: 60,
component: markRaw(SwitchField),
props: {
plugin: props.plugin,
},
permissions: ["system:plugins:manage"],
},
];
})
);
</script>
<template>

View File

@ -1,6 +1,6 @@
import type { ComputedRef, Ref } from "vue";
import { computed } from "vue";
import { type Plugin } from "@halo-dev/api-client";
import { type Plugin, PluginStatusPhaseEnum } from "@halo-dev/api-client";
import { cloneDeep } from "lodash-es";
import { apiClient } from "@/utils/api-client";
import { Dialog, Toast } from "@halo-dev/components";
@ -9,7 +9,7 @@ import { useMutation } from "@tanstack/vue-query";
interface usePluginLifeCycleReturn {
isStarted: ComputedRef<boolean | undefined>;
getFailedMessage: () => string | undefined;
getStatusMessage: () => string | undefined;
changeStatus: () => void;
changingStatus: Ref<boolean>;
uninstall: (deleteExtensions?: boolean) => void;
@ -22,18 +22,33 @@ export function usePluginLifeCycle(
const isStarted = computed(() => {
return (
plugin?.value?.status?.phase === "STARTED" && plugin.value?.spec.enabled
plugin?.value?.status?.phase === PluginStatusPhaseEnum.Started &&
plugin.value?.spec.enabled
);
});
const getFailedMessage = () => {
const getStatusMessage = () => {
if (!plugin?.value) return;
const { enabled } = plugin.value.spec || {};
const { phase } = plugin.value.status || {};
// Starting up
if (
enabled &&
phase !== (PluginStatusPhaseEnum.Started || PluginStatusPhaseEnum.Failed)
) {
return t("core.common.status.starting_up");
}
// Starting failed
if (!isStarted.value) {
const lastCondition = plugin.value.status?.conditions?.[0];
return (
[lastCondition?.reason, lastCondition?.message].join(":") || "Unknown"
[lastCondition?.reason, lastCondition?.message]
.filter(Boolean)
.join(":") || "Unknown"
);
}
};
@ -138,7 +153,7 @@ export function usePluginLifeCycle(
return {
isStarted,
getFailedMessage,
getStatusMessage,
changeStatus,
changingStatus,
uninstall,

View File

@ -1607,6 +1607,7 @@ core:
not_activated: Not activated
installed: Installed
not_installed: Not installed
starting_up: Starting
text:
none: None
tip: Tip

View File

@ -1551,6 +1551,7 @@ core:
not_activated: 未启用
installed: 已安装
not_installed: 未安装
starting_up: 启动中
text:
none:
tip: 提示

View File

@ -1509,6 +1509,7 @@ core:
not_activated: 未啟用
installed: 已安裝
not_installed: 未安裝
starting_up: 启动中
text:
none:
tip: 提示