feat: add version check and alert for updates in MainLayout and BuildInfoModal

- Integrated semver to compare current and latest versions.
- Added an alert in BuildInfoModal to notify users of available updates.
- Updated MainLayout to manage version state and display indicators for new versions.
This commit is contained in:
kastov 2025-04-09 13:42:38 +03:00
parent 7a7cb03e7e
commit 79fa16176f
No known key found for this signature in database
GPG key ID: 1B27BE29057F4C90
6 changed files with 155 additions and 28 deletions

View file

@ -65,6 +65,14 @@ jobs:
}
EOF
- name: Generate changelog
id: changelog
run: |
CHANGELOG=$(npx changelogen --from=${{ steps.tag.outputs.previousTag }} --to=${{ steps.tag.outputs.latestTag }})
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Install dependencies and build
run: |
npm install
@ -88,13 +96,17 @@ jobs:
prerelease: false
name: ${{ github.ref_name }}
body: |
🎉 Automatic release of Remnawave Frontend, v${{ github.ref_name }}
🌊 Remnawave Frontend v${{ github.ref_name }}
This release was automatically created through GitHub Actions.
<p align="center">
<a href="https://t.me/remnawave" target="_blank" rel="noopener noreferrer">
<img src="https://img.shields.io/badge/Join%20community-Telegram-26A5E4?style=for-the-badge&logo=telegram&logoColor=white" alt="Join community on Telegram" width="220" height="auto">
</a>
</p>
### 📝 Changes
📝 Compare changes: [${{ steps.tag.outputs.previousTag }}...${{ steps.tag.outputs.latestTag }}](https://github.com/${{ github.repository }}/compare/${{ steps.tag.outputs.previousTag }}...${{ steps.tag.outputs.latestTag }})
✏️ Compare: [${{ steps.tag.outputs.previousTag }}...${{ steps.tag.outputs.latestTag }}](https://github.com/${{ github.repository }}/compare/${{ steps.tag.outputs.previousTag }}...${{ steps.tag.outputs.latestTag }})
${{ env.CHANGELOG }}
### 📦 Artifacts
- remnawave-frontend.zip - archive with built frontend

4
changelog.config.json Normal file
View file

@ -0,0 +1,4 @@
{
"hideAuthorEmail": true,
"noAuthors": true
}

14
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "@remnawave/frontend",
"version": "1.5.4",
"version": "1.5.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@remnawave/frontend",
"version": "1.5.4",
"version": "1.5.5",
"license": "AGPL-3.0-only",
"dependencies": {
"@gfazioli/mantine-text-animate": "^1.0.2",
@ -59,6 +59,7 @@
"react-imask": "^7.6.1",
"react-router-dom": "6.27.0",
"recharts": "^2.15.1",
"semver": "^7.7.1",
"tiny-invariant": "^1.3.3",
"uqr": "^0.1.2",
"vite-plugin-deadfile": "^1.4.0",
@ -79,6 +80,7 @@
"@types/node": "^22.13.17",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.29.0",
"@typescript-eslint/parser": "^8.29.0",
"@vitejs/plugin-react": "^4.3.4",
@ -2996,6 +2998,13 @@
"@types/react": "*"
}
},
"node_modules/@types/semver": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
"integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@ -9814,7 +9823,6 @@
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"

View file

@ -2,7 +2,7 @@
"name": "@remnawave/frontend",
"private": false,
"type": "module",
"version": "1.5.4",
"version": "1.5.5",
"license": "AGPL-3.0-only",
"author": "REMNAWAVE <github.com/remnawave>",
"homepage": "https://github.com/remnawave",
@ -80,6 +80,7 @@
"react-imask": "^7.6.1",
"react-router-dom": "6.27.0",
"recharts": "^2.15.1",
"semver": "^7.7.1",
"tiny-invariant": "^1.3.3",
"uqr": "^0.1.2",
"vite-plugin-deadfile": "^1.4.0",
@ -100,6 +101,7 @@
"@types/node": "^22.13.17",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.29.0",
"@typescript-eslint/parser": "^8.29.0",
"@vitejs/plugin-react": "^4.3.4",

View file

@ -1,8 +1,19 @@
import { AppShell, Badge, Burger, Code, Container, Group, ScrollArea, Text } from '@mantine/core'
import {
AppShell,
Badge,
Burger,
Code,
Container,
Group,
Indicator,
ScrollArea,
Text
} from '@mantine/core'
import { useClickOutside, useDisclosure, useMediaQuery } from '@mantine/hooks'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { Outlet } from 'react-router-dom'
import semver from 'semver'
import axios from 'axios'
import { getBuildInfo } from '@shared/utils/get-build-info/get-build-info.util'
@ -21,6 +32,15 @@ export function MainLayout() {
const [buildInfoModalOpened, setBuildInfoModalOpened] = useState(false)
const [isMediaQueryReady, setIsMediaQueryReady] = useState(false)
const [versions, setVersions] = useState<{
currentVersion: string
latestVersion: string
}>({
currentVersion: '0.0.0',
latestVersion: '0.0.0'
})
const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false)
const buildInfo = getBuildInfo()
const isMobile = useMediaQuery(`(max-width: 64rem)`, undefined, {
@ -52,6 +72,31 @@ export function MainLayout() {
}
})
const { data: latestVersion } = useQuery({
queryKey: ['github-latest-version'],
staleTime: sToMs(3600),
refetchInterval: sToMs(3600),
queryFn: async () => {
const response = await axios.get<{
release: {
tag: string
}
}>('https://ungh.cc/repos/remnawave/panel/releases/latest')
return response.data.release.tag
}
})
useEffect(() => {
setVersions({
currentVersion: buildInfo.tag ?? '0.0.0',
latestVersion: latestVersion ?? '0.0.0'
})
}, [latestVersion, buildInfo.tag])
useEffect(() => {
setIsNewVersionAvailable(semver.gt(versions.latestVersion, versions.currentVersion))
}, [versions])
return isMediaQueryReady ? (
<AppShell
header={{ height: 64 }}
@ -116,27 +161,41 @@ export function MainLayout() {
</Text>
</Group>
{buildInfo.branch === 'dev' && (
<Badge
color="red"
onClick={() => setBuildInfoModalOpened(true)}
radius="sm"
size="lg"
style={{ cursor: 'help', marginLeft: 'auto' }}
variant="light"
<Indicator
color="cyan"
disabled={!isNewVersionAvailable}
processing
size={11}
>
dev
</Badge>
<Badge
color="red"
onClick={() => setBuildInfoModalOpened(true)}
radius="sm"
size="lg"
style={{ cursor: 'help', marginLeft: 'auto' }}
variant="light"
>
dev
</Badge>
</Indicator>
)}
{buildInfo.branch !== 'dev' && (
<Code
c="cyan"
fw={700}
onClick={() => setBuildInfoModalOpened(true)}
style={{ cursor: 'pointer', marginLeft: 'auto' }}
<Indicator
color="cyan"
disabled={!isNewVersionAvailable}
processing
size={11}
>
{`v${packageJson.version}`}
</Code>
<Code
c="cyan"
fw={700}
onClick={() => setBuildInfoModalOpened(true)}
style={{ cursor: 'pointer', marginLeft: 'auto' }}
>
{`v${packageJson.version}`}
</Code>
</Indicator>
)}
{isSocialButton && (
@ -175,6 +234,7 @@ export function MainLayout() {
<BuildInfoModal
buildInfo={buildInfo}
isNewVersionAvailable={isNewVersionAvailable}
onClose={() => setBuildInfoModalOpened(false)}
opened={buildInfoModalOpened}
/>

View file

@ -5,20 +5,39 @@ import {
TbCheck as IconCheck,
TbCopy as IconCopy,
TbGitBranch as IconGitBranch,
TbHash as IconHash
TbHash as IconHash,
TbRipple
} from 'react-icons/tb'
import { Badge, Box, Button, Code, Group, Modal, Stack, Text, Title, Tooltip } from '@mantine/core'
import {
Alert,
Badge,
Box,
Button,
Code,
Group,
Modal,
Stack,
Text,
Title,
Tooltip
} from '@mantine/core'
import { useClipboard } from '@mantine/hooks'
import { IBuildInfo } from '@shared/utils/get-build-info/interfaces/build-info.interface'
interface BuildInfoModalProps {
buildInfo: IBuildInfo
isNewVersionAvailable: boolean
onClose: () => void
opened: boolean
}
export function BuildInfoModal({ opened, onClose, buildInfo }: BuildInfoModalProps) {
export function BuildInfoModal({
opened,
onClose,
buildInfo,
isNewVersionAvailable
}: BuildInfoModalProps) {
const buildDate = new Date(buildInfo.buildTime).toLocaleString()
const clipboard = useClipboard({ timeout: 1000 })
@ -54,6 +73,28 @@ export function BuildInfoModal({ opened, onClose, buildInfo }: BuildInfoModalPro
withCloseButton
>
<Stack gap="md">
{isNewVersionAvailable && (
<Alert
color="teal"
icon={<TbRipple size={22} />}
title="Update available"
variant="outline"
>
<Text size="sm">A new version of Remnawave is available.</Text>
<Button
color="cyan"
component="a"
href={'https://github.com/remnawave/panel/releases/latest'}
mt="xs"
size="xs"
target="_blank"
variant="outline"
>
Check out
</Button>
</Alert>
)}
<Group align="flex-start">
<IconCalendar size={20} />
<Box>