From 9073a97eece7edca1c0c2a176c8a617ca0efe822 Mon Sep 17 00:00:00 2001 From: ReaJason Date: Sat, 1 Mar 2025 23:08:29 +0800 Subject: [PATCH] feat(ui): support show update button --- .../javaweb/boot/config/WebConfig.java | 21 +++++ .../boot/controller/VersionController.java | 65 ++++++++++++++- .../javaweb/boot/entity/VersionInfo.java | 15 ++++ web/src/components/version-badge.tsx | 79 ++++++++++++++++--- web/src/i18n/en.json | 4 + web/src/i18n/zh-CN.json | 4 + 6 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 boot/src/main/java/com/reajason/javaweb/boot/config/WebConfig.java create mode 100644 boot/src/main/java/com/reajason/javaweb/boot/entity/VersionInfo.java diff --git a/boot/src/main/java/com/reajason/javaweb/boot/config/WebConfig.java b/boot/src/main/java/com/reajason/javaweb/boot/config/WebConfig.java new file mode 100644 index 00000000..515f80e7 --- /dev/null +++ b/boot/src/main/java/com/reajason/javaweb/boot/config/WebConfig.java @@ -0,0 +1,21 @@ +package com.reajason.javaweb.boot.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +/** + * @author ReaJason + */ +@Configuration +public class WebConfig { + + @Bean + public RestTemplate restTemplate() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(3000); + factory.setReadTimeout(3000); + return new RestTemplate(factory); + } +} \ No newline at end of file diff --git a/boot/src/main/java/com/reajason/javaweb/boot/controller/VersionController.java b/boot/src/main/java/com/reajason/javaweb/boot/controller/VersionController.java index e3b6214b..d6827921 100644 --- a/boot/src/main/java/com/reajason/javaweb/boot/controller/VersionController.java +++ b/boot/src/main/java/com/reajason/javaweb/boot/controller/VersionController.java @@ -1,10 +1,20 @@ package com.reajason.javaweb.boot.controller; +import com.reajason.javaweb.boot.entity.VersionInfo; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import org.thymeleaf.util.StringUtils; + +import java.util.List; +import java.util.Map; /** * @author ReaJason @@ -16,10 +26,61 @@ import org.springframework.web.bind.annotation.RestController; public class VersionController { @Value("${spring.application.version}") - String version; + private String version; + + private final RestTemplate restTemplate; + + public VersionController(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } @GetMapping - public String version() { + public VersionInfo version() { + String latestVersion = getLatestGithubRelease(); + return VersionInfo.builder() + .currentVersion(version) + .latestVersion(latestVersion) + .hasUpdate(!StringUtils.equals(version, latestVersion)) + .build(); + } + + private String getLatestGithubRelease() { + try { + String latestVersion = tryFetchRelease("https://api.github.com"); + if (latestVersion != null) { + return latestVersion; + } + latestVersion = tryFetchRelease("https://gh.llkk.cc/https://api.github.com"); + if (latestVersion != null) { + return latestVersion; + } + } catch (Exception ignored) { + } return version; } + + private String tryFetchRelease(String baseUrl) { + String apiUrl = String.format("%s/repos/%s/%s/releases", baseUrl, "ReaJason", "MemShellParty"); + + ResponseEntity>> response = restTemplate.exchange( + apiUrl, + HttpMethod.GET, + null, + new ParameterizedTypeReference<>() { + } + ); + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + List> body = response.getBody(); + for (Map map : body) { + String targetCommitish = (String) map.get("target_commitish"); + Boolean prerelease = (Boolean) map.get("prerelease"); + Boolean draft = (Boolean) map.get("draft"); + if ("master".equals(targetCommitish) && !prerelease && !draft) { + String tagName = (String) map.get("name"); + return tagName.startsWith("v") ? tagName.substring(1) : tagName; + } + } + } + return null; + } } diff --git a/boot/src/main/java/com/reajason/javaweb/boot/entity/VersionInfo.java b/boot/src/main/java/com/reajason/javaweb/boot/entity/VersionInfo.java new file mode 100644 index 00000000..2262feae --- /dev/null +++ b/boot/src/main/java/com/reajason/javaweb/boot/entity/VersionInfo.java @@ -0,0 +1,15 @@ +package com.reajason.javaweb.boot.entity; + +import lombok.Data; +import lombok.Builder; + +/** + * @author ReaJason + */ +@Data +@Builder +public class VersionInfo { + private String currentVersion; + private String latestVersion; + private boolean hasUpdate; +} \ No newline at end of file diff --git a/web/src/components/version-badge.tsx b/web/src/components/version-badge.tsx index b597638e..86041f53 100644 --- a/web/src/components/version-badge.tsx +++ b/web/src/components/version-badge.tsx @@ -1,30 +1,89 @@ import { env } from "@/config"; import { useQuery } from "@tanstack/react-query"; -import { LoaderCircle } from "lucide-react"; +import { CircleX, LoaderCircle, RefreshCcw } from "lucide-react"; import type React from "react"; +import { useTranslation } from "react-i18next"; import { Button } from "./ui/button"; +import { TooltipContent, TooltipTrigger } from "./ui/tooltip"; +import { Tooltip } from "./ui/tooltip"; +import { TooltipProvider } from "./ui/tooltip"; +type VersionInfo = { + currentVersion: string; + latestVersion: string; + hasUpdate: boolean; +}; const VersionBadge: React.FC = () => { - const { isPending, data } = useQuery({ + const { isPending, data, isError } = useQuery({ queryKey: ["version"], queryFn: async () => { const response = await fetch(`${env.API_URL}/version`); if (response.ok) { - return await response.text(); + return await response.json(); } return "unknown"; }, }); + const inProduction = env.MODE === "production"; + const { t } = useTranslation(); return (
- + {isPending && ( + + )} + {isError && ( + + )} + {data?.hasUpdate && inProduction && ( + + + + + + +

+ {t("version.updateAvailableTooltip", { + currentVersion: data.currentVersion, + latestVersion: data.latestVersion, + })} +

+
+
+
+ )} + {data && (!data.hasUpdate || !inProduction) && ( + + )}
); }; diff --git a/web/src/i18n/en.json b/web/src/i18n/en.json index 0919bea2..3e86d6d0 100644 --- a/web/src/i18n/en.json +++ b/web/src/i18n/en.json @@ -129,5 +129,9 @@ "targetServerRequest": "Request", "try-to-use-shell": "Try to use the memory shell", "waitingForGeneration": "// Waiting for generation..." + }, + "version": { + "updateAvailable": "Update Available", + "updateAvailableTooltip": "Click to Open Github Release Page ( v{{currentVersion}} -> v{{latestVersion}})" } } diff --git a/web/src/i18n/zh-CN.json b/web/src/i18n/zh-CN.json index aae688c8..644560b8 100644 --- a/web/src/i18n/zh-CN.json +++ b/web/src/i18n/zh-CN.json @@ -129,5 +129,9 @@ "targetServerRequest": "请求适配", "try-to-use-shell": "尝试利用内存马", "waitingForGeneration": "// 等待填写参数生成中..." + }, + "version": { + "updateAvailable": "有可用升级", + "updateAvailableTooltip": "点击前往 GitHub Release ( v{{currentVersion}} -> v{{latestVersion}})" } }