Skip to content

Show Versions#540

Merged
mlsmaycon merged 5 commits intofeature/local-user-invitesfrom
feature/versions
Jan 25, 2026
Merged

Show Versions#540
mlsmaycon merged 5 commits intofeature/local-user-invitesfrom
feature/versions

Conversation

@braginini
Copy link
Copy Markdown
Contributor

@braginini braginini commented Jan 25, 2026

Screenshot 2026-01-25 at 18 58 50 Screenshot 2026-01-25 at 19 25 01

Summary by CodeRabbit

Release Notes

  • New Features
    • Added version information panel to the navigation, displaying current and available versions for Management and Dashboard.
    • Update notification banner appears when new versions are available, linking to upgrade documentation.
    • Version panel is responsive and hidden on NetBird-hosted deployments.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 25, 2026

📝 Walkthrough

Walkthrough

A version tracking system is added to the dashboard. The GitHub workflow extracts version information from repository tags, makes it available during the Next.js build, and a new VersionInfo component displays current versus available versions in the navigation UI with update availability indicators.

Changes

Cohort / File(s) Summary
Configuration & Build
next.config.js, .github/workflows/build_and_push.yml
Environment variable NEXT_PUBLIC_DASHBOARD_VERSION added to Next.js config with fallback to "development"; GitHub workflow step added to derive version from git tags and inject into build environment.
Type Definitions
src/interfaces/Instance.ts
New VersionInfo interface exported with three string fields: management_current_version, management_available_version, dashboard_available_version.
UI Components
src/components/VersionInfo.tsx, src/layouts/Navigation.tsx
New NavigationVersionInfo component created to fetch and display version information with update availability badges; integrated into Navigation layout. Component includes version comparison logic, loading skeleton, tooltip details, and conditional rendering for NetBird deployments.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A version hops from tag to tag,
Through workflows and configs we drag,
Dashboard shows what's new and old,
A tale of updates, bravely told,
With fetches and comparisons bright,
The system tracks versions just right! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Show Versions' is partially related to the changeset but does not capture the main objective. The PR primarily implements user invitation flow (invite acceptance page, invite modal, invites table, related interfaces and utilities), with version display being a secondary feature. The title focuses on only one minor aspect rather than the primary change. Rename the title to reflect the primary feature, such as 'Add user invitation flow with version display' or 'Implement user invite acceptance and management features'.
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/components/VersionInfo.tsx`:
- Around line 19-33: The compareVersions function fails for versions with a
leading "v" or pre-release/build suffixes; update compareVersions to normalize
both inputs before splitting and parsing by (1) trimming whitespace, removing a
leading "v" or "V", and stripping anything after a hyphen or plus
(pre-release/build metadata), (2) then splitting on "." and parsing numeric
parts with parseInt (defaulting to 0) and (3) preserving the existing
loop/comparison logic; reference the compareVersions function and variables
currentParts/latestParts to locate where to apply the normalization.

In `@src/modules/users/UserInviteModal.tsx`:
- Line 326: The h2 in UserInviteModal.tsx has a malformed className string
("text-lg my-0 leading-[1.5 text-center") — the leading utility is missing a
closing bracket; update the className for the <h2> (the element rendering
getTitle()) to include the closing bracket so it reads like "text-lg my-0
leading-[1.5] text-center" (or equivalent valid Tailwind syntax) to fix the CSS
syntax error.

In `@src/modules/users/UserInvitesTable.tsx`:
- Around line 411-422: The default sort state in the useLocalStorage call
(variables sorting, setSorting) uses an invalid field id "is_current" which
doesn't exist on UserInviteListItem; update the initial array passed to
useLocalStorage("netbird-table-sort-invites"+path, [...]) to remove or replace
the first object { id: "is_current", desc: true } with a real property name from
UserInviteListItem (for example "status", "isCurrent", "created_at", or another
existing key), so the table sorting works against a valid field.

In `@src/utils/unauthenticatedApi.ts`:
- Around line 57-69: The invite token is interpolated directly into the path in
fetchInviteInfo and acceptInvite which can break routing for tokens containing
reserved characters; before calling unauthenticatedRequest, URL-encode the token
(e.g., via encodeURIComponent) and use the encoded value in the path
`/users/invites/${encodedToken}` and `/users/invites/${encodedToken}/accept` so
the routes are safe and not treated as extra path segments.
🧹 Nitpick comments (7)
src/interfaces/User.ts (1)

20-64: Prefer Role enum for invite role fields.
Keeps role values consistent across the app and avoids stringly-typed roles.

♻️ Suggested refactor
 export interface UserInviteCreateRequest {
   email: string;
   name: string;
-  role: string;
+  role: Role;
   auto_groups: string[];
   expires_in?: number;
 }
 
 export interface UserInviteCreateResponse {
   id: string;
   email: string;
   name: string;
-  role: string;
+  role: Role;
   auto_groups: string[];
   status: string;
   invite_link: string;
   invite_expires_at: string;
 }
@@
 export interface UserInviteListItem {
   id: string;
   email: string;
   name: string;
-  role: string;
+  role: Role;
   auto_groups: string[];
   expires_at: string;
   created_at: string;
   expired: boolean;
 }
src/app/invite/page.tsx (2)

74-75: Special character validation may reject common characters.

The regex [!@#$%^&*(),.?":{}|<>] excludes common special characters like -, _, /, \, [, ], ~, and +. Consider expanding the set or aligning the hint text (line 263) with the actual validation.

♻️ Suggested improvement
- const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
+ const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>\-_\[\]\\\/;'`~=+]/.test(password);

And update the hint accordingly:

- <PasswordRule met={hasSpecialChar} text="One special character (!@#$%^&*)" />
+ <PasswordRule met={hasSpecialChar} text="One special character" />

95-98: Redundant client-side expiration check.

The isExpired computation duplicates the server's valid field check. Since inviteInfo.valid already accounts for expiration server-side, the client-side date comparison is redundant and could cause false negatives if the user's clock is skewed.

Consider relying solely on inviteInfo.valid:

♻️ Suggested simplification
- const isExpired = useMemo(() => {
-   if (!inviteInfo) return false;
-   return new Date(inviteInfo.expires_at) < new Date();
- }, [inviteInfo]);
-
  // ...
  
- if (isExpired || !inviteInfo?.valid) {
+ if (!inviteInfo?.valid) {
src/modules/users/UserInviteModal.tsx (1)

231-231: Redundant null check on required callback.

onUserCreated is a required prop in ModalProps, so the && guard is unnecessary.

♻️ Optional cleanup
- onUserCreated && onUserCreated(user);
+ onUserCreated(user);

Same applies to line 253:

- onInviteCreated && onInviteCreated(invite);
+ onInviteCreated(invite);
src/modules/users/UserInvitesTable.tsx (2)

503-532: Duplicate InviteUserButton component.

This component is duplicated from src/modules/users/UsersTable.tsx (lines 297-326). Consider extracting it to a shared module to avoid maintenance burden.

#!/bin/bash
# Verify the duplication
rg -n -A 35 'export const InviteUserButton' src/modules/users/

479-481: Use strict equality.

Prefer === over == for consistency.

♻️ Suggested fix
- <DataTableRowsPerPage table={table} disabled={invites?.length == 0} />
+ <DataTableRowsPerPage table={table} disabled={invites?.length === 0} />
  <DataTableRefreshButton
-   isDisabled={invites?.length == 0}
+   isDisabled={invites?.length === 0}
src/modules/users/UsersTable.tsx (1)

146-152: Confirm invite-toggle eligibility and guard account.settings access.

If settings can be undefined, account?.settings.embedded_idp_enabled will throw. Also, please confirm whether cloud tenants should be able to view invites; the current condition hides the toggle on cloud.

♻️ Suggested null-safety tweak
-  const embeddedIdpEnabled = account?.settings.embedded_idp_enabled;
+  const embeddedIdpEnabled = account?.settings?.embedded_idp_enabled ?? false;

Comment on lines +19 to +33
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;

const currentParts = current.split(".").map((p) => parseInt(p, 10) || 0);
const latestParts = latest.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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix version comparison for “vX.Y.Z” / pre-release strings.
parseInt("v1") yields NaN, so current logic can miss updates or show false positives when API returns versions with a leading “v” or suffixes. Normalize before parsing.

🛠️ Suggested fix
-function compareVersions(current: string, latest: string): boolean {
+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;
 
-  const currentParts = current.split(".").map((p) => parseInt(p, 10) || 0);
-  const latestParts = latest.split(".").map((p) => parseInt(p, 10) || 0);
+  const normalize = (v: string) =>
+    v
+      .trim()
+      .replace(/^v/i, "")
+      .split(/[^\d]+/)
+      .filter(Boolean)
+      .map((p) => parseInt(p, 10));
+
+  const currentParts = normalize(current);
+  const latestParts = normalize(latest);
+  if (!currentParts.length || !latestParts.length) return false;
 
   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;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
const currentParts = current.split(".").map((p) => parseInt(p, 10) || 0);
const latestParts = latest.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;
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;
const normalize = (v: string) =>
v
.trim()
.replace(/^v/i, "")
.split(/[^\d]+/)
.filter(Boolean)
.map((p) => parseInt(p, 10));
const currentParts = normalize(current);
const latestParts = normalize(latest);
if (!currentParts.length || !latestParts.length) return false;
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;
}
🤖 Prompt for AI Agents
In `@src/components/VersionInfo.tsx` around lines 19 - 33, The compareVersions
function fails for versions with a leading "v" or pre-release/build suffixes;
update compareVersions to normalize both inputs before splitting and parsing by
(1) trimming whitespace, removing a leading "v" or "V", and stripping anything
after a hyphen or plus (pre-release/build metadata), (2) then splitting on "."
and parsing numeric parts with parseInt (defaulting to 0) and (3) preserving the
existing loop/comparison logic; reference the compareVersions function and
variables currentParts/latestParts to locate where to apply the normalization.

<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>
{isNetBirdHosted() ? "Invite User" : "Create User"}
</h2>
<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>{getTitle()}</h2>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix CSS class syntax error.

There's a typo in the className - missing closing bracket for the leading value.

🐛 Proposed fix
- <h2 className={"text-lg my-0 leading-[1.5 text-center]"}>{getTitle()}</h2>
+ <h2 className={"text-lg my-0 leading-[1.5] text-center"}>{getTitle()}</h2>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>{getTitle()}</h2>
<h2 className={"text-lg my-0 leading-[1.5] text-center"}>{getTitle()}</h2>
🤖 Prompt for AI Agents
In `@src/modules/users/UserInviteModal.tsx` at line 326, The h2 in
UserInviteModal.tsx has a malformed className string ("text-lg my-0 leading-[1.5
text-center") — the leading utility is missing a closing bracket; update the
className for the <h2> (the element rendering getTitle()) to include the closing
bracket so it reads like "text-lg my-0 leading-[1.5] text-center" (or equivalent
valid Tailwind syntax) to fix the CSS syntax error.

Comment on lines +411 to +422
const [sorting, setSorting] = useLocalStorage<SortingState>(
"netbird-table-sort-invites" + path,
[
{
id: "is_current",
desc: true,
},
{
id: "name",
desc: true,
},
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Invalid default sort field.

The default sorting references is_current which doesn't exist in UserInviteListItem. This will cause the sort to have no effect or potentially error.

🐛 Proposed fix
  const [sorting, setSorting] = useLocalStorage<SortingState>(
    "netbird-table-sort-invites" + path,
    [
-     {
-       id: "is_current",
-       desc: true,
-     },
      {
        id: "name",
        desc: true,
      },
+     {
+       id: "expires_at",
+       desc: false,
+     },
    ],
  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [sorting, setSorting] = useLocalStorage<SortingState>(
"netbird-table-sort-invites" + path,
[
{
id: "is_current",
desc: true,
},
{
id: "name",
desc: true,
},
],
const [sorting, setSorting] = useLocalStorage<SortingState>(
"netbird-table-sort-invites" + path,
[
{
id: "name",
desc: true,
},
{
id: "expires_at",
desc: false,
},
],
);
🤖 Prompt for AI Agents
In `@src/modules/users/UserInvitesTable.tsx` around lines 411 - 422, The default
sort state in the useLocalStorage call (variables sorting, setSorting) uses an
invalid field id "is_current" which doesn't exist on UserInviteListItem; update
the initial array passed to useLocalStorage("netbird-table-sort-invites"+path,
[...]) to remove or replace the first object { id: "is_current", desc: true }
with a real property name from UserInviteListItem (for example "status",
"isCurrent", "created_at", or another existing key), so the table sorting works
against a valid field.

Comment on lines +57 to +69
export async function fetchInviteInfo(token: string): Promise<UserInviteInfo> {
return unauthenticatedRequest<UserInviteInfo>("GET", `/users/invites/${token}`);
}

export async function acceptInvite(
token: string,
password: string,
): Promise<UserInviteAcceptResponse> {
return unauthenticatedRequest<UserInviteAcceptResponse>(
"POST",
`/users/invites/${token}/accept`,
{ password },
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

URL-encode invite tokens before path interpolation.
Unencoded tokens can break routing or be interpreted as extra path segments.

🛠️ Suggested fix
 export async function fetchInviteInfo(token: string): Promise<UserInviteInfo> {
-  return unauthenticatedRequest<UserInviteInfo>("GET", `/users/invites/${token}`);
+  const encodedToken = encodeURIComponent(token);
+  return unauthenticatedRequest<UserInviteInfo>(
+    "GET",
+    `/users/invites/${encodedToken}`,
+  );
 }
 
 export async function acceptInvite(
   token: string,
   password: string,
 ): Promise<UserInviteAcceptResponse> {
+  const encodedToken = encodeURIComponent(token);
   return unauthenticatedRequest<UserInviteAcceptResponse>(
     "POST",
-    `/users/invites/${token}/accept`,
+    `/users/invites/${encodedToken}/accept`,
     { password },
   );
 }
🤖 Prompt for AI Agents
In `@src/utils/unauthenticatedApi.ts` around lines 57 - 69, The invite token is
interpolated directly into the path in fetchInviteInfo and acceptInvite which
can break routing for tokens containing reserved characters; before calling
unauthenticatedRequest, URL-encode the token (e.g., via encodeURIComponent) and
use the encoded value in the path `/users/invites/${encodedToken}` and
`/users/invites/${encodedToken}/accept` so the routes are safe and not treated
as extra path segments.

@mlsmaycon mlsmaycon changed the base branch from main to feature/local-user-invites January 25, 2026 20:19
@mlsmaycon
Copy link
Copy Markdown
Contributor

@CodeRabbit, review this Pr and update description again now that it is pointing to the right target

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 25, 2026

@mlsmaycon, I'll review the entire PR and update the description now that it's targeting the correct branch.

✅ Actions performed

Summary regeneration triggered.

@mlsmaycon mlsmaycon merged commit c686939 into feature/local-user-invites Jan 25, 2026
4 checks passed
@mlsmaycon mlsmaycon deleted the feature/versions branch January 25, 2026 20:27
@coderabbitai coderabbitai Bot mentioned this pull request Jan 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants