mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-04 10:52:17 +02:00
chore: add update guide
This commit is contained in:
parent
6606a7f901
commit
330b81d908
4 changed files with 275 additions and 10 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import type { Team } from "@stackframe/stack";
|
||||
import {
|
||||
ArrowUpCircle,
|
||||
AudioLines,
|
||||
Brain,
|
||||
ChevronLeft,
|
||||
|
|
@ -54,6 +55,7 @@ import {
|
|||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useAppConfig } from "@/context/AppConfigContext";
|
||||
import { useLatestReleaseVersion } from "@/hooks/useLatestReleaseVersion";
|
||||
import type { LocalUser } from "@/lib/auth";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -89,6 +91,12 @@ export function AppSidebar() {
|
|||
// Version info from app config context
|
||||
const versionInfo = config ? { ui: config.uiVersion, api: config.apiVersion } : null;
|
||||
|
||||
// Check for updates only on self-hosted (OSS) deployments — cloud is managed for the user.
|
||||
const { latest: latestRelease, isBehind, isLatest } = useLatestReleaseVersion(
|
||||
versionInfo?.ui,
|
||||
{ enabled: config?.deploymentMode === "oss" },
|
||||
);
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return pathname.startsWith(path);
|
||||
};
|
||||
|
|
@ -232,17 +240,53 @@ export function AppSidebar() {
|
|||
<div className="flex items-center justify-between">
|
||||
{/* Logo - only show when expanded */}
|
||||
{effectiveState === "expanded" && (
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 px-2 text-xl font-bold"
|
||||
>
|
||||
Dograh
|
||||
{versionInfo && (
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
v{versionInfo.ui}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 px-2 text-xl font-bold"
|
||||
>
|
||||
Dograh
|
||||
{versionInfo && (
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
v{versionInfo.ui}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
{isBehind && latestRelease && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href="https://docs.dograh.com/deployment/update"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 rounded-md border bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-amber-900 transition-opacity hover:opacity-80 dark:bg-amber-950 dark:text-amber-200"
|
||||
>
|
||||
<ArrowUpCircle className="h-3 w-3" />
|
||||
Update
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Latest: {latestRelease} — click to see the update guide</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</Link>
|
||||
{isLatest && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex items-center rounded-md border bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
|
||||
Latest
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>You're running the latest release</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Toggle button - center it when collapsed */}
|
||||
<SidebarTrigger className={cn(
|
||||
|
|
|
|||
107
ui/src/hooks/useLatestReleaseVersion.ts
Normal file
107
ui/src/hooks/useLatestReleaseVersion.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Options {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface Result {
|
||||
latest: string | null;
|
||||
isBehind: boolean;
|
||||
isLatest: boolean;
|
||||
}
|
||||
|
||||
const CACHE_KEY = "dograh-latest-release";
|
||||
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
||||
const SEMVER_RE = /^(?:[a-z][a-z0-9-]*-)?v?(\d+)\.(\d+)\.(\d+)$/i;
|
||||
|
||||
function parseSemver(tag: string): [number, number, number] | null {
|
||||
const m = tag.match(SEMVER_RE);
|
||||
if (!m) return null;
|
||||
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
||||
}
|
||||
|
||||
function isOlder(current: string, latest: string): boolean {
|
||||
const c = parseSemver(current);
|
||||
const l = parseSemver(latest);
|
||||
if (!c || !l) return false;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (c[i] < l[i]) return true;
|
||||
if (c[i] > l[i]) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useLatestReleaseVersion(
|
||||
currentVersion: string | undefined,
|
||||
{ enabled }: Options,
|
||||
): Result {
|
||||
const [latest, setLatest] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !currentVersion) return;
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(CACHE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as { tag: string; fetchedAt: number };
|
||||
if (Date.now() - parsed.fetchedAt < CACHE_TTL_MS) {
|
||||
setLatest(parsed.tag);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed cache
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
fetch("https://api.github.com/repos/dograh-hq/dograh/releases/latest")
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
.then((data) => {
|
||||
if (cancelled || !data?.tag_name) return;
|
||||
const tag = data.tag_name as string;
|
||||
try {
|
||||
localStorage.setItem(
|
||||
CACHE_KEY,
|
||||
JSON.stringify({ tag, fetchedAt: Date.now() }),
|
||||
);
|
||||
} catch {
|
||||
// storage may be full or disabled
|
||||
}
|
||||
setLatest(tag);
|
||||
})
|
||||
.catch(() => {
|
||||
// silent — don't break the sidebar if GitHub is unreachable
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [enabled, currentVersion]);
|
||||
|
||||
const normalizedCurrent = currentVersion
|
||||
? currentVersion.startsWith("v")
|
||||
? currentVersion
|
||||
: `v${currentVersion}`
|
||||
: null;
|
||||
|
||||
const currentParsed = normalizedCurrent ? parseSemver(normalizedCurrent) : null;
|
||||
const latestParsed = latest ? parseSemver(latest) : null;
|
||||
|
||||
const isBehind = !!(
|
||||
normalizedCurrent &&
|
||||
latest &&
|
||||
isOlder(normalizedCurrent, latest)
|
||||
);
|
||||
|
||||
const isLatest = !!(
|
||||
currentParsed &&
|
||||
latestParsed &&
|
||||
currentParsed[0] === latestParsed[0] &&
|
||||
currentParsed[1] === latestParsed[1] &&
|
||||
currentParsed[2] === latestParsed[2]
|
||||
);
|
||||
|
||||
return { latest, isBehind, isLatest };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue