Compare commits

...
Author SHA1 Message Date
TechHutTV d0cca1aa31 Darker light-mode danger-text 2026-09-16 11:26:30 -07:00
Brandon Hopkins 6a26f546c5 Hydration-safe theme provider, review contrast fixes 2026-09-15 08:17:15 -07:00
Brandon Hopkins 5776a7dd5c Tokenize hard-coded colors, fix dark-only literals in light mode 2026-09-15 07:27:08 -07:00
Brandon Hopkins 208176ba9d Align colors with desktop UI 2026-09-15 07:14:08 -07:00
Brandon Hopkins 8a1ba01ef2 Merge remote-tracking branch 'origin/main' into ui-accessibility 2026-09-15 06:58:25 -07:00
Maycon SantosandClaude Fable 5.1 da81cb7f50 Pick a private-capable proxy cluster when bootstrapping the agent network (#780)
* Pick a private-capable proxy cluster when bootstrapping the agent network

The connect-provider wizard bootstraps the account's agent network
endpoint from a proxy cluster it auto-picks off /reverse-proxies/domains,
taking the first validated free domain. That list includes clusters that
cannot host the endpoint: the endpoint is a private service, reachable
only from connected peers and authorised by their tunnel identity, which
needs a cluster with at least one connected embedded proxy — the same
supports_private flag the Reverse Proxy modal gates NetBird-Only Access
on. Management refuses a bootstrap onto a cluster reporting it false, so
the wizard could propose a pick the API rejects, and the endpoint it
assigns is immutable.

Filter the candidates on supports_private before auto-picking, and give
the "nothing to pick" callout a second message for the case where
clusters exist but none of them has an embedded proxy — a different
problem from having no proxy at all, and a different fix.

Only an explicit false disqualifies a cluster: a management build that
predates the flag reports nothing at all, and dropping every cluster
there would block setup on a backend that would have accepted it.

Callout takes an optional data-testid so a spec can assert on the state
it announces rather than on its copy.

* Derive the bootstrap cluster instead of holding it in state

Nothing picks the bootstrap cluster: there is no selector, so the state
could only ever mirror the first usable entry of the domains list, with
an effect writing it back on arrival and a render pass to show it. Derive
it from that list instead, which drops the state, the effect and the two
resets that kept them in sync.

* Declare the e2e proxy private so a bootstrap cluster exists

The connect-provider wizard now offers only clusters whose proxies report
the private capability, because the agent network endpoint is a private
service and management refuses to bootstrap it anywhere else. The e2e
environment's reverse-proxy ran with NB_PROXY_PRIVATE at its default of
false, so with that filter in place no cluster qualified: the wizard
showed the no-cluster callout, Continue stayed disabled, and every spec
that walks through the wizard failed on the Models tab it could no longer
reach — agentgateway, Kimi and the refused-save spec.

Declare the capability on the primary proxy. The no-ports proxy stays
centralised, so the pick also has to skip past a cluster that does not
qualify, which is the case the wizard filter exists for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6

* Send the no-cluster callout to the agent network Clusters section

The callout told users to connect a proxy under Reverse Proxy, the generic
services page. Agent Network has its own Clusters tab under Configuration,
which is where a user setting up the agent network expects to manage the
clusters it runs on, so both messages link there instead. The tab and the
callout gate on the same permission (services read), so every role that
sees the callout can follow the link.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6

* Say private capabilities, not embedded proxy, in the no-cluster callout

The requirement is a cluster with private capabilities, which is what the
supports_private flag reports. Tying the message to an embedded proxy named
one way of getting there as if it were the requirement itself, and pointed
users at the wrong fix. The callout now states the requirement and where to
connect such a cluster.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 22:19:37 +02:00
Jack Carter f4e80d37c4 feat: restore the Active toggle and keep it visible on disabled rows (#803)
Disabled routing peers, network resources, policies and legacy routes were
only distinguishable by a faded row, and the fade also dimmed the controls
needed to re-enable them. Bring back the Active toggle column that was
hidden in #654, and add a cellClassName hook to DataTable with a shared
fadeDisabledRowCells helper that fades every cell of a disabled row except
the toggle and actions cells.
2026-09-14 15:43:50 +02:00
Pascal Fischer de045582e5 mark is cloud false unless explicitly overridden (#799) 2026-09-12 09:07:45 +02:00
Eduard GertandClaude Opus 5 6a85748b91 chore: update Next.js to 16.3.4 and refresh dependencies (#797)
Bump next from 16.3.0 to 16.3.4 and refresh the transitive lockfile
entries that came with it (@next/* swc binaries, sharp 0.35.4,
rolldown, vitest, playwright).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 14:47:14 +02:00
Nicolas Frati a9a91905fa Add rootless dashboard container image (#791) 2026-09-09 23:00:19 +02:00
Eduard Gert 3459a4db28 chore: remove dead code and unused dependencies (#787)
Remove the unreachable FirewallGPT module and the abandoned traffic-event
detail prototypes, along with the dependencies they were the sole consumers of.

FirewallGPT was disabled in 9d8d9ee4 by removing its trigger button, leaving
the modal mounted but impossible to open. TrafficEventDetails was never
imported anywhere, and it was the only consumer of TrafficEventChart.

Dependency changes:

- Drop @faker-js/faker, whose only usage was mock chart data in the removed
  TrafficEventChart. This also resolves GHSA-qxc2-j82w-r537 without needing
  the 9 -> 10 major upgrade.
- Drop react-confetti-explosion, only used by the removed
  FirewallGPTSuccessModal.
- Bump @types/node to 20.19.43 so it satisfies the peer range of the vite
  version pulled in by vitest, silencing an ERESOLVE warning on install.
  Stays on the 20.x line to match the Node 20 used in CI.
- Add an allowScripts deny policy for the four packages with install scripts.
  Both @axa-fr postinstalls are bare echo statements, fsevents ships a
  prebuilt binary, and the unrs-resolver postinstall only verifies a binding
  that optionalDependencies already installs.
2026-09-09 11:38:38 +02:00
TechHutTV e8df6c153c More fixes on posture check elements 2026-08-28 13:41:04 +02:00
TechHutTV 8023ff5047 Light mode fixes; borders, keycaps, callouts 2026-08-28 11:15:25 +02:00
TechHutTV 32ecc374bb Fix posture check icons, no change on switch 2026-08-28 10:37:55 +02:00
TechHutTV f3a4f22e1e Pinned posture check icon colors; darkened dialog borders 2026-08-28 10:21:36 +02:00
TechHutTV 968ca333c1 Tokenized canvas edge strockes plus tests 2026-08-27 23:32:47 +02:00
TechHutTV 1e77129bdf Lighter-light mode hover colors for badges/dropdown 2026-08-27 23:11:31 +02:00
TechHutTV 0dfdaf62c6 Fix button hover 2026-08-27 18:47:49 +02:00
TechHutTV 2f74430616 Banner colors and settings sidebar fix 2026-08-27 18:37:24 +02:00
TechHutTV b3cf73950e Fix button seperation 2026-08-27 18:33:37 +02:00
TechHutTV addea360a1 Add useTheme to testing 2026-08-27 18:21:44 +02:00
TechHutTV 4a597a7fdd Merge remote-tracking branch 'origin/main' into ui-accessibility 2026-08-27 17:43:54 +02:00
TechHutTV ba88b838f9 Merge remote-tracking branch 'origin/main' into ui-accessibility 2026-08-27 17:36:51 +02:00
Brandon Hopkins a8275e54d5 Audit timeline contrast fixes plus nameserver gradient inversion 2026-06-29 21:28:23 -07:00
Brandon Hopkins f77d0dc48e Theme provider review fixes: leak guard, token colors, dev misuse error 2026-06-29 21:28:23 -07:00
Brandon Hopkins 67cd3ddf08 Fix High-availability badge hover and Beta badge 2026-06-29 21:28:23 -07:00
Brandon Hopkins d90fe688f5 clean up badge colors 2026-06-29 21:28:23 -07:00
Brandon Hopkins 6dd3dfa153 Modal footer tint, darker modal/input borders in light mode 2026-06-29 21:28:23 -07:00
Brandon Hopkins 68128dffb7 minor fixes and color cleanup 2026-06-29 21:28:23 -07:00
Brandon Hopkins daff0659d4 theme switcher in profile dropdown 2026-06-29 21:28:05 -07:00
Brandon Hopkins 019320a481 StatusPicker sync 2026-06-29 21:26:44 -07:00
Brandon Hopkins d1a2d407b3 next-themes is removed and replaced with a self-owned provider 2026-06-29 21:26:28 -07:00
TechHutTV 41043bb10c Fix light mode announcement banner 2026-06-29 21:26:28 -07:00
TechHutTV d4f6adb807 Fixed invisible light-mode filter text on peers page 2026-06-29 21:26:28 -07:00
Brandon Hopkins ccf417e103 light mode in AnnouncementBanner.tsx 2026-06-29 21:26:28 -07:00
TechHutTV 6a6a3b86c4 Light-mode color fixes plus CodeRabbit-audited nb-gray token migration 2026-06-29 21:26:28 -07:00
TechHutTV 9849e0a70c fixing color for shared UI components 2026-06-29 21:22:16 -07:00
TechHutTV 9249464d5c light mode for control center 2026-06-29 21:22:16 -07:00
TechHutTV 06daecf89a fix callout colors 2026-06-29 21:22:16 -07:00
TechHutTV d58d50c4ec fix light sub-text and directional arrows 2026-06-29 21:22:16 -07:00
TechHutTV 97007db62e light mode draft with switcher 2026-06-29 21:22:15 -07:00
198 changed files with 2148 additions and 3583 deletions

No files matched your search

File renamed without changes.
+29
View File
@@ -98,6 +98,24 @@ jobs:
type=ref,event=pr
type=sha
- name: Docker meta (rootless)
id: meta_rootless
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_IMAGE }}
${{ env.GHCR_IMAGE }}
flavor: |
latest=false
suffix=-rootless,onlatest=true
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
type=ref,event=pr
type=sha
- name: Log in to Docker Hub
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: docker/login-action@v3
@@ -123,7 +141,18 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push rootless Docker image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.rootless
push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta_rootless.outputs.tags }}
labels: ${{ steps.meta_rootless.outputs.labels }}
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
run: |
echo '### Pushed tags' >> $GITHUB_STEP_SUMMARY
echo '${{ steps.meta.outputs.tags }}' >> $GITHUB_STEP_SUMMARY
echo '${{ steps.meta_rootless.outputs.tags }}' >> $GITHUB_STEP_SUMMARY
+6 -1
View File
@@ -35,6 +35,9 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
# local UI sandbox (throwaway preview harness — never commit)
/src/app/sandbox/
# config
.local*config*.json
.test-config.json
@@ -53,4 +56,6 @@ playwright-report*.zip
/public/ironrdp-pkg/
/public/netbird.wasm
.idea
src/.local-config*
src/.local-config*
# local mock data (throwaway preview — never commit)
/src/utils/mockData.ts
+24 -12
View File
@@ -11,6 +11,7 @@
# ./build.sh # build + export netbird-dashboard.tar.gz
# IMAGE_TAG=v1 ./build.sh # override the image tag
# SKIP_BUILD=1 ./build.sh # reuse an existing out/ (skip npm build)
# ROOTLESS=1 ./build.sh # build the amd64-rootless image variant
#
set -euo pipefail
@@ -19,8 +20,26 @@ cd "$(dirname "$0")"
IMAGE_NAME="${IMAGE_NAME:-netbird-dashboard}"
IMAGE_TAG="${IMAGE_TAG:-amd64}"
DOCKERFILE="docker/Dockerfile"
CONTAINER_PORT=80
OUTPUT_SUFFIX=""
case "${ROOTLESS:-0}" in
0) ;;
1)
DOCKERFILE="docker/Dockerfile.rootless"
CONTAINER_PORT=8080
OUTPUT_SUFFIX="-rootless"
[[ "${IMAGE_TAG}" == *-rootless ]] || IMAGE_TAG="${IMAGE_TAG}-rootless"
;;
*)
echo "ERROR: ROOTLESS must be 0 or 1." >&2
exit 1
;;
esac
IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
OUTPUT="${OUTPUT:-${IMAGE_NAME}.tar.gz}"
OUTPUT="${OUTPUT:-${IMAGE_NAME}${OUTPUT_SUFFIX}.tar.gz}"
# Build the static export (out/) unless told to reuse an existing one. Next 16
# no longer runs ESLint during `next build`; types are checked by the build via
@@ -39,17 +58,10 @@ if [[ ! -d out ]]; then
exit 1
fi
# The .dockerignore lives under docker/, but Docker reads it from the build
# context root. Stage it for the duration of the build so node_modules/.next
# don't get shipped into the build context.
cleanup() { rm -f .dockerignore; }
trap cleanup EXIT
cp docker/.dockerignore .dockerignore
echo "==> Building ${IMAGE} for linux/amd64"
docker build \
--platform linux/amd64 \
-f docker/Dockerfile \
-f "${DOCKERFILE}" \
-t "${IMAGE}" \
.
@@ -60,13 +72,13 @@ echo "==> Done: ${OUTPUT} ($(du -h "${OUTPUT}" | cut -f1))"
echo
echo "Copy and run on the server:"
echo " scp ${OUTPUT} user@your-server:/tmp/"
echo " docker load < /tmp/${OUTPUT}"
echo " docker load < /tmp/${OUTPUT##*/}"
echo
echo "Run (nginx serves on container port 80). Set the runtime env vars the"
echo "Run (nginx serves on container port ${CONTAINER_PORT}). Set the runtime env vars the"
echo "dashboard templates at startup — for the Agent Network onboarding test,"
echo "flip NETBIRD_AGENT_NETWORK_ONLY=true:"
echo " docker run -d --name ${IMAGE_NAME} --restart unless-stopped \\"
echo " -p 8080:80 \\"
echo " -p 8080:${CONTAINER_PORT} \\"
echo " -e NETBIRD_MGMT_API_ENDPOINT=https://your-mgmt:443 \\"
echo " -e AUTH_AUTHORITY=https://your-idp/realms/netbird \\"
echo " -e AUTH_CLIENT_ID=netbird-dashboard \\"
+29
View File
@@ -0,0 +1,29 @@
FROM alpine:3.24
RUN apk add --no-cache bash ca-certificates gettext nginx tzdata && \
rm -rf /var/cache/apk/* && \
mkdir -p /tmp/nginx
STOPSIGNAL SIGINT
EXPOSE 8080
WORKDIR /usr/share/nginx/html
COPY docker/default.conf /etc/nginx/http.d/default.conf
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/rootless.conf /etc/nginx/http.d/rootless.conf
COPY docker/init_react_envs.sh /usr/local/init_react_envs.sh
COPY out/ /usr/share/nginx/html/
RUN sed -i \
-e 's/listen 80 default_server;/listen 8080 default_server;/' \
-e 's/listen \[::\]:80 default_server;/listen [::]:8080 default_server;/' \
/etc/nginx/http.d/default.conf && \
sed -i '/^user nginx;$/d' /etc/nginx/nginx.conf && \
chmod +x /usr/local/init_react_envs.sh && \
chgrp -R 0 /etc/nginx/http.d /usr/share/nginx/html /tmp/nginx && \
chmod -R g=u /etc/nginx/http.d /usr/share/nginx/html /tmp/nginx
USER 10001:0
ENTRYPOINT ["/usr/local/init_react_envs.sh"]
CMD ["/usr/sbin/nginx", "-e", "/dev/stderr", "-g", "pid /tmp/nginx.pid;"]
+47 -11
View File
@@ -1,23 +1,59 @@
# NetBird Dashboard
NetBird Dashboard is NetBirds Management server UI. It allows users to signin, view setup keys and manage peers. This image is **not ready** for production use.
## Tags
```latest``` ```vX.X.X``` not available yet.
```main``` built on every PR being merged to the repository
NetBird Dashboard is NetBird's management server UI.
## Tags
Each published image tag also has a rootless variant with the `-rootless`
suffix. For example, `main` is also published as `main-rootless`.
## How to use this image
HTTP run:
HTTP:
```shell
docker run -d --rm -p 80:80 wiretrustee/dashboard:main
docker run -d --rm -p 80:80 netbirdio/dashboard:main
```
Using SSL certificate from Let's Encrypt®:
Using an SSL certificate from Let's Encrypt:
```shell
docker run -d --rm -p 80:80 -p 443:443 \
-e LETSENCRYPT_DOMAIN=app.mydomain.com \
-e LETSENCRYPT_EMAIL=hello@mydomain.com \
netbirdio/dashboard:main
```
> For SSL generation, you need to run this image in a server with proper public IP and a domain name pointing to it.
For certificate generation, the server needs a public IP and a domain name
pointing to it.
### Rootless and OpenShift
The rootless image listens on port `8080`, runs as UID `10001` by default, and
supports OpenShift's arbitrary UID when the container retains GID `0`:
```shell
docker run -d --rm \
--user 12345:0 \
-p 8080:8080 \
-e AUTH_AUTHORITY=https://idp.example.com \
-e AUTH_CLIENT_ID=netbird-dashboard \
-e AUTH_AUDIENCE=netbird-dashboard \
-e AUTH_SUPPORTED_SCOPES='openid profile email' \
-e USE_AUTH0=false \
-e NETBIRD_MGMT_API_ENDPOINT=https://api.example.com \
netbirdio/dashboard:main-rootless
```
The rootless image does not include Certbot or expose HTTPS. Terminate TLS at
the OpenShift Route, Ingress, or another reverse proxy.
## Environment variables
* ```NGINX_SSL_PORT``` Changes the port that Nginx listens to. Defaults to ```443```
* ```LETSENCRYPT_DOMAIN``` Enables Certbot`s client execution for the specified domain. Defaults to ```none```
* ```LETSENCRYPT_EMAIL``` Email used in Certbot`s client execution to register the certificate request. Defaults to ```example@local```
- `NGINX_SSL_PORT` changes the port that Nginx listens to. Defaults to `443`.
- `LETSENCRYPT_DOMAIN` enables Certbot for the specified domain. Defaults to
`none`.
- `LETSENCRYPT_EMAIL` is used to register the certificate request. Defaults to
`example@local`.
These certificate variables apply only to the standard image.
+6
View File
@@ -173,6 +173,12 @@ for f in $(grep -R -l AUTH_SUPPORTED_SCOPES /usr/share/nginx/html); do
rm "$f".copy
done
# When a command is provided, initialization happens before the server starts.
# The rootless image uses this path so nginx never needs to be reloaded.
if (( $# > 0 )); then
exec "$@"
fi
# Reload nginx so the patched CSP header takes effect.
# supervisord starts nginx (priority 100) before this script (priority 201) and
# never reloads it, so without this nginx keeps serving the static default.conf
+7
View File
@@ -0,0 +1,7 @@
# nginx includes this file inside the http context.
# All runtime state stays in a directory writable by the arbitrary UID used by OpenShift.
client_body_temp_path /tmp/nginx/client_temp;
proxy_temp_path /tmp/nginx/proxy_temp;
fastcgi_temp_path /tmp/nginx/fastcgi_temp;
uwsgi_temp_path /tmp/nginx/uwsgi_temp;
scgi_temp_path /tmp/nginx/scgi_temp;
+10
View File
@@ -926,6 +926,16 @@ services:
# GeoLite2 download removes a startup stall of up to 2 minutes
# when pkgs.netbird.io is slow.
- NB_PROXY_DISABLE_GEOLOCATION=true
# Declare the private capability so this cluster can host the agent
# network endpoint. That endpoint is a private service — reachable only
# from connected peers — and management refuses to bootstrap it onto a
# cluster without a private-capable proxy; the connect-provider wizard
# reads the same flag (supports_private) to pick its cluster. Without
# it no cluster qualifies and the wizard stops at the provider tab,
# which strands every spec that walks through it. The no-ports proxy
# below stays centralised, so the pick has to skip past a cluster that
# does not qualify.
- NB_PROXY_PRIVATE=true
volumes:
- ./proxy-certs:/certs:ro
command: [
@@ -0,0 +1,206 @@
/**
* Agent Network bootstrap-cluster spec.
*
* The account's agent network endpoint is assigned once, from the proxy
* cluster the connect-provider wizard pins it to, and it is immutable after
* that. Not every live cluster can host it: the endpoint is a private service,
* reachable only from connected peers and authorised by their tunnel identity,
* which only a cluster with private capabilities can serve. Management
* reports that per cluster as supports_private and refuses a
* bootstrap onto a cluster without it.
*
* This spec pins the wizard's half of that contract: it picks a
* private-capable cluster even when a centralised one is listed first, and it
* blocks setup — with a message naming what is missing — when no listed
* cluster qualifies. Both the settings and domains routes are mocked, so the
* assertions do not depend on which clusters the test backend happens to have
* connected, and no provider is created against it.
*/
import { type Browser, expect, type Page, test } from "@playwright/test";
import { loginToApp } from "../helpers/auth";
const AGENT_NETWORK_CONFIG_KEY = "netbird-test-agent-network";
// Listed first, and unusable: a centralised proxy cluster. Picking it is the
// bug this spec guards against.
const CENTRAL_CLUSTER = "central.proxy.netbird.io";
// Listed second, and the only valid pick.
const PRIVATE_CLUSTER = "private.proxy.netbird.io";
const UNBOOTSTRAPPED_DEFAULTS = {
endpoint: "",
proxy_address: "",
dedicated: false,
enable_log_collection: true,
enable_prompt_collection: false,
redact_pii: false,
access_log_retention_days: 30,
};
const BOOTSTRAPPED_SETTINGS = {
...UNBOOTSTRAPPED_DEFAULTS,
endpoint: `violet.${PRIVATE_CLUSTER}`,
proxy_address: PRIVATE_CLUSTER,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
};
/** A free (cluster-backed) domain as /reverse-proxies/domains reports it. */
function freeDomain(name: string, supportsPrivate: boolean) {
return {
id: name,
domain: name,
validated: true,
type: "free",
supports_private: supportsPrivate,
supports_custom_ports: true,
require_subdomain: true,
};
}
async function json(route: import("@playwright/test").Route, body: unknown) {
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(body),
});
}
/**
* Opens the providers page for an account that has not been bootstrapped,
* with the given clusters on the domains endpoint. Returns the page plus a
* promise-friendly getter for the bootstrap POST body, which is captured and
* answered locally so the real backend is never mutated.
*/
async function openWizard(
browser: Browser,
domains: ReturnType<typeof freeDomain>[],
): Promise<{ page: Page; close: () => Promise<void> }> {
const context = await browser.newContext({
storageState: "e2e/fixtures/auth/owner.json",
});
try {
await context.addInitScript(
([key, value]) => {
try {
window.localStorage.setItem(key as string, value as string);
} catch (e) {}
},
[AGENT_NETWORK_CONFIG_KEY, "enabled"],
);
const page = await context.newPage();
await page.route("**/api/agent-network/settings", (route) => {
const method = route.request().method();
if (method === "GET") return json(route, UNBOOTSTRAPPED_DEFAULTS);
if (method === "POST") return json(route, BOOTSTRAPPED_SETTINGS);
return route.continue();
});
await page.route("**/api/reverse-proxies/domains", (route) =>
route.request().method() === "GET"
? json(route, domains)
: route.continue(),
);
// Keep the create hermetic: the wizard only reaches this once the
// bootstrap it does first has succeeded.
await page.route("**/api/agent-network/providers", (route) =>
route.request().method() === "POST"
? json(route, { id: "prov-e2e", name: "e2e-provider" })
: route.continue(),
);
await loginToApp(page, "owner");
await page.goto("/agent-network/providers");
await page.keyboard.press("Escape");
return { page, close: () => context.close() };
} catch (e) {
await context.close();
throw e;
}
}
/** Fills the provider tab with the minimum a create needs. */
async function fillProviderTab(page: Page) {
await page
.getByPlaceholder("https://api.openai.com")
.fill("https://api.openai.com");
await page.getByPlaceholder("sk-...").first().fill("sk-e2e-bootstrap-key");
}
test.describe("Agent Network bootstrap cluster @agent-network", () => {
test("bootstraps onto a private-capable cluster, skipping a centralised one listed first", async ({
browser,
}) => {
const { page, close } = await openWizard(browser, [
freeDomain(CENTRAL_CLUSTER, false),
freeDomain(PRIVATE_CLUSTER, true),
]);
try {
await page
.getByRole("button", { name: "Connect Provider" })
.first()
.click({ force: true });
// The unusable cluster is listed, but it is not a blocker: a usable one
// exists, so the wizard proceeds without the warning.
await expect(
page.getByTestId("agent-network-no-cluster-callout"),
).toHaveCount(0);
await fillProviderTab(page);
const bootstrap = page.waitForResponse(
(resp) =>
resp.url().includes("/agent-network/settings") &&
resp.request().method() === "POST",
{ timeout: 30_000 },
);
await page
.getByRole("button", { name: "Continue" })
.click({ force: true });
await page
.getByRole("button", { name: "Connect Provider" })
.last()
.click({ force: true });
const payload = (await bootstrap).request().postDataJSON();
expect(payload.proxy_address).toBe(PRIVATE_CLUSTER);
} finally {
await close();
}
});
test("blocks setup when no listed cluster has private capabilities", async ({
browser,
}) => {
const { page, close } = await openWizard(browser, [
freeDomain(CENTRAL_CLUSTER, false),
]);
try {
await page
.getByRole("button", { name: "Connect Provider" })
.first()
.click({ force: true });
const callout = page.getByTestId("agent-network-no-cluster-callout");
await expect(callout).toBeVisible({ timeout: 10_000 });
await expect(callout).toContainText("private capabilities");
// The fix lives in the agent network's own Clusters section, not the
// generic Reverse Proxy page, so that is where the callout sends users.
await expect(callout.getByRole("link")).toHaveAttribute(
"href",
"/agent-network/configuration?tab=clusters",
);
// With nothing to pin to there is no bootstrap to make, so the wizard
// cannot advance — rather than sending a request management refuses.
await fillProviderTab(page);
await expect(
page.getByRole("button", { name: "Continue" }),
).toBeDisabled();
} finally {
await close();
}
});
});
+570 -463
View File
File diff suppressed because it is too large. Load diff
+8 -4
View File
@@ -48,7 +48,7 @@
"@types/crypto-js": "^4.2.2",
"@types/d3": "^7.4.3",
"@types/lodash": "4.17.24",
"@types/node": "20.10.6",
"@types/node": "20.19.43",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-window": "^1.8.8",
@@ -75,11 +75,10 @@
"js-cookie": "^3.0.7",
"lodash": "4.18.1",
"lucide-react": "^0.566.0",
"next": "16.3.0",
"next": "16.3.4",
"punycode": "^2.3.1",
"react": "^19.2.4",
"react-chartjs-2": "^5.3.0",
"react-confetti-explosion": "^3.0.3",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.4",
"react-ga4": "^2.1.0",
@@ -101,7 +100,6 @@
"minimatch": ">=10.2.1"
},
"devDependencies": {
"@faker-js/faker": "^9.5.1",
"@playwright/test": "^1.52.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
@@ -114,5 +112,11 @@
"prettier": "3.0.3",
"tailwindcss": "^3.4.17",
"vitest": "^4.1.10"
},
"allowScripts": {
"@axa-fr/oidc-client": false,
"@axa-fr/react-oidc": false,
"fsevents": false,
"unrs-resolver": false
}
}
@@ -60,7 +60,7 @@ export default function Integrations() {
(!isNetBirdCloud() && (permission?.settings?.read ?? false))
}
>
<div className={"border-l border-nb-gray-930 w-full"}>
<div className={"w-full"}>
<IdentityProviderTab />
<SSOTab />
<EventStreamingTab />
@@ -87,7 +87,7 @@ export default function AgentNetworkConfigurationPage() {
<GroupsProvider>
<PeersProvider>
<AIProvidersProvider>
<div className={"border-l border-nb-gray-930 w-full"}>
<div className={"w-full"}>
<Tabs.Content
value={TAB_BUDGET_SETTINGS}
className={"w-full"}
+13 -2
View File
@@ -15,6 +15,11 @@ import React, { useState } from "react";
import { useSWRConfig } from "swr";
import GroupsProvider from "@/contexts/GroupsProvider";
import PeersProvider from "@/contexts/PeersProvider";
import { useTheme } from "@/contexts/ThemeProvider";
import {
canvasBackground,
canvasDotColor,
} from "@/modules/control-center/utils/canvas-theme";
import PoliciesProvider from "@/contexts/PoliciesProvider";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
@@ -105,6 +110,8 @@ const PRO_OPTIONS = { hideAttribution: true };
const DEFAULT_VIEWPORT = { x: 0, y: 0, zoom: EMPTY_STATE_ZOOM };
function ControlCenterCanvas() {
const { resolvedTheme } = useTheme();
const isLight = resolvedTheme === "light";
const canvas = useCanvasState();
const ui = useControlCenterUI();
const draft = useDraft();
@@ -283,7 +290,7 @@ function ControlCenterCanvas() {
onInit={stableOnInit}
maxZoom={DEFAULT_MAX_ZOOM}
minZoom={DEFAULT_MIN_ZOOM}
colorMode={"dark"}
colorMode={isLight ? "light" : "dark"}
panOnDrag={canInteract}
panOnScroll={draft.isSelectMode && !emptyState}
zoomOnScroll={canInteract}
@@ -297,7 +304,11 @@ function ControlCenterCanvas() {
selectionOnDrag={draft.isSelectMode && !emptyState}
selectionMode={SelectionMode.Partial}
>
<Background bgColor={"#181a1d"} gap={20} color={"#717171"} />
<Background
bgColor={canvasBackground(resolvedTheme)}
gap={20}
color={canvasDotColor(resolvedTheme)}
/>
<CanvasContextMenu onOpenChange={setContextMenuOpen} />
<NodeContextMenu
position={nodeContextMenuPos}
+1 -1
View File
@@ -115,7 +115,7 @@ const GroupDetailsName = () => {
>
<div
className={cn(
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer",
"flex h-8 w-8 items-center justify-center gap-2 text-nb-gray-300 hover:text-nb-gray-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer",
!isAllowedToRename &&
"opacity-40 cursor-not-allowed pointer-events-none",
)}
+2 -2
View File
@@ -257,7 +257,7 @@ const PeerHeader = () => {
<ModalTrigger>
<div
className={
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
"flex h-8 w-8 items-center justify-center gap-2 text-nb-gray-400 hover:text-nb-gray-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
}
>
<PencilIcon size={16} />
@@ -741,7 +741,7 @@ function EditableValue({
<span>{value}</span>
{canEdit && (
<button
className="flex w-7 h-7 items-center justify-center gap-2 text-nb-gray-400 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
className="flex w-7 h-7 items-center justify-center gap-2 text-nb-gray-400 hover:text-nb-gray-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
onClick={(e) => {
e.stopPropagation();
onEdit();
+1 -1
View File
@@ -107,7 +107,7 @@ export default function NetBirdSettings() {
page={"Settings"}
hasAccess={permission?.billing?.read || permission?.settings?.read}
>
<div className={"border-l border-nb-gray-930 w-full"}>
<div className={"w-full"}>
{account && <AuthenticationTab account={account} />}
{permission.setup_keys.read && <SetupKeysTab />}
{account?.settings?.embedded_idp_enabled &&
+1 -1
View File
@@ -183,7 +183,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
<div className={"flex items-center gap-3"}>
<div
className={
"w-10 h-10 rounded-full relative flex items-center justify-center text-white uppercase text-md font-medium bg-nb-gray-900"
"w-10 h-10 rounded-full relative flex items-center justify-center text-nb-gray-100 uppercase text-md font-medium bg-nb-gray-900"
}
style={
isServiceUser
+67 -4
View File
@@ -5,6 +5,69 @@
:root {
--toasts-before: 0;
--lift: 1;
/* nb-gray INVERTED ramp (light mode) — space-separated RGB channels.
High stops are surfaces (near-white), low stops are text (near-black).
DEFAULT (the app background) sits slightly below white so white
inputs, buttons and cards stand in front of it.
The ramp is pure neutral (R = G = B) and matches the desktop client's
light ramp (netbirdio/netbird#7344) so both UIs share one palette. */
--nb-gray-DEFAULT: 243 243 243;
--nb-gray-50: 26 26 26;
--nb-gray-100: 33 33 33;
--nb-gray-200: 50 50 50;
--nb-gray-250: 58 58 58;
--nb-gray-300: 77 77 77;
--nb-gray-350: 92 92 92;
--nb-gray-400: 108 108 108;
--nb-gray-500: 135 135 135;
--nb-gray-600: 154 154 154;
--nb-gray-700: 209 209 209;
--nb-gray-800: 226 226 226;
--nb-gray-850: 234 234 234;
--nb-gray-900: 238 238 238;
--nb-gray-910: 240 240 240;
--nb-gray-920: 243 243 243;
--nb-gray-925: 245 245 245;
--nb-gray-930: 246 246 246;
--nb-gray-935: 248 248 248;
--nb-gray-940: 250 250 250;
--nb-gray-950: 252 252 252;
--nb-gray-960: 255 255 255;
/* Loading-skeleton colours, consumed by ThemeProvider's SkeletonTheme. */
--skeleton-base: 238 238 238;
--skeleton-highlight: 247 247 247;
}
.dark {
/* nb-gray ORIGINAL ramp (dark mode) */
--nb-gray-DEFAULT: 24 26 29;
--nb-gray-50: 244 246 247;
--nb-gray-100: 228 231 233;
--nb-gray-200: 203 210 214;
--nb-gray-250: 183 192 198;
--nb-gray-300: 163 173 181;
--nb-gray-350: 143 156 168;
--nb-gray-400: 124 137 148;
--nb-gray-500: 97 110 121;
--nb-gray-600: 83 93 103;
--nb-gray-700: 71 78 87;
--nb-gray-800: 63 68 75;
--nb-gray-850: 54 59 64;
--nb-gray-900: 46 50 56;
--nb-gray-910: 43 47 51;
--nb-gray-920: 37 40 45;
--nb-gray-925: 30 33 35;
--nb-gray-930: 37 40 44;
--nb-gray-935: 31 33 36;
--nb-gray-940: 28 30 33;
--nb-gray-950: 24 26 29;
--nb-gray-960: 22 24 27;
/* Dark skeletons keep their existing nb-gray-920 / nb-gray-850 values. */
--skeleton-base: 37 40 45;
--skeleton-highlight: 54 59 64;
}
html{
@@ -12,13 +75,13 @@ html{
}
h1 {
@apply text-2xl font-medium text-gray-700 dark:text-nb-gray-100 my-1;
@apply text-2xl font-medium text-nb-gray-100 my-1;
}
h2 {
@apply text-xl font-medium text-gray-700 dark:text-nb-gray-100 my-1;
@apply text-xl font-medium text-nb-gray-100 my-1;
}
p {
@apply font-light tracking-wide text-gray-700 dark:text-zinc-50 text-sm;
@apply font-light tracking-wide text-nb-gray-200 text-sm;
}
.p-default {
@@ -95,7 +158,7 @@ p {
}
.timescape input:focus {
@apply bg-nb-gray-700 text-white rounded py-1 px-0.5 border-0 outline-0;
@apply bg-nb-gray-700 text-nb-gray-50 rounded py-1 px-0.5 border-0 outline-0;
}
.timescape .separator {
Loaded 20 of 198 files, more files were not shown because too many files have changed in this diff. Show more