Skip to content

Commit 8f98317

Browse files
committed
feat(versioning): implement version check and update notifications with UI components for version updates
1 parent 0c37523 commit 8f98317

File tree

12 files changed

+663
-45
lines changed

12 files changed

+663
-45
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,5 +1865,18 @@
18651865
"pending": "In Progress",
18661866
"githubStarsUnit": "stars",
18671867
"githubProgress": "Star progress"
1868+
},
1869+
"version": {
1870+
"newVersionAvailable": "New version available!",
1871+
"currentVersion": "Current",
1872+
"latestVersion": "Latest",
1873+
"clickToUpdate": "A new version is available for download",
1874+
"upToDate": "Up to date",
1875+
"runningLatest": "Running {{version}}",
1876+
"updateBanner": "Update available: {{current}} → {{latest}}",
1877+
"updateBannerMobile": "Update: {{latest}}",
1878+
"updateCommandLabel": "Connect via SSH and run:",
1879+
"closeBanner": "Close update notification",
1880+
"needsUpdate": "Needs update"
18681881
}
18691882
}

dashboard/public/statics/locales/fa.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,5 +1837,18 @@
18371837
"pending": "در حال انجام",
18381838
"githubStarsUnit": "ستاره",
18391839
"githubProgress": "پیشرفت ستاره‌ها"
1840+
},
1841+
"version": {
1842+
"newVersionAvailable": "نسخه جدید موجود است!",
1843+
"currentVersion": "نسخه فعلی",
1844+
"latestVersion": "آخرین نسخه",
1845+
"clickToUpdate": "نسخه جدید برای دانلود موجود است",
1846+
"upToDate": "به‌روز است",
1847+
"runningLatest": "نسخه {{version}} در حال اجرا",
1848+
"updateBanner": "به‌روزرسانی موجود: {{current}} → {{latest}}",
1849+
"updateBannerMobile": "به‌روزرسانی: {{latest}}",
1850+
"updateCommandLabel": "از طریق SSH متصل شوید و اجرا کنید:",
1851+
"closeBanner": "بستن اعلان به‌روزرسانی",
1852+
"needsUpdate": "نیاز به به‌روزرسانی"
18401853
}
18411854
}

dashboard/public/statics/locales/ru.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,5 +1815,18 @@
18151815
"formHasErrors": "Пожалуйста, исправьте ошибки формы перед отправкой",
18161816
"formInvalid": "Форма недействительна. Пожалуйста, проверьте все обязательные поля.",
18171817
"missingFields": "Пожалуйста, заполните обязательные поля: {{fields}}"
1818+
},
1819+
"version": {
1820+
"newVersionAvailable": "Доступна новая версия!",
1821+
"currentVersion": "Текущая",
1822+
"latestVersion": "Последняя",
1823+
"clickToUpdate": "Доступна новая версия для загрузки",
1824+
"upToDate": "Актуально",
1825+
"runningLatest": "Версия {{version}}",
1826+
"updateBanner": "Доступно обновление: {{current}} → {{latest}}",
1827+
"updateBannerMobile": "Обновление: {{latest}}",
1828+
"updateCommandLabel": "Подключитесь по SSH и выполните:",
1829+
"closeBanner": "Закрыть уведомление об обновлении",
1830+
"needsUpdate": "Требуется обновление"
18181831
}
18191832
}

dashboard/public/statics/locales/zh.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,5 +1874,18 @@
18741874
"formHasErrors": "请在提交前修复表单错误",
18751875
"formInvalid": "表单无效。请检查所有必填字段。",
18761876
"missingFields": "请填写必填字段:{{fields}}"
1877+
},
1878+
"version": {
1879+
"newVersionAvailable": "有新版本可用!",
1880+
"currentVersion": "当前版本",
1881+
"latestVersion": "最新版本",
1882+
"clickToUpdate": "有新版本可供下载",
1883+
"upToDate": "已是最新",
1884+
"runningLatest": "正在运行 {{version}}",
1885+
"updateBanner": "可用更新: {{current}} → {{latest}}",
1886+
"updateBannerMobile": "更新: {{latest}}",
1887+
"updateCommandLabel": "通过 SSH 连接并运行:",
1888+
"closeBanner": "关闭更新通知",
1889+
"needsUpdate": "需要更新"
18771890
}
18781891
}

dashboard/src/components/common/topbar-ad.tsx

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useEffect, useState } from 'react'
33
import { useTranslation } from 'react-i18next'
44
import { Button } from '@/components/ui/button'
55
import { cn } from '@/lib/utils'
6-
import { useTheme, type ColorTheme } from '@/components/common/theme-provider'
6+
import { useTheme } from '@/components/common/theme-provider'
7+
import { getGradientByColorTheme } from '@/constants/ThemeGradients'
78
import useDirDetection from '@/hooks/use-dir-detection'
89

910
const TOPBAR_AD_STORAGE_KEY = 'topbar_ad_closed'
@@ -24,44 +25,6 @@ interface TopbarAdConfig {
2425
}
2526
}
2627

27-
const getGradientByColorTheme = (colorTheme: ColorTheme, isDark: boolean): string => {
28-
const gradients: Record<ColorTheme, { light: string; dark: string }> = {
29-
default: {
30-
light: 'bg-gradient-to-r from-blue-100/90 via-indigo-100/90 to-blue-100/90',
31-
dark: 'bg-gradient-to-r from-blue-950/50 via-indigo-950/50 to-blue-950/50',
32-
},
33-
red: {
34-
light: 'bg-gradient-to-r from-red-100/90 via-rose-100/90 to-red-100/90',
35-
dark: 'bg-gradient-to-r from-red-950/50 via-rose-950/50 to-red-950/50',
36-
},
37-
rose: {
38-
light: 'bg-gradient-to-r from-rose-100/90 via-pink-100/90 to-rose-100/90',
39-
dark: 'bg-gradient-to-r from-rose-950/50 via-pink-950/50 to-rose-950/50',
40-
},
41-
orange: {
42-
light: 'bg-gradient-to-r from-orange-100/90 via-amber-100/90 to-orange-100/90',
43-
dark: 'bg-gradient-to-r from-orange-950/50 via-amber-950/50 to-orange-950/50',
44-
},
45-
green: {
46-
light: 'bg-gradient-to-r from-green-100/90 via-emerald-100/90 to-green-100/90',
47-
dark: 'bg-gradient-to-r from-green-950/50 via-emerald-950/50 to-green-950/50',
48-
},
49-
blue: {
50-
light: 'bg-gradient-to-r from-blue-100/90 via-cyan-100/90 to-blue-100/90',
51-
dark: 'bg-gradient-to-r from-blue-950/50 via-cyan-950/50 to-blue-950/50',
52-
},
53-
yellow: {
54-
light: 'bg-gradient-to-r from-yellow-100/90 via-amber-100/90 to-yellow-100/90',
55-
dark: 'bg-gradient-to-r from-yellow-950/50 via-amber-950/50 to-yellow-950/50',
56-
},
57-
violet: {
58-
light: 'bg-gradient-to-r from-violet-100/90 via-purple-100/90 to-violet-100/90',
59-
dark: 'bg-gradient-to-r from-violet-950/50 via-purple-950/50 to-violet-950/50',
60-
},
61-
}
62-
63-
return isDark ? gradients[colorTheme].dark : gradients[colorTheme].light
64-
}
6528

6629
export default function TopbarAd() {
6730
const { i18n } = useTranslation()
@@ -206,7 +169,7 @@ export default function TopbarAd() {
206169
}
207170

208171
const isDark = resolvedTheme === 'dark'
209-
const gradientBg = getGradientByColorTheme(colorTheme, isDark)
172+
const gradientBg = getGradientByColorTheme(colorTheme, isDark, 'ad')
210173

211174
return (
212175
<div
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { SidebarTrigger } from '@/components/ui/sidebar'
2+
import { useVersionCheck } from '@/hooks/use-version-check'
3+
import { getSystemStats } from '@/service/api'
4+
import { useEffect, useState } from 'react'
5+
import { cn } from '@/lib/utils'
6+
7+
export function SidebarTriggerWithBadge() {
8+
const [currentVersion, setCurrentVersion] = useState<string | null>(null)
9+
const { hasUpdate, isLoading } = useVersionCheck(currentVersion)
10+
11+
useEffect(() => {
12+
const fetchVersion = async () => {
13+
try {
14+
const data = await getSystemStats()
15+
if (data?.version) {
16+
setCurrentVersion(data.version)
17+
}
18+
} catch (error) {
19+
console.error('Failed to fetch version:', error)
20+
}
21+
}
22+
fetchVersion()
23+
}, [])
24+
25+
// Show badge when there's an update available
26+
// The badge is especially important when sidebar is closed/collapsed, but we show it always for visibility
27+
const showBadge = !isLoading && hasUpdate
28+
29+
return (
30+
<div className="relative inline-block">
31+
<SidebarTrigger />
32+
{showBadge && (
33+
<span
34+
className={cn(
35+
'absolute -top-1 -right-1 h-3 w-3 rounded-full',
36+
'bg-amber-500 dark:bg-amber-400',
37+
'border-2 border-background'
38+
)}
39+
aria-label="Update available"
40+
/>
41+
)}
42+
</div>
43+
)
44+
}
45+

dashboard/src/components/layout/sidebar.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { GithubStar } from '@/components/layout/github-star'
22
import { GoalProgress } from '@/components/layout/goal-progress'
3+
import { VersionBadge } from '@/components/layout/version-badge'
4+
import { SidebarTriggerWithBadge } from '@/components/layout/sidebar-trigger-with-badge'
35
import { Language } from '@/components/common/language'
46
import { NavMain } from '@/components/layout/nav-main'
57
import { NavSecondary } from '@/components/layout/nav-secondary'
68
import { NavUser } from '@/components/layout/nav-user'
79
import { useTheme } from '@/components/common/theme-provider'
810
import { ThemeToggle } from '@/components/common/theme-toggle'
9-
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, SidebarTrigger, useSidebar } from '@/components/ui/sidebar'
11+
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarRail, useSidebar } from '@/components/ui/sidebar'
12+
import { TooltipProvider } from '@/components/ui/tooltip'
1013
import { Link } from 'react-router'
1114
import { DISCUSSION_GROUP, DOCUMENTATION, DONATION_URL, REPO_URL } from '@/constants/Project'
1215
import { useAdmin } from '@/hooks/use-admin'
@@ -320,21 +323,24 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
320323
{t('pasarguard')}
321324
</span>
322325
</Link>
323-
<SidebarTrigger />
326+
<SidebarTriggerWithBadge />
324327
</div>
325328
<Sidebar variant="sidebar" collapsible="icon" {...props} className="border-sidebar-border p-0" side={isRTL ? 'right' : 'left'}>
326329
<SidebarRail />
327330
<SidebarHeader>
328331
<SidebarMenu>
329332
<SidebarMenuItem>
330333
{state === 'collapsed' && !isMobile ? (
331-
<SidebarMenuButton size="lg" asChild>
334+
<SidebarMenuButton size="lg" asChild className="relative">
332335
<a href={REPO_URL} target="_blank" className="justify-center !gap-0">
333336
<img
334337
src={resolvedTheme === 'dark' ? window.location.pathname + 'statics/favicon/logo.png' : window.location.pathname + 'statics/favicon/logo-dark.png'}
335338
alt="PasarGuard Logo"
336339
className="h-6 w-6 flex-shrink-0 object-contain"
337340
/>
341+
<TooltipProvider>
342+
<VersionBadge currentVersion={version.replace(/[^0-9.]/g, '')} />
343+
</TooltipProvider>
338344
</a>
339345
</SidebarMenuButton>
340346
) : (
@@ -345,9 +351,14 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
345351
alt="PasarGuard Logo"
346352
className="h-8 w-8 flex-shrink-0 object-contain"
347353
/>
348-
<div className="flex flex-col">
354+
<div className="flex flex-col overflow-hidden">
349355
<span className={cn(isRTL ? 'text-right' : 'text-left', 'truncate text-sm font-semibold leading-tight')}>{t('pasarguard')}</span>
350-
<span className="text-xs opacity-45">{version}</span>
356+
<div className="flex items-center gap-1.5">
357+
<span className="text-xs opacity-45">{version}</span>
358+
<TooltipProvider>
359+
<VersionBadge currentVersion={version.replace(/[^0-9.]/g, '')} />
360+
</TooltipProvider>
361+
</div>
351362
</div>
352363
</a>
353364
</SidebarMenuButton>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
2+
import { useVersionCheck } from '@/hooks/use-version-check'
3+
import { cn } from '@/lib/utils'
4+
import { useTranslation } from 'react-i18next'
5+
import { useSidebar } from '@/components/ui/sidebar'
6+
7+
interface VersionBadgeProps {
8+
currentVersion: string | null
9+
className?: string
10+
}
11+
12+
export function VersionBadge({ currentVersion, className }: VersionBadgeProps) {
13+
const { t } = useTranslation()
14+
const { hasUpdate, latestVersion, releaseUrl, isLoading } = useVersionCheck(currentVersion)
15+
const { state, isMobile } = useSidebar()
16+
17+
if (isLoading || !currentVersion) {
18+
return null
19+
}
20+
21+
const releaseLink = releaseUrl || 'https://github.com/PasarGuard/panel/releases/latest'
22+
const showText = isMobile || state === 'expanded'
23+
const showBadge = state === 'collapsed' && !isMobile
24+
25+
// Show badge when collapsed on desktop
26+
if (showBadge && hasUpdate) {
27+
return (
28+
<span
29+
className={cn(
30+
'absolute bottom-0 right-0 h-2.5 w-2.5 rounded-full',
31+
'bg-amber-500 dark:bg-amber-400',
32+
'border border-background',
33+
'translate-x-1/2 translate-y-1/2',
34+
'z-10'
35+
)}
36+
aria-label="Update available"
37+
/>
38+
)
39+
}
40+
41+
// Show text on mobile or when expanded with tooltip
42+
if (showText && hasUpdate && latestVersion) {
43+
return (
44+
<Tooltip delayDuration={100}>
45+
<TooltipTrigger asChild>
46+
<a
47+
href={releaseLink}
48+
target="_blank"
49+
rel="noopener noreferrer"
50+
className={cn('inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400 hover:underline truncate max-w-full', className)}
51+
onClick={(e) => e.stopPropagation()}
52+
>
53+
<span className="h-1.5 w-1.5 rounded-full bg-amber-500 dark:bg-amber-400 shrink-0" />
54+
<span className="truncate">{t('version.needsUpdate')}</span>
55+
</a>
56+
</TooltipTrigger>
57+
<TooltipContent side="bottom" className="p-1.5">
58+
<div className="space-y-0.5 text-[10px]">
59+
<p className="font-medium">{t('version.newVersionAvailable')}</p>
60+
<p>
61+
{t('version.currentVersion')}: v{currentVersion}{t('version.latestVersion')}: v{latestVersion}
62+
</p>
63+
<p className="text-[9px]">{t('version.clickToUpdate')}</p>
64+
</div>
65+
</TooltipContent>
66+
</Tooltip>
67+
)
68+
}
69+
70+
// Default: show dot with tooltip (for non-mobile, non-expanded states)
71+
if (!hasUpdate) {
72+
return (
73+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500/50 dark:bg-emerald-400/50" />
74+
)
75+
}
76+
77+
return (
78+
<Tooltip delayDuration={100}>
79+
<TooltipTrigger asChild>
80+
<a
81+
href={releaseLink}
82+
target="_blank"
83+
rel="noopener noreferrer"
84+
className={cn('inline-flex', className)}
85+
onClick={(e) => e.stopPropagation()}
86+
>
87+
<span className="h-1.5 w-1.5 rounded-full bg-amber-500 dark:bg-amber-400" />
88+
</a>
89+
</TooltipTrigger>
90+
<TooltipContent side="bottom" className="p-1.5">
91+
<div className="space-y-0.5 text-[10px]">
92+
<p className="font-medium">{t('version.newVersionAvailable')}</p>
93+
<p>
94+
{t('version.currentVersion')}: v{currentVersion}{t('version.latestVersion')}: v{latestVersion}
95+
</p>
96+
<p className="text-[9px]">{t('version.clickToUpdate')}</p>
97+
</div>
98+
</TooltipContent>
99+
</Tooltip>
100+
)
101+
}
102+

0 commit comments

Comments
 (0)