mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e1c29b769 |
No files matched your search
@@ -7,6 +7,7 @@ import * as React from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { isNetBirdCloud } from "@utils/netbird";
|
||||
import { isNewerVersion } from "@utils/version";
|
||||
import { useApplicationContext } from "@/contexts/ApplicationProvider";
|
||||
import { VersionInfo as VersionInfoType } from "@/interfaces/Instance";
|
||||
|
||||
@@ -17,36 +18,14 @@ function formatVersion(version: string): string {
|
||||
return version;
|
||||
}
|
||||
|
||||
// Self-hosted builds can carry a numeric build suffix (e.g. "0.76.3-31256681241")
|
||||
// that overflows the sidebar. Show the semver only and keep the rest for the
|
||||
// Builds can carry a suffix that overflows the sidebar: semver build metadata
|
||||
// ("0.77.0+enterprise.1" on enterprise builds) or a numeric CI build number
|
||||
// ("0.76.3-31256681241"). Show the release only and keep the full string for the
|
||||
// tooltip. Pre-release labels like "-rc.1" are left intact so they stay visible.
|
||||
function formatShortVersion(version: string): string {
|
||||
return formatVersion(version).replace(/^(v?\d+(?:\.\d+)*)-\d+$/, "$1");
|
||||
}
|
||||
|
||||
function compareVersions(current: string, latest: string): boolean {
|
||||
// Returns true if latest is newer than current
|
||||
if (!current || !latest) return false;
|
||||
if (current === "development") return false;
|
||||
|
||||
// Strip "v" prefix if present
|
||||
const normalizedCurrent = current.replace(/^v/, "");
|
||||
const normalizedLatest = latest.replace(/^v/, "");
|
||||
|
||||
const currentParts = normalizedCurrent
|
||||
.split(".")
|
||||
.map((p) => parseInt(p, 10) || 0);
|
||||
const latestParts = normalizedLatest
|
||||
.split(".")
|
||||
.map((p) => parseInt(p, 10) || 0);
|
||||
|
||||
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
||||
const c = currentParts[i] || 0;
|
||||
const l = latestParts[i] || 0;
|
||||
if (l > c) return true;
|
||||
if (l < c) return false;
|
||||
}
|
||||
return false;
|
||||
return formatVersion(version)
|
||||
.replace(/\+.*$/, "")
|
||||
.replace(/^(v?\d+(?:\.\d+)*)-\d+$/, "$1");
|
||||
}
|
||||
|
||||
export const NavigationVersionInfo = () => {
|
||||
@@ -84,12 +63,18 @@ const NavigationVersionInfoContent = () => {
|
||||
|
||||
if (!versionInfo) return null;
|
||||
|
||||
// Compare versions to detect updates (returns false for "development" versions)
|
||||
const managementUpdateAvailable = compareVersions(
|
||||
versionInfo.management_current_version,
|
||||
versionInfo.management_available_version,
|
||||
);
|
||||
const dashboardUpdateAvailable = compareVersions(
|
||||
// Prefer the server's verdict: it knows the release channel the installation
|
||||
// runs on and compares with a full semver implementation. Fall back to a local
|
||||
// comparison for management servers that don't report the flag yet.
|
||||
const managementUpdateAvailable =
|
||||
versionInfo.management_update_available ??
|
||||
isNewerVersion(
|
||||
versionInfo.management_current_version,
|
||||
versionInfo.management_available_version,
|
||||
);
|
||||
// The dashboard's installed version is baked in at build time and the server
|
||||
// never sees it, so this one is always compared here.
|
||||
const dashboardUpdateAvailable = isNewerVersion(
|
||||
dashboardVersion,
|
||||
versionInfo.dashboard_available_version,
|
||||
);
|
||||
|
||||
@@ -22,4 +22,9 @@ export interface VersionInfo {
|
||||
management_current_version: string;
|
||||
management_available_version: string;
|
||||
dashboard_available_version: string;
|
||||
// Whether a newer management release exists, decided server-side with a full
|
||||
// semver comparison — so an enterprise build ("0.77.0+enterprise.1") counts
|
||||
// as up to date against "0.77.0". Optional: management servers that predate
|
||||
// the field omit it.
|
||||
management_update_available?: boolean;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isNativeSSHSupported } from "./version.js";
|
||||
import { compareVersions, isNativeSSHSupported, isNewerVersion } from "./version.js";
|
||||
|
||||
console.log("=== Testing isNativeSSHSupported ===");
|
||||
const sshTestCases = [
|
||||
@@ -17,6 +17,9 @@ const sshTestCases = [
|
||||
{ version: "v0.60.0-beta", shouldSupport: true, desc: "with suffix" },
|
||||
{ version: "v0.60.0-rc1", shouldSupport: true, desc: "with rc suffix" },
|
||||
{ version: "v0.59.9-beta", shouldSupport: false, desc: "old version with suffix" },
|
||||
{ version: "0.60.0+enterprise.1", shouldSupport: true, desc: "enterprise build" },
|
||||
{ version: "0.59.9+enterprise.1", shouldSupport: false, desc: "old enterprise build" },
|
||||
{ version: "0.76.3-31256681241", shouldSupport: true, desc: "CI build suffix" },
|
||||
];
|
||||
|
||||
let failures = 0;
|
||||
@@ -24,11 +27,56 @@ sshTestCases.forEach(({ version, shouldSupport, desc }) => {
|
||||
const result = isNativeSSHSupported(version);
|
||||
const status = result === shouldSupport ? "âś“" : "âś—";
|
||||
if (result !== shouldSupport) failures++;
|
||||
const label = desc ? `${version.padEnd(15)} (${desc})` : version.padEnd(15);
|
||||
const label = desc ? `${version.padEnd(22)} (${desc})` : version.padEnd(22);
|
||||
console.log(
|
||||
`${status} ${label} → ${result.toString().padStart(5)} (expected: ${shouldSupport})`,
|
||||
);
|
||||
});
|
||||
|
||||
console.log("\n=== Testing compareVersions (version >= minVersion) ===");
|
||||
const compareTestCases = [
|
||||
{ version: "0.60.0", min: "0.60.0", expected: true },
|
||||
{ version: "0.60.1", min: "0.60.0", expected: true },
|
||||
{ version: "0.59.9", min: "0.60.0", expected: false },
|
||||
// Suffixed builds must compare on the release only — the trailing build
|
||||
// number is not a fourth release component.
|
||||
{ version: "0.60.0-beta.1", min: "0.60.1", expected: false, desc: "pre-release vs newer patch" },
|
||||
{ version: "0.60.0+enterprise.1", min: "0.60.1", expected: false, desc: "enterprise vs newer patch" },
|
||||
{ version: "0.77.0+enterprise.1", min: "0.77.0", expected: true, desc: "enterprise build metadata ignored" },
|
||||
];
|
||||
|
||||
compareTestCases.forEach(({ version, min, expected, desc }) => {
|
||||
const result = compareVersions(version, min);
|
||||
const status = result === expected ? "âś“" : "âś—";
|
||||
if (result !== expected) failures++;
|
||||
const label = `${version} >= ${min}`.padEnd(38);
|
||||
console.log(
|
||||
`${status} ${label}${desc ? ` (${desc})` : ""} → ${result.toString().padStart(5)} (expected: ${expected})`,
|
||||
);
|
||||
});
|
||||
|
||||
console.log("\n=== Testing isNewerVersion (update available) ===");
|
||||
const updateTestCases = [
|
||||
// The shape enterprise/cloud installations report: same release, plus a build
|
||||
// tag. Not an update.
|
||||
{ current: "0.77.0+enterprise.1", latest: "0.77.0", expected: false, desc: "enterprise build, same release" },
|
||||
{ current: "0.77.0+enterprise.1", latest: "0.77.1", expected: true, desc: "enterprise build, newer patch" },
|
||||
{ current: "0.77.0+enterprise.2", latest: "0.76.9", expected: false, desc: "enterprise build, older latest" },
|
||||
{ current: "0.76.3-31256681241", latest: "0.76.3", expected: false, desc: "CI build suffix" },
|
||||
{ current: "v2.91.0", latest: "2.91.0", expected: false, desc: "v prefix on one side" },
|
||||
{ current: "development", latest: "0.77.0", expected: false, desc: "development build" },
|
||||
{ current: "0.77.0", latest: "", expected: false, desc: "unknown latest" },
|
||||
];
|
||||
|
||||
updateTestCases.forEach(({ current, latest, expected, desc }) => {
|
||||
const result = isNewerVersion(current, latest);
|
||||
const status = result === expected ? "âś“" : "âś—";
|
||||
if (result !== expected) failures++;
|
||||
const label = `${current || "(empty)"} → ${latest || "(empty)"}`.padEnd(38);
|
||||
console.log(
|
||||
`${status} ${label}${desc ? ` (${desc})` : ""} → ${result.toString().padStart(5)} (expected: ${expected})`,
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`\n${failures} test(s) failed`);
|
||||
process.exit(failures > 0 ? 1 : 0);
|
||||
+54
-6
@@ -36,6 +36,32 @@ export const getLatestNetbirdRelease = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Split a version string into its numeric release components.
|
||||
*
|
||||
* Handles every shape NetBird components report: an optional "v" prefix, semver
|
||||
* build metadata ("0.77.0+enterprise.1" on enterprise management builds), a CI
|
||||
* build suffix ("0.76.3-31256681241"), and pre-release labels ("0.60.0-rc.1").
|
||||
* Everything after the release itself is dropped — build metadata carries no
|
||||
* precedence in semver, and a pre-release of X.Y.Z is treated as X.Y.Z so a
|
||||
* feature gate keyed on a release also holds for its release candidates.
|
||||
*
|
||||
* Splitting on "." alone (the previous behaviour) left the suffix inside a
|
||||
* component, so "0.77.0+enterprise.1" parsed as [0, 77, 0, 1] — smuggling the
|
||||
* build number in as a fourth release component. That ranked today's tags
|
||||
* correctly only by coincidence; dropping the suffix removes the guesswork.
|
||||
*/
|
||||
const releaseParts = (version: string): number[] =>
|
||||
version
|
||||
.trim()
|
||||
.replace(/^v/i, "")
|
||||
.split(/[-+]/, 1)[0]
|
||||
.split(".")
|
||||
.map((part) => {
|
||||
const parsed = parseInt(part, 10);
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
});
|
||||
|
||||
/**
|
||||
* Compare semantic versions.
|
||||
* Returns true if version >= minVersion.
|
||||
@@ -44,12 +70,8 @@ export const compareVersions = (
|
||||
version: string,
|
||||
minVersion: string,
|
||||
): boolean => {
|
||||
const parseVersion = (v: string): number[] => {
|
||||
return v.replace(/^v/, "").split(".").map(Number);
|
||||
};
|
||||
|
||||
const vParts = parseVersion(version);
|
||||
const minParts = parseVersion(minVersion);
|
||||
const vParts = releaseParts(version);
|
||||
const minParts = releaseParts(minVersion);
|
||||
|
||||
for (let i = 0; i < Math.max(vParts.length, minParts.length); i++) {
|
||||
const vPart = vParts[i] || 0;
|
||||
@@ -62,6 +84,32 @@ export const compareVersions = (
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true when `latest` is a strictly newer release than `current` — i.e.
|
||||
* an update is available. Only release components decide: an enterprise build
|
||||
* ("0.77.0+enterprise.1") is up to date against the "0.77.0" it was built from,
|
||||
* matching how the management server evaluates it server-side.
|
||||
*
|
||||
* "development" builds never report an update, in either position.
|
||||
*/
|
||||
export const isNewerVersion = (current: string, latest: string): boolean => {
|
||||
if (!current || !latest) return false;
|
||||
if (current === "development" || latest === "development") return false;
|
||||
|
||||
const currentParts = releaseParts(current);
|
||||
const latestParts = releaseParts(latest);
|
||||
|
||||
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
||||
const currentPart = currentParts[i] || 0;
|
||||
const latestPart = latestParts[i] || 0;
|
||||
|
||||
if (latestPart > currentPart) return true;
|
||||
if (latestPart < currentPart) return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if peer as routing peer is supported by the provided version and operating system.
|
||||
* Routing peers are supported on Windows, macOS, iOS & Android starting from NetBird v0.36.6+.
|
||||
|
||||
Reference in new issue
Block a user