- {showMismatch ? (
+ {mismatch === null ? null : (
- {t("mismatch", {
- api: api?.version ?? "",
- frontend: frontend?.version ?? "",
- })}
+ {t("mismatch", mismatch)}
- ) : null}
+ )}
{showStatus ? (
) : null}
@@ -341,7 +279,9 @@ export const PluginList = ({
{t("title")}
- {t("desc", { count: rows.length - 1 })}
+ {t("desc", {
+ count: rows.filter(row => row.kind === "plugin").length,
+ })}
}>
diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-rows.ts b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-rows.ts
index f0f77de7db..95652f79fd 100644
--- a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-rows.ts
+++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-rows.ts
@@ -1,9 +1,31 @@
-import type { PluginRow } from "./plugin-list";
-import type { AdminPluginUpdates } from "./plugin-updates-query";
-import type { AdminInstalledPlugins } from "./plugins-query";
+import type { PluginTone } from "./plugin-tone";
+import type {
+ AdminComponentUpdate,
+ AdminPluginUpdates,
+} from "./plugin-updates-query";
+import type {
+ AdminInstalledPlugin,
+ AdminInstalledPlugins,
+ AdminPluginAuthor,
+} from "./plugins-query";
import { pluginToneOf } from "./plugin-tone";
+export interface PluginRow {
+ author: AdminPluginAuthor | null;
+ description: null | string;
+ icon: null | string;
+ id: string;
+ kind: "core" | "plugin";
+ license: null | string;
+ registrations: AdminInstalledPlugin["registrations"] | null;
+ title: string;
+ tone: null | PluginTone;
+ update: AdminComponentUpdate | undefined;
+ version: null | string;
+ versionMismatch: boolean;
+}
+
export const pluginRowsOf = ({
core,
data,
diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-tone.ts b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-tone.ts
index bd9857a47e..311f6aa123 100644
--- a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-tone.ts
+++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-tone.ts
@@ -1,17 +1,64 @@
+import type { LucideIcon } from "lucide-react";
+
+import {
+ ArrowUpCircleIcon,
+ CircleCheckIcon,
+ CircleDashedIcon,
+ CircleHelpIcon,
+ CircleOffIcon,
+ ShieldAlertIcon,
+ TriangleAlertIcon,
+} from "lucide-react";
+
import type { AdminComponentUpdate } from "./plugin-updates-query";
-export const PLUGIN_TONES = [
- "security",
- "error",
- "update",
- "unknown",
- "pending",
- "ok",
- "off",
- "disabled",
-] as const;
+export type PluginTone =
+ | "disabled"
+ | "error"
+ | "off"
+ | "ok"
+ | "pending"
+ | "security"
+ | "unknown"
+ | "update";
-export type PluginTone = (typeof PLUGIN_TONES)[number];
+export const PLUGIN_TONE_STYLE: Record<
+ PluginTone,
+ { className: string; icon: LucideIcon }
+> = {
+ security: {
+ icon: ShieldAlertIcon,
+ className: "bg-destructive/10 text-destructive dark:bg-destructive/20",
+ },
+ error: {
+ icon: TriangleAlertIcon,
+ className: "bg-warn/10 text-warn dark:bg-warn/20",
+ },
+ update: {
+ icon: ArrowUpCircleIcon,
+ className: "bg-primary/10 text-primary dark:bg-primary/20",
+ },
+ unknown: {
+ icon: CircleHelpIcon,
+ className: "bg-muted text-muted-foreground",
+ },
+ pending: {
+ icon: CircleDashedIcon,
+ className: "bg-muted text-muted-foreground",
+ },
+ ok: {
+ icon: CircleCheckIcon,
+ className: "bg-success/10 text-success dark:bg-success/20",
+ },
+ off: {
+ icon: CircleOffIcon,
+ className: "bg-muted text-muted-foreground",
+ },
+ disabled: {
+ icon: CircleOffIcon,
+ className: "bg-muted text-muted-foreground",
+ },
+};
export const pluginToneOf = (
update: AdminComponentUpdate | undefined,
diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-update-status.test.tsx b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-update-status.test.tsx
index fcd569c18f..2d7506b628 100644
--- a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-update-status.test.tsx
+++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-update-status.test.tsx
@@ -146,7 +146,6 @@ describe("PluginUpdateStatus", () => {
expect(screen.getByText(`${T}.status.update_available`)).toBeTruthy();
expect(screen.getByText(`${T}.failed_with_previous`)).toBeTruthy();
- expect(screen.queryByText(`${T}.checked_at`)).toBeNull();
});
it("reports a check that failed before anything was found", () => {
@@ -167,13 +166,6 @@ describe("PluginUpdateStatus", () => {
renderStatus(update({ stale: true }));
expect(screen.getByText(`${T}.stale`)).toBeTruthy();
- expect(screen.queryByText(`${T}.checked_at`)).toBeNull();
- });
-
- it("says when the last check is current", () => {
- renderStatus(update());
-
- expect(screen.getByText(`${T}.checked_at`)).toBeTruthy();
});
it.each([
diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-update-status.tsx b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-update-status.tsx
index fe48bdb158..591d3abf06 100644
--- a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-update-status.tsx
+++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-update-status.tsx
@@ -201,7 +201,7 @@ const ResultLines = ({
);
case "update_available": {
- const latest = result.latest;
+ const { alternative, latest } = result;
if (latest === null) return null;
return (
@@ -246,22 +246,22 @@ const ResultLines = ({
{t("updates.compatibility.unknown")}
) : null}
- {result.alternative === null ? null : (
+ {alternative === null ? null : (
{t.rich("updates.compatibility.alternative", {
version: () => (
),
})}
- {safeHttpsHref(result.alternative.url) === null ? null : (
+ {safeHttpsHref(alternative.url) === null ? null : (
<>
·
-
+
{t("updates.release_notes")}
>
@@ -288,13 +288,10 @@ const ResultLines = ({
export const PluginUpdateStatus = ({
coreVersion,
enabled,
- showCheckedAt = true,
update,
}: {
coreVersion: string;
enabled: boolean;
- /** Off where the same date is already shown nearby. */
- showCheckedAt?: boolean;
update: AdminComponentUpdate | undefined;
}) => {
const t = useTranslations("admin.system.plugins");
@@ -345,18 +342,6 @@ export const PluginUpdateStatus = ({
: t("updates.failed", { reason })}
)}
-
- {showCheckedAt &&
- update.state === "checked" &&
- !update.stale &&
- lastError === null &&
- update.lastSuccessAt !== null ? (
-
- {t.rich("updates.checked_at", {
- date: () => ,
- })}
-
- ) : null}
);
};
diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-updates-query.ts b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-updates-query.ts
index 62e5ff436d..1e741ef891 100644
--- a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-updates-query.ts
+++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-updates-query.ts
@@ -55,7 +55,7 @@ export type CheckPluginUpdatesResult =
}
| {
retryAfterSeconds: null | number;
- status: "cooldown" | "disabled" | "running";
+ status: "cooldown" | "disabled" | "interrupted" | "running";
}
| { status: "error" };
diff --git a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-version.tsx b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-version.tsx
index 73d1645673..0f240226c1 100644
--- a/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-version.tsx
+++ b/packages/vitnode/src/views/admin/views/core/system/plugins/plugin-version.tsx
@@ -6,7 +6,7 @@ import { Badge } from "@/components/ui/badge";
import { unstableChannelLabel, unstableChannelOf } from "./unstable-channel";
-export const UnstableChannelTag = ({ version }: { version: null | string }) => {
+const UnstableChannelTag = ({ version }: { version: string }) => {
const t = useTranslations("admin.system.plugins.unstable");
const channel = unstableChannelOf(version);
if (channel === null) return null;
@@ -15,7 +15,7 @@ export const UnstableChannelTag = ({ version }: { version: null | string }) => {
return (