add: agent update notifier, pagination

This commit is contained in:
Théo LAGACHE
2026-02-10 15:52:08 +01:00
parent 0a2ca8c0b6
commit 80bdca138e
5 changed files with 107 additions and 46 deletions
@@ -15,6 +15,8 @@ export type paginationNavigationProps = {
export const PaginationNavigation = (props: paginationNavigationProps) => {
const { className, totalPages, currentPage, goToPage, goToPrevPage, goToNextPage, maxVisiblePages = 3 } = props;
if (totalPages <= 1) return null;
return (
<Pagination className={cn("", className)}>
<PaginationContent>
@@ -6,10 +6,12 @@ import {Card} from "@/components/ui/card";
import {ConnectionIndicator} from "@/components/wrappers/common/connection-indicator";
import {formatDateLastContact} from "@/utils/date-formatting";
import {AgentWith} from "@/db/schema/08_agent";
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database} from "lucide-react";
import {Activity, ChevronRight, Copy, Check, Fingerprint, Server, Database, AlertCircle} from "lucide-react";
import {Badge} from "@/components/ui/badge";
import {truncateWords} from "@/utils/text";
import {useIsMobile} from "@/hooks/use-mobile";
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
import {useAgentUpdateCheck} from "@/features/agents/hooks/use-agent-update-check";
export type agentCardProps = {
data: AgentWith;
@@ -20,6 +22,8 @@ export const AgentCard = (props: agentCardProps) => {
const isMobile = useIsMobile();
const [isCopied, setIsCopied] = useState(false);
const {newRelease, isUpdateAvailable} = useAgentUpdateCheck(agent.version);
const handleCopy = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -44,9 +48,23 @@ export const AgentCard = (props: agentCardProps) => {
{isMobile ? truncateWords(agent.name, 2) : agent.name}
</h3>
{agent.version && (
<Badge variant="secondary" className="h-5 px-1.5 text-[10px] font-bold tracking-wider">
v{agent.version}
</Badge>
<div className="flex items-center gap-2">
<Badge variant="secondary" className="h-5 px-1.5 text-[10px] font-bold tracking-wider">
v{agent.version}
</Badge>
{isUpdateAvailable && (
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center justify-center w-5 h-5 bg-yellow-500/10 rounded-full border border-yellow-500/20 text-yellow-600">
<AlertCircle className="w-3 h-3" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>Update available: {newRelease?.tag_name}</p>
</TooltipContent>
</Tooltip>
)}
</div>
)}
</div>
@@ -0,0 +1,19 @@
"use client";
import {useQuery} from "@tanstack/react-query";
import {getNewAgentRelease} from "@/features/updates/services/github";
export const useAgentUpdateCheck = (currentVersion?: string | null) => {
const {data: newRelease, isLoading} = useQuery({
queryKey: ["agent-new-release", currentVersion],
queryFn: () => currentVersion ? getNewAgentRelease(currentVersion) : Promise.resolve(null),
staleTime: 1000 * 60 * 60,
enabled: !!currentVersion,
});
return {
newRelease,
isLoading,
isUpdateAvailable: !!newRelease,
};
};
+63 -41
View File
@@ -14,7 +14,7 @@ type ParsedVersion = {
};
function parseVersion(version: string): ParsedVersion {
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/);
const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/);
if (!match) return {
major: 0,
minor: 0,
@@ -30,6 +30,49 @@ function parseVersion(version: string): ParsedVersion {
};
}
const findLatestVersion = (currentVersion: string, releases: GitHubRelease[]): GitHubRelease | null => {
const cleanCurrentVersion = currentVersion.replace(/^v/, "");
const isStable = /^\d+\.\d+\.\d+$/.test(cleanCurrentVersion);
const isRc = /^\d+\.\d+\.\d+-rc\.\d+$/.test(cleanCurrentVersion);
const currentParsedVersion = parseVersion(cleanCurrentVersion);
if (isRc) {
const latestStableVersion = releases.find(r => !r.prerelease);
const latestRcVersion = releases.find(r => r.prerelease && (r.tag_name.includes("rc") || (r.name && r.name.includes("rc"))));
if (latestStableVersion) {
const versionStr = latestStableVersion.tag_name || latestStableVersion.name;
const latestStableParsedVersion = parseVersion(versionStr);
if (latestStableParsedVersion.major > currentParsedVersion.major) return latestStableVersion;
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor > currentParsedVersion.minor) return latestStableVersion;
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor === currentParsedVersion.minor && latestStableParsedVersion.patch >= currentParsedVersion.patch) return latestStableVersion;
}
if (latestRcVersion) {
const versionStr = latestRcVersion.tag_name || latestRcVersion.name;
const latestRcParsedVersion = parseVersion(versionStr);
if (latestRcParsedVersion.major > currentParsedVersion.major) return latestRcVersion;
if (latestRcParsedVersion.major === currentParsedVersion.major && latestRcParsedVersion.minor > currentParsedVersion.minor) return latestRcVersion;
if (latestRcParsedVersion.major === currentParsedVersion.major && latestRcParsedVersion.minor === currentParsedVersion.minor && latestRcParsedVersion.patch > currentParsedVersion.patch) return latestRcVersion;
if (latestRcParsedVersion.major === currentParsedVersion.major && latestRcParsedVersion.minor === currentParsedVersion.minor && latestRcParsedVersion.patch === currentParsedVersion.patch &&
latestRcParsedVersion.rc !== undefined && currentParsedVersion.rc !== undefined && latestRcParsedVersion.rc > currentParsedVersion.rc) return latestRcVersion;
}
} else if (isStable) {
const latestStableVersion = releases.find(r => !r.prerelease);
if (latestStableVersion) {
const versionStr = latestStableVersion.tag_name || latestStableVersion.name;
const latestStableParsedVersion = parseVersion(versionStr);
if (latestStableParsedVersion.major > currentParsedVersion.major) return latestStableVersion;
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor > currentParsedVersion.minor) return latestStableVersion;
if (latestStableParsedVersion.major === currentParsedVersion.major && latestStableParsedVersion.minor === currentParsedVersion.minor && latestStableParsedVersion.patch > currentParsedVersion.patch) return latestStableVersion;
}
}
return null;
};
export const getNewRelease = async (currentVersion: string): Promise<GitHubRelease | null> => {
try {
const response = await fetch("https://api.github.com/repos/Portabase/portabase/releases");
@@ -38,49 +81,28 @@ export const getNewRelease = async (currentVersion: string): Promise<GitHubRelea
}
const releases: GitHubRelease[] = await response.json();
const isStable = /^\d+\.\d+\.\d+$/.test(currentVersion);
const isRc = /^\d+\.\d+\.\d+-rc\.\d+$/.test(currentVersion);
console.log("isStable", isStable)
console.log("isRc", isRc)
const currentParsedVersion = parseVersion(currentVersion)
const latestStableVersion = releases.find(r => !r.prerelease)
const latestRcVersion = releases.find(r => r.name.includes("rc"))!
if (isRc) {
if (!latestRcVersion) return null
const latestRcParsedVersion = parseVersion(latestRcVersion.name)
if (currentParsedVersion.major < latestRcParsedVersion.major) return latestRcVersion;
if (currentParsedVersion.minor < latestRcParsedVersion.minor) return latestRcVersion;
if (currentParsedVersion.patch < latestRcParsedVersion.patch) return latestRcVersion;
if (currentParsedVersion.rc! < latestRcParsedVersion.rc!) return latestRcVersion;
if (!latestStableVersion) return null
const latestStableParsedVersion = parseVersion(latestStableVersion.name)
if (currentParsedVersion.major < latestStableParsedVersion.major) return latestStableVersion;
if (currentParsedVersion.minor < latestStableParsedVersion.minor) return latestStableVersion;
if (currentParsedVersion.patch < latestStableParsedVersion.patch) return latestStableVersion;
return null;
} else if (isStable) {
if (!latestStableVersion) return null
const latestStableParsedVersion = parseVersion(latestStableVersion.name)
if (currentParsedVersion.major < latestStableParsedVersion.major) return latestStableVersion;
if (currentParsedVersion.minor < latestStableParsedVersion.minor) return latestStableVersion;
if (currentParsedVersion.patch < latestStableParsedVersion.patch) return latestStableVersion;
return null;
}
return null;
return findLatestVersion(currentVersion, releases);
} catch (error) {
console.error("Failed to fetch latest release", error);
return null;
}
};
export const getNewAgentRelease = async (currentVersion: string): Promise<GitHubRelease | null> => {
try {
const response = await fetch("https://api.github.com/repos/Portabase/agent-rust/releases", {
next: { revalidate: 3600 }
});
if (!response.ok) {
return null;
}
const releases: GitHubRelease[] = await response.json();
return findLatestVersion(currentVersion, releases);
} catch (error) {
console.error("Failed to fetch latest agent release", error);
return null;
}
};