Compare commits

...
Author SHA1 Message Date
Viktor Liu 0844507d8e Remove client-side 5m request timeout cap 2026-03-19 16:03:07 +01:00
Viktor Liu 7812f76bcf Address review 2026-03-19 16:02:16 +01:00
Viktor Liu ba31f63400 Remove useMemo from clusterSupportsCustomPorts to fix stale disabled state 2026-03-19 16:02:16 +01:00
Viktor Liu 5cf41dbc76 Add header auth and access restrictions to reverse proxy 2026-03-19 16:02:16 +01:00
Viktor LiuandEduard Gert aff2365ef7 Add layer 4 protocol support to reverse proxy (#579)
* Add layer 4 proto support

* Fix initialResource fallback and UDP session_idle_timeout

* Fix tlsResourceId init for resource-driven create flows, UDP timeout label

* Address PR review: ServiceMode enum, resource init fix, modal title, a11y

* Add L4 protocol values to ReverseProxyTargetProtocol, remove unsafe double cast

* Add aria-labels to L4 port/host inputs

* Unify domain input for all service modes including L4

* Support L4 proxy events

* Fix custom port reset on edit and show port in L4 service link

* Remove redundant listen port from L4 target cell

* Show link only for HTTP/TLS services, copy-on-click for TCP/UDP

* Move mode badge before domain and use fixed width for alignment

* Fix HTTP services to open as link instead of copy

* Hide old proxy clusters from L4 domain selector

* Move service type inside modal

* Update auth cell

* Add target selector component

* Extract into separate components

* hide services types for not supported clusters

* Remove advanced settings tab in http targetmodal and use accordion instead

* Update advanced settings

* Update target device row

* Update text

* Add type cell

* Fix flat target name cell

* Update modal title

* Fix edit target in flat table

* Remove unused proxycluster interface

* Move proxy type icon into type component

* sync cloud

* use emptyrow

* fix l4 type

* fix duplicate error notification

* Set the correct target type

* Fix subnet host editable

* Fix subnet host editable

* hide selector when initial resource or peer

* Rename dropdown

* Update text

* update status cell

* merge cloud

* Update tooltips

* Address coderabbit comments

* Fix skeleton device card

* Update listen port tooltip

* Adjust padding

* update package-lock.json

* bump next to 16.1.7

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-03-18 17:43:00 +01:00
Zoltan PappandEduard Gert bad057d415 [dashboard] feat: add auto_update_always toggle to client settings (#580)
* [dashboard] feat: add auto_update_always toggle to client settings

Add "Always Update" toggle to the Clients settings tab that controls
whether updates are installed automatically in the background or require
user interaction from the UI. Includes a warning icon and caution callout
when enabled to highlight the risk of disrupting active connections.

* [dashboard] fix: improve auto-update UI clarity and toggle label

Clarify that automatic updates require user interaction by updating the
description. Rename "Always Update" to "Force Automatic Updates" for
clarity. Move warning callout below the toggle switch instead of inside it.

* Update src/modules/settings/ClientSettingsTab.tsx

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-03-16 15:35:44 +01:00
Misha Bragin 4d846e2c94 Improve text for optional resource setiings (#584) 2026-03-12 20:48:09 +01:00
Eduard Gert 15fb6e0b05 Refactor resource modal (#582) 2026-03-12 16:30:51 +01:00
Eduard Gert 55c5525626 Fix resource group policy when adding single resource as destination (#581) 2026-03-11 19:23:59 +01:00
Eduard Gert c0c1f4688e Add proxy events sort (#560)
* Add proxy events sort

* Fix coderabbit comment

* Disable local sort when server pagination is used
2026-03-10 10:10:53 +01:00
Eduard Gert b5a8f751ba Create policies inside resources (#568)
* Add acl tooltips

* Adjust resource modal and add tooltips

* Prevent nextjs navigation trigger on tab change

* Update wording

* add acl into resource

* Refactor resource policies

* Add prop to hide group edit and disable redirect

* Add skeleton loader to network page

* Create policy for new resources

* Show existing policies if groups are matching

* Add confirm dialog after creating resource without policy

* Add dialog if user edits policy that is used in multiple resources

* Add callout when selecting resource groups containing policies

* Add dialog if deleting policies containing resources

* Fix stale policies and new group creation in resource modal

* Remove whitespace

* Fix sort

* Cleanup

* Address coderabbit comments

* Fix policy alignment

* Fix initial resource

* disable selector if user did not select  resource groups

* Consider current resource when editing / deleting policy

* Remove unused mutate

* Fix dot position

* Remove ask for policy

* Fix policy index

* Fix multiple resource confirm dialog on policy cell
2026-03-10 10:10:38 +01:00
Eduard Gert 10a8e7b745 Fix stale certificate issued state (#575)
* Fix stale certificate issued state

* fix coderabbit
2026-03-09 10:08:35 +01:00
Viktor Liu 60e8394010 Add per-target options to reverse proxy (#576) 2026-03-06 18:55:28 +01:00
Eduard Gert 9420214059 Bump minimatch and ajv dependencies (#572) 2026-03-02 11:32:52 +01:00
b949f60afe Feature/client service expose (#567)
* add draft

* add reverse proxy activities

* move peer expose settings into client settings tab and fix activity descriptions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* prevent false positive group report

* add docs link

* allow save when groups are added to the setting

* Add loading skeleton to client settings, update icon, use grouphelper to allow creating new groups, remove .patch

* mv expose settings from extra settings

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-02-24 14:54:58 +01:00
Eduard Gert d498e4cc25 Fix dns records pagination (#566) 2026-02-20 21:42:26 +01:00
Eduard Gert 130dc0c32c Fix group unused filter (#565) 2026-02-19 10:32:14 +01:00
Eduard Gert f5824d6ddb Allow empty groups for reverse proxy sso auth (#563) 2026-02-18 16:27:13 +01:00
Eduard Gert 829395f908 Add hover to reverse proxy auth methods (#564) 2026-02-18 13:39:19 +01:00
Eduard Gert 8eebec78b4 Preserve query params for ssh and rdp (#559) 2026-02-16 17:34:08 +01:00
raghvendra 3e01a6dafd refactor: simplify FullScreenLoading to use boolean prop instead of string union (#555) 2026-02-16 11:10:26 +01:00
Maycon Santos 1555b94043 Fix service cluster status (#556) 2026-02-16 09:23:22 +01:00
Eduard Gert 6c62127d42 Update announcement (#553) 2026-02-13 20:56:40 +01:00
Eduard Gert b71d0fde89 Add reverse proxy (#552)
* **New Features**
  * Full Reverse Proxy UI: Services, Targets, Clusters, Custom Domains (with verification) and a Proxy Events page.
  * In-app modals for service auth (SSO, password, PIN) and a new PIN input component.

* **Improvements**
  * Network & Peer pages: tabbed views (Resources, Routing Peers, Services) and improved tables, search and filters.
  * Toast stacking/visibility and global toast styling refined.
2026-02-13 18:59:16 +01:00
Misha Bragin 84c239ce30 Indicate that local user auth is disabled (#551) 2026-02-12 15:16:34 +01:00
Aaron Dewes ba66201c64 Remove architecture info tooltip for MacOS (#550)
* Remove architecture info tooltip for MacOS

Previously, this tooltip helped users determine which binary to download. Since #501, there is only one universal binary download link, so keeping the tooltip explaining how to determine the CPU architecture is unnecessary.

* fix: Remove unused imports
2026-02-12 11:21:08 +01:00
raghvendra c6341e000f docs: fix broken Auth0 quickstart link in README (#548)
* docs: fix broken Auth0 quickstart link

* docs: spell error fixes in readme

* docs: fix typo in NETBIRD_MGMT_API_ENDPOINT placeholder in readme
2026-02-09 11:41:09 +08:00
Eduard Gert 750f660bcc Update NextJS to 16.1.6 (#547)
* Update NextJS to 16.1.6

* Update Node in workflow

* Fix rabbit comments

* Fix types

* Add engines field
2026-02-02 15:34:23 +01:00
Misha Bragin ea148545e8 Disable local users when LocalAuthDisabled = true (#546) 2026-02-01 14:31:57 +01:00
Misha Bragin d2febbf27b Fix version comparison (#544) 2026-01-27 14:13:27 +01:00
Misha Bragin 615b4487ad Point to the right upgrade doc (#543) 2026-01-27 12:23:56 +01:00
Misha Bragin a7c7800916 Add invite notification count badge (#542) 2026-01-27 10:44:39 +01:00
Eduard Gert 3d51e0893e Update announcement (#538)
* Update announcement

* Fix repeated fetches
2026-01-27 09:33:43 +01:00
Misha Bragin d7d44b5817 Adjust Invites API (#541)
* Add API adjustments

* Invite_link renamed to invite_token
2026-01-26 19:25:56 +01:00
Misha Bragin f67f39b68b Local user invites (#539) 2026-01-25 21:40:49 +01:00
dependabot[bot] d2bc7a1f57 Bump lodash from 4.17.21 to 4.17.23 (#537)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-23 13:28:28 +01:00
Eduard Gert 818ba5daa4 Allow wildcard dns zone records (#536) 2026-01-20 17:32:14 +01:00
Ali AmerandEduard Gert 3a30f76629 Add Frontend Support for Peer Debug Bundle Trigger and History (#485)
* implement debug ui

* update job ui

* Add type cell, show tooltip if peer is offline, add copy to clipboard for upload key, show error reason in tooltip

* update job event description

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-01-20 17:12:33 +01:00
Misha Bragin 34dc21c89d Add password change (embedded Idp) (#535) 2026-01-20 15:00:14 +01:00
Eduard Gert 2e37703622 Update CONTRIBUTOR_LICENSE_AGREEMENT.md (#534) 2026-01-19 14:55:04 +01:00
Eduard Gert 8aec338c43 Fix dns doc link (#533) 2026-01-19 10:01:55 +01:00
Viktor Liu f4f0c240fd Bump wasm to v0.63.0 (#531) 2026-01-19 09:49:26 +01:00
Viktor Liu 04e22a3c7e Enable SSH for Windows and Android peers (#532)
* Enable SSH for Windows and Android peers, hide update badge for temporary peers

* Fix RDP to use tcp protocol instead of netbird-ssh
2026-01-19 09:49:08 +01:00
229 changed files with 18181 additions and 4483 deletions

No files matched your search

+12 -1
View File
@@ -19,7 +19,7 @@ jobs:
- name: setup-node
uses: actions/setup-node@v3
with:
node-version: '18'
node-version: '20'
cache: 'npm'
- name: Install dependencies
@@ -54,8 +54,19 @@ jobs:
fileName: "ironrdp_web_bg.wasm"
out-file-path: 'public/ironrdp-pkg'
- name: Get version from tag
id: version
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
else
echo "version=development" >> $GITHUB_OUTPUT
fi
- name: Build
run: npm run build
env:
NEXT_PUBLIC_DASHBOARD_VERSION: ${{ steps.version.outputs.version }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
+2
View File
@@ -37,6 +37,8 @@ next-env.d.ts
# config
.local-config.json
.test-config.json
cypress.env.json
.configs/.local-config.zitadel.json
.configs/.staging-config.json
.configs/.temp-config.json
+1 -1
View File
@@ -1,7 +1,7 @@
## Contributor License Agreement
This Contributor License Agreement (referred to as the "Agreement") is entered into by the individual
submitting this Agreement and NetBird GmbH, c/o Max-Beer-StraĂźe 2-4 MĂĽnzstraĂźe 12 10178 Berlin, Germany,
submitting this Agreement and NetBird GmbH, BrunnenstraĂźe 196, 10119 Berlin, Germany,
referred to as "NetBird" (collectively, the "Parties"). The Agreement outlines the terms and conditions
under which NetBird may utilize software contributions provided by the Contributor for inclusion in
its software development projects. By submitting this Agreement, the Contributor confirms their acceptance
+11 -8
View File
@@ -10,6 +10,7 @@ See [NetBird repo](https://github.com/netbirdio/netbird)
The purpose of this project is simple - make it easy to manage VPN built with [NetBird](https://github.com/netbirdio/netbird).
The dashboard makes it possible to:
- track the status of your peers
- remove peers
- manage Setup Keys (to authenticate new peers)
@@ -17,10 +18,10 @@ The dashboard makes it possible to:
- define access controls
## Some Screenshots
<img src="./src/assets/screenshots/peers.png" alt="peers"/>
<img src="./src/assets/screenshots/add-peer.png" alt="add-peer"/>
## Technologies Used
- NextJS
@@ -33,8 +34,9 @@ The dashboard makes it possible to:
- Let's Encrypt
## How to run
Disclaimer. We believe that proper user management system is not a trivial task and requires quite some effort to make it right. Therefore we decided to
use Auth0 service that covers all our needs (user management, social login, JTW for the management API).
use Auth0 service that covers all our needs (user management, social login, JWT for the management API).
Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
1. Install [Docker](https://docs.docker.com/get-docker/)
@@ -43,9 +45,9 @@ Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
`AUTH0_DOMAIN` `AUTH0_CLIENT_ID` `AUTH0_AUDIENCE`
To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) up until "Configure Allowed Web Origins"
To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react) up until "Configure Allowed Web Origins"
4. NetBird UI Dashboard uses NetBirds Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
4. NetBird UI Dashboard uses NetBird's Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
5. Run docker container without SSL (Let's Encrypt):
```shell
@@ -54,9 +56,10 @@ Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
-e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> \
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMENT API URL> \
netbirdio/dashboard:main
```
6. Run docker container with SSL (Let's Encrypt):
```shell
@@ -68,7 +71,7 @@ Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
-e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> \
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMENT API URL> \
netbirdio/dashboard:main
```
@@ -84,11 +87,11 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
You can start editing by modifying the code inside `src/..`
The page auto-updates as you edit the file.
## How to migrate from old dashboard (v1)
## How to migrate from old dashboard (v1)
The new dashboard comes with a new docker image `netbirdio/dashboard:main`.
To migrate from the old dashboard (v1) `wiretrustee/dashboard:main` to the new one, please follow the steps below.
1. Stop the dashboard container `docker compose down dashboard`
2. Replace the docker image name in your `docker-compose.yml` with `netbirdio/dashboard:main`
3. Recreate the dashboard container `docker compose up -d --force-recreate dashboard`
3. Recreate the dashboard container `docker compose up -d --force-recreate dashboard`
+12
View File
@@ -0,0 +1,12 @@
[
{
"tag": "New",
"text": "NetBird Reverse Proxy - Expose internal services to the public with automatic TLS and optional authentication.",
"link": "https://docs.netbird.io/manage/reverse-proxy",
"linkText": "Learn more",
"variant": "important",
"isExternal": true,
"closeable": true,
"isCloudOnly": false
}
]
+2
View File
@@ -7,6 +7,8 @@ const nextConfig = {
reactStrictMode: false,
env: {
APP_ENV: process.env.APP_ENV || "production",
NEXT_PUBLIC_DASHBOARD_VERSION:
process.env.NEXT_PUBLIC_DASHBOARD_VERSION || "development",
},
};
+2142 -2339
View File
File diff suppressed because it is too large. Load diff
+37 -34
View File
@@ -2,6 +2,9 @@
"name": "netbird-dashboard",
"version": "2.0.0",
"private": true,
"engines": {
"node": ">=20.9.0"
},
"scripts": {
"copy": "copyfiles -f ./node_modules/@axa-fr/react-oidc/dist/OidcServiceWorker.js ./public",
"copytrusted": "copyfiles -f ./public/local/OidcTrustedDomains.js ./public",
@@ -13,34 +16,34 @@
"cypress:open": "cypress open"
},
"dependencies": {
"@axa-fr/react-oidc": "^7.22.18",
"@axa-fr/react-oidc": "^7.26.3",
"@dagrejs/dagre": "^1.1.5",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@tabler/icons-react": "^2.39.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@tabler/icons-react": "^3.36.1",
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.10.7",
"@types/crypto-js": "^4.2.2",
"@types/d3": "^7.4.3",
"@types/lodash": "^4.14.200",
"@types/node": "20.10.6",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-window": "^1.8.8",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
@@ -49,8 +52,9 @@
"chart.js": "^4.4.8",
"chroma-js": "^3.1.2",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"cmdk": "^1.1.1",
"crypto-js": "^4.2.0",
"d3": "^7.9.0",
"date-fns": "^2.30.0",
@@ -58,28 +62,27 @@
"elkjs": "^0.10.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"flowbite": "^1.8.1",
"flowbite-react": "^0.6.4",
"framer-motion": "^10.16.4",
"framer-motion": "^12.29.2",
"ip-address": "^10.1.0",
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"lucide-react": "^0.539.0",
"next": "^14.2.35",
"lodash": "^4.17.23",
"lucide-react": "^0.562.0",
"next": "^16.1.7",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^18.3.1",
"react-day-picker": "^8.9.1",
"react-dom": "^18.3.1",
"react": "^19.2.4",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.4",
"react-ga4": "^2.1.0",
"react-hot-toast": "^2.4.1",
"react-hotjar": "^6.2.0",
"react-hotjar": "^6.3.1",
"react-hotkeys-hook": "^4.4.1",
"react-icons": "^5.5.0",
"react-jwt": "^1.2.0",
"react-loading-skeleton": "^3.3.1",
"react-responsive": "^9.0.2",
"react-virtuoso": "^4.9.0",
"sonner": "^2.0.7",
"swr": "^2.2.4",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
@@ -91,7 +94,7 @@
"@types/chroma-js": "^3.1.1",
"@types/js-cookie": "^3.0.6",
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.5",
"eslint-config-next": "^16.1.6",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3.4.17"
@@ -6,5 +6,5 @@ import React from "react";
export default function Redirect() {
useRedirect("/events/audit");
return <FullScreenLoading height={"auto"} />;
return <FullScreenLoading fullScreen={false} />;
}
+1 -1
View File
@@ -11,5 +11,5 @@ export default function DNS() {
router.push("/dns/nameservers");
}, [router]);
return <FullScreenLoading height={"auto"} />;
return <FullScreenLoading fullScreen={false} />;
}
+80
View File
@@ -0,0 +1,80 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import dayjs from "dayjs";
import { ExternalLinkIcon } from "lucide-react";
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
import React, { useMemo } from "react";
import ActivityIcon from "@/assets/icons/ActivityIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
import ServerPaginationProvider from "@/contexts/ServerPaginationProvider";
import PageContainer from "@/layouts/PageContainer";
import ReverseProxyEventsTable from "@/modules/reverse-proxy/events/ReverseProxyEventsTable";
import { usePortalElement } from "@hooks/usePortalElement";
import { REVERSE_PROXY_EVENTS_DOCS_LINK } from "@/interfaces/ReverseProxy";
export default function ProxyEventsPage() {
const { permission } = usePermissions();
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
const defaultFilters = useMemo(
() => ({
start_date: dayjs().subtract(7, "day").startOf("day").toISOString(),
end_date: dayjs().endOf("day").toISOString(),
sort_by: "timestamp",
sort_order: "desc",
}),
[],
);
return (
<PageContainer>
<div className="p-default py-6">
<Breadcrumbs>
<Breadcrumbs.Item
label="Activity"
disabled
icon={<ActivityIcon size={13} />}
/>
<Breadcrumbs.Item
href="/events/proxy"
label="Proxy Events"
icon={<ReverseProxyIcon size={15} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Proxy Events</h1>
<Paragraph>
View access logs for your reverse proxy services, including allowed
and denied requests.
</Paragraph>
<Paragraph>
Learn more about{" "}
<InlineLink href={REVERSE_PROXY_EVENTS_DOCS_LINK} target="_blank">
Proxy Events <ExternalLinkIcon size={12} />
</InlineLink>{" "}
in our documentation.
</Paragraph>
</div>
<RestrictedAccess
page="Proxy Events"
hasAccess={permission?.services?.read}
>
<ServerPaginationProvider
url="/events/proxy"
defaultPageSize={10}
defaultFilters={defaultFilters}
>
<ReverseProxyEventsTable headingTarget={portalTarget} />
</ServerPaginationProvider>
</RestrictedAccess>
</PageContainer>
);
}
+1 -1
View File
@@ -61,7 +61,7 @@ export default function NetworkRoutes() {
in our documentation.
</Paragraph>
<Callout className={"max-w-xl mt-3"} variant={"warning"}>
<Callout className={"max-w-xl mt-5"} variant={"warning"}>
<span>
We recommend using the new Networks concept to easier visualise
and manage access to your resources.{" "}
+127 -54
View File
@@ -12,14 +12,13 @@ import {
} from "@components/DropdownMenu";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import Separator from "@components/Separator";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import { cn, singularize } from "@utils/helpers";
import {
ArrowUpRightIcon,
HelpCircle,
Layers3Icon,
MoreVertical,
PencilLineIcon,
ServerIcon,
@@ -28,19 +27,29 @@ import {
Trash2,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import React, { useMemo } from "react";
import useUrlTab from "@/hooks/useUrlTab";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { Network } from "@/interfaces/Network";
import { Network, NetworkResource, NetworkRouter } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
import { NetworkAccessControlProvider } from "@/modules/networks/NetworkAccessControlProvider";
import {
NetworkProvider,
useNetworksContext,
} from "@/modules/networks/NetworkProvider";
import { ResourcesSection } from "@/modules/networks/resources/ResourcesSection";
import { NetworkRoutingPeersSection } from "@/modules/networks/routing-peers/NetworkRoutingPeersSection";
import { ResourcesTabContent } from "@/modules/networks/resources/ResourcesTabContent";
import { NetworkRoutingPeersTabContent } from "@/modules/networks/routing-peers/NetworkRoutingPeersTabContent";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
import PeerIcon from "@/assets/icons/PeerIcon";
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
import ReverseProxiesProvider, {
flattenReverseProxies,
useReverseProxies,
} from "@/contexts/ReverseProxiesProvider";
import { SkeletonNetwork } from "@components/skeletons/SkeletonNetwork";
export default function NetworkDetailPage() {
const queryParameter = useSearchParams();
@@ -53,17 +62,34 @@ export default function NetworkDetailPage() {
useRedirect("/networks", false, !networkId);
return network && !isLoading ? (
<NetworkOverview network={network} />
<ReverseProxiesProvider initialNetwork={network}>
<NetworkOverview network={network} />
</ReverseProxiesProvider>
) : (
<FullScreenLoading />
<SkeletonNetwork />
);
}
function NetworkOverview({ network }: Readonly<{ network: Network }>) {
const { permission } = usePermissions();
const [networkModal, setNetworkModal] = useState(false);
const { mutate } = useSWRConfig();
const { data: resources, isLoading: isResourcesLoading } = useFetchApi<
NetworkResource[]
>(`/networks/${network.id}/resources`);
const { data: routers, isLoading: isRoutersLoading } = useFetchApi<
NetworkRouter[]
>(`/networks/${network.id}/routers`);
const { reverseProxies, isLoading: isServicesLoading } = useReverseProxies();
const services = useMemo(
() => flattenReverseProxies({ reverseProxies, network }),
[reverseProxies, network],
);
const [tab, setTab] = useUrlTab(
["resources", "routing-peers", "services"],
"resources",
);
const isActive = !!(
network?.routing_peers_count && network.routing_peers_count > 0
@@ -71,56 +97,103 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
return (
<PageContainer>
<NetworkProvider network={network}>
<div className={"p-default py-6 mb-4"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
disabled={!permission.networks.read}
icon={<NetworkRoutesIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/network"}
label={network.name}
active={true}
/>
</Breadcrumbs>
<NetworkAccessControlProvider>
<NetworkProvider network={network}>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/networks"}
label={"Networks"}
disabled={!permission.networks.read}
icon={<NetworkRoutesIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/network"}
label={network.name}
active={true}
/>
</Breadcrumbs>
<div className={"flex justify-between max-w-6xl"}>
<div
className={"w-full lg:w-1/2 flex justify-between items-center"}
>
<div className={"flex justify-between max-w-6xl"}>
<div
className={cn(
"flex items-center w-full",
!network.description && "gap-2",
)}
className={"w-full lg:w-1/2 flex justify-between items-center"}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
</div>
<NetworkProvider network={network}>
<div
className={cn(
"flex items-center w-full",
!network.description && "gap-2",
)}
>
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
</div>
<NetworkActions />
</NetworkProvider>
</div>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
</div>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
</div>
</div>
<Tabs
defaultValue={tab}
onValueChange={setTab}
value={tab}
className={"pb-0 mb-0"}
>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"resources"}>
<Layers3Icon size={14} />
{singularize("Resources", network?.resources?.length)}
</TabsTrigger>
<TabsTrigger value={"routing-peers"}>
<PeerIcon
size={12}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Routing Peers", network?.routing_peers_count)}
</TabsTrigger>
<TabsTrigger value={"services"}>
<ReverseProxyIcon
size={16}
className={
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
}
/>
{singularize("Services", services.length)}
</TabsTrigger>
</TabsList>
<Separator />
<ResourcesSection network={network} />
<div className={"h-3"} />
<Separator />
<NetworkRoutingPeersSection network={network} />
</NetworkProvider>
<TabsContent value={"resources"} className={"pb-8"}>
<ResourcesTabContent
data={resources}
isLoading={isResourcesLoading}
/>
</TabsContent>
<TabsContent value={"routing-peers"} className={"pb-8"}>
<NetworkRoutingPeersTabContent
routers={routers}
isLoading={isRoutersLoading}
/>
</TabsContent>
<TabsContent value={"services"} className={"pb-8"}>
<ReverseProxyFlatTargetsTabContent
targets={services}
isLoading={isServicesLoading}
/>
</TabsContent>
</Tabs>
</NetworkProvider>
</NetworkAccessControlProvider>
</PageContainer>
);
}
+226 -105
View File
@@ -26,6 +26,7 @@ import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { singularize } from "@utils/helpers";
import dayjs from "dayjs";
import { isEmpty, trim } from "lodash";
import {
@@ -36,11 +37,14 @@ import {
FlagIcon,
Globe,
History,
ListIcon,
MapPin,
MonitorSmartphoneIcon,
NetworkIcon,
PencilIcon,
RadioTowerIcon,
} from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { toASCII } from "punycode";
import React, { useMemo, useState } from "react";
@@ -50,20 +54,27 @@ import RoundedFlag from "@/assets/countries/RoundedFlag";
import CircleIcon from "@/assets/icons/CircleIcon";
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
import PeerIcon from "@/assets/icons/PeerIcon";
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
import { useCountries } from "@/contexts/CountryProvider";
import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { useHasChanges } from "@/hooks/useHasChanges";
import type { Group } from "@/interfaces/Group";
import type { Peer } from "@/interfaces/Peer";
import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
import { PeerRemoteJobsSection } from "@/modules/peer/PeerRemoteJobsSection";
import ReverseProxiesProvider, {
flattenReverseProxies,
useReverseProxies,
} from "@/contexts/ReverseProxiesProvider";
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
import Link from "next/link";
import { PeerExpirationSettings } from "@/modules/peer/PeerExpirationSettings";
export default function PeerPage() {
@@ -96,10 +107,12 @@ export default function PeerPage() {
/>
);
return peer && !isLoading ? (
<PeerProvider peer={peer} key={peerId} isPeerDetailPage={true}>
<PeerOverview key={peer?.id} />
</PeerProvider>
return peer && peer.id && !isLoading ? (
<ReverseProxiesProvider initialPeer={peer}>
<PeerProvider peer={peer} key={peerId} isPeerDetailPage={true}>
<PeerOverview key={peer?.id} />
</PeerProvider>
</ReverseProxiesProvider>
) : (
<FullScreenLoading />
);
@@ -111,38 +124,60 @@ function PeerOverview() {
return (
<PageContainer>
<RoutesProvider>
<div className={"p-default py-6 pb-0"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/peers"}
label={"Peers"}
icon={<PeerIcon size={13} />}
/>
<Breadcrumbs.Item label={peer.ip} active />
</Breadcrumbs>
<PeerGeneralInformation />
</div>
<PeerOverviewTabs />
<PeerSettingsProvider>
<div className={"p-default py-6 pb-0"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/peers"}
label={"Peers"}
icon={<PeerIcon size={13} />}
/>
<Breadcrumbs.Item label={peer.ip} active />
</Breadcrumbs>
<PeerHeader />
</div>
<PeerOverviewTabs />
</PeerSettingsProvider>
</RoutesProvider>
</PageContainer>
);
}
const PeerGeneralInformation = () => {
const router = useRouter();
type PeerSettingsContextType = {
selectedGroups: Group[];
setSelectedGroups: React.Dispatch<React.SetStateAction<Group[]>>;
hasChanges: boolean;
updatePeer: (newName?: string) => Promise<void>;
name: string;
setName: (name: string) => void;
tab: string;
setTab: (tab: string) => void;
};
const PeerSettingsContext = React.createContext<PeerSettingsContextType | null>(
null,
);
const usePeerSettings = () => {
const context = React.useContext(PeerSettingsContext);
if (!context) {
throw new Error("usePeerSettings must be used within PeerSettingsProvider");
}
return context;
};
const PeerSettingsProvider = ({ children }: { children: React.ReactNode }) => {
const { mutate } = useSWRConfig();
const { peer, user, peerGroups, update } = usePeer();
const { peer, peerGroups, update } = usePeer();
const { permission } = usePermissions();
const [name, setName] = useState(peer.name);
const [showEditNameModal, setShowEditNameModal] = useState(false);
const [tab, setTab] = useState("overview");
const [selectedGroups, setSelectedGroups, { getAllGroupCalls }] =
useGroupHelper({
initial: peerGroups?.filter((g) => g?.name !== "All"),
peer,
});
/**
* Detect if there are changes in the peer information, if there are changes, then enable the save button.
*/
const { hasChanges, updateRef: updateHasChangedRef } = useHasChanges([
selectedGroups,
]);
@@ -172,7 +207,31 @@ const PeerGeneralInformation = () => {
});
};
return (
<PeerSettingsContext.Provider
value={{
selectedGroups,
setSelectedGroups,
hasChanges,
updatePeer,
name,
setName,
tab,
setTab,
}}
>
{children}
</PeerSettingsContext.Provider>
);
};
const PeerHeader = () => {
const router = useRouter();
const { peer, user } = usePeer();
const { permission } = usePermissions();
const { name, setName, hasChanges, updatePeer, tab } = usePeerSettings();
const [showEditNameModal, setShowEditNameModal] = useState(false);
const isOverviewTab = tab === "overview";
return (
<>
@@ -233,49 +292,145 @@ const PeerGeneralInformation = () => {
</div>
)}
</div>
<div className={"flex gap-4"}>
<Button
variant={"default"}
className={"w-full"}
onClick={() => router.push("/peers")}
>
Cancel
</Button>
<Button
variant={"primary"}
className={"w-full"}
onClick={() => updatePeer()}
disabled={
!hasChanges || !permission.peers.read || !permission.groups.update
}
>
Save Changes
</Button>
</div>
{isOverviewTab && (
<div className={"flex gap-4"}>
<Button
variant={"default"}
className={"w-full"}
onClick={() => router.push("/peers")}
>
Cancel
</Button>
<Button
variant={"primary"}
className={"w-full"}
onClick={() => updatePeer()}
disabled={
!hasChanges ||
!permission.peers.update ||
!permission.groups.update
}
>
Save Changes
</Button>
</div>
)}
</div>
</>
);
};
const PeerOverviewTabs = () => {
const { peer } = usePeer();
const { permission } = usePermissions();
const { reverseProxies, isLoading: isServicesLoading } = useReverseProxies();
const { tab, setTab } = usePeerSettings();
const flatTargets = useMemo(
() => flattenReverseProxies({ reverseProxies, peer }),
[reverseProxies, peer],
);
return (
<Tabs
defaultValue={tab}
onValueChange={setTab}
value={tab}
className={"pt-4 pb-0 mb-0"}
>
<TabsList justify={"start"} className={"px-8"}>
<TabsTrigger value={"overview"}>
<ListIcon size={16} />
Overview
</TabsTrigger>
{permission.routes.read && (
<TabsTrigger value={"network-routes"}>
<NetworkIcon size={16} />
Network Routes
</TabsTrigger>
)}
{peer?.id && permission.peers.read && (
<TabsTrigger value={"accessible-peers"}>
<MonitorSmartphoneIcon size={16} />
Accessible Peers
</TabsTrigger>
)}
{peer?.id && permission.services?.read && (
<TabsTrigger value={"reverse-proxies"}>
<ReverseProxyIcon
size={16}
className="fill-nb-gray-400 group-data-[state=active]/trigger:fill-netbird"
/>
{singularize("Services", flatTargets.length)}
</TabsTrigger>
)}
{peer?.id && permission.peers.delete && (
<TabsTrigger value={"peer-job"}>
<RadioTowerIcon size={16} />
Remote Jobs
</TabsTrigger>
)}
</TabsList>
<TabsContent value={"overview"} className={"pb-8"}>
<PeerOverviewTabContent />
</TabsContent>
{permission.routes.read && (
<TabsContent value={"network-routes"} className={"pb-8"}>
<PeerNetworkRoutesSection peer={peer} />
</TabsContent>
)}
{peer?.id && permission.peers.read && (
<TabsContent value={"accessible-peers"} className={"pb-8"}>
<AccessiblePeersSection peerID={peer.id} />
</TabsContent>
)}
{peer?.id && permission.services?.read && (
<TabsContent value={"reverse-proxies"} className={"pb-8"}>
<ReverseProxyFlatTargetsTabContent
targets={flatTargets}
isLoading={isServicesLoading}
hideResourceColumn
emptyTableTitle={"This peer has no services"}
emptyTableDescription={
"Add your services to this peer and securely expose them through NetBird's reverse proxy"
}
/>
</TabsContent>
)}
{peer.id && permission.peers.delete && (
<TabsContent value={"peer-job"} className={"pb-8"}>
<PeerRemoteJobsSection peerID={peer.id} />
</TabsContent>
)}
</Tabs>
);
};
const PeerOverviewTabContent = () => {
const { peer } = usePeer();
const { permission } = usePermissions();
const { selectedGroups, setSelectedGroups } = usePeerSettings();
return (
<div className={"px-8"}>
<div
className={
"flex-wrap xl:flex-nowrap flex gap-10 w-full mt-5 max-w-6xl items-start"
"flex-wrap xl:flex-nowrap flex gap-10 w-full items-start pt-2 max-w-6xl"
}
>
<PeerInformationCard peer={peer} />
<div className={"flex flex-col gap-6 lg:w-1/2 transition-all"}>
<div className={"flex flex-col gap-8 lg:w-1/2 transition-all"}>
<PeerExpirationSettings />
<PeerSSHToggle />
{/* Remote Access Buttons */}
<div>
<Label>Remote Access</Label>
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
<div className="flex gap-3">
<SSHButton peer={peer} />
<RDPButton peer={peer} />
</div>
</div>
{permission.groups.read && (
<div>
<Label>Assigned Groups</Label>
@@ -291,55 +446,21 @@ const PeerGeneralInformation = () => {
/>
</div>
)}
<PeerSSHToggle />
{/* Remote Access Buttons */}
<div>
<Label>Remote Access</Label>
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
<div className="flex gap-3">
<SSHButton peer={peer} />
<RDPButton peer={peer} />
</div>
</div>
</div>
</div>
</>
);
};
const PeerOverviewTabs = () => {
const { peer } = usePeer();
const { permission } = usePermissions();
const [tab, setTab] = useState(
permission.routes.read ? "network-routes" : "accessible-peers",
);
return (
<Tabs
defaultValue={tab}
onValueChange={(v) => setTab(v)}
value={tab}
className={"pt-10 pb-0 mb-0"}
>
<TabsList justify={"start"} className={"px-8"}>
{permission.routes.read && (
<TabsTrigger value={"network-routes"}>
<NetworkIcon size={16} />
Network Routes
</TabsTrigger>
)}
{peer?.id && permission.peers.read && (
<TabsTrigger value={"accessible-peers"}>
<MonitorSmartphoneIcon size={16} />
Accessible Peers
</TabsTrigger>
)}
</TabsList>
{permission.routes.read && (
<TabsContent value={"network-routes"} className={"pb-8"}>
<PeerNetworkRoutesSection peer={peer} />
</TabsContent>
)}
{peer?.id && permission.peers.read && (
<TabsContent value={"accessible-peers"} className={"pb-8"}>
<AccessiblePeersSection peerID={peer.id} />
</TabsContent>
)}
</Tabs>
</div>
);
};
+1 -1
View File
@@ -105,7 +105,7 @@ function PeersBlockedView() {
<div className={"px-3 pt-1 pb-8 max-w-3xl w-full"}>
<div
className={
"rounded-md border border-nb-gray-900/70 grid w-full bg-nb-gray-930/40"
"rounded-md border border-nb-gray-900/70 grid w-full bg-nb-gray-930/40 stepper-bg-variant"
}
>
<SetupModalContent header={false} footer={false} />
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Custom Domains - Reverse Proxy - ${globalMetaTitle}`,
};
export default BlankLayout;
@@ -0,0 +1,70 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
import ReverseProxiesProvider from "@/contexts/ReverseProxiesProvider";
import { REVERSE_PROXY_CUSTOM_DOMAINS_DOCS_LINK } from "@/interfaces/ReverseProxy";
import PageContainer from "@/layouts/PageContainer";
const CustomDomainsTable = lazy(
() => import("@/modules/reverse-proxy/domain/CustomDomainsTable"),
);
export default function ReverseProxyCustomDomainsPage() {
const { permission } = usePermissions();
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/reverse-proxy/services"}
label={"Reverse Proxy"}
icon={<ReverseProxyIcon size={16} />}
/>
<Breadcrumbs.Item
href={"/reverse-proxy/custom-domains"}
label={"Custom Domains"}
active={true}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Domains</h1>
<Paragraph>
Add and manage custom domains for your reverse proxy services.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink
href={REVERSE_PROXY_CUSTOM_DOMAINS_DOCS_LINK}
target={"_blank"}
>
Custom Domains
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess
page={"Custom Domains"}
hasAccess={permission?.services?.read}
>
<ReverseProxiesProvider>
<Suspense fallback={<SkeletonTable />}>
<CustomDomainsTable headingTarget={portalTarget} />
</Suspense>
</ReverseProxiesProvider>
</RestrictedAccess>
</PageContainer>
);
}
@@ -0,0 +1,15 @@
"use client";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function ReverseProxyRedirectPage() {
const router = useRouter();
useEffect(() => {
router.replace("/reverse-proxy/services");
}, [router]);
return <FullScreenLoading fullScreen={false} />;
}
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Services - Reverse Proxy - ${globalMetaTitle}`,
};
export default BlankLayout;
@@ -0,0 +1,83 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import InlineLink from "@components/InlineLink";
import Paragraph from "@components/Paragraph";
import SkeletonTable from "@components/skeletons/SkeletonTable";
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { usePortalElement } from "@hooks/usePortalElement";
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
import ReverseProxiesProvider from "@/contexts/ReverseProxiesProvider";
import { REVERSE_PROXY_DOCS_LINK } from "@/interfaces/ReverseProxy";
import PageContainer from "@/layouts/PageContainer";
import { Callout } from "@components/Callout";
import { isNetBirdHosted } from "@utils/netbird";
const ReverseProxyTable = lazy(
() => import("@/modules/reverse-proxy/table/ReverseProxyTable"),
);
export default function ReverseProxyServicesPage() {
const { permission } = usePermissions();
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<PageContainer>
<div className={"p-default py-6"}>
<Breadcrumbs>
<Breadcrumbs.Item
href={"/reverse-proxy/services"}
label={"Reverse Proxy"}
icon={<ReverseProxyIcon size={16} />}
/>
<Breadcrumbs.Item
href={"/reverse-proxy/services"}
label={"Services"}
active={true}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Services</h1>
<Paragraph>
Expose services securely through NetBird&apos;s reverse proxy.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink href={REVERSE_PROXY_DOCS_LINK} target={"_blank"}>
Services
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
{isNetBirdHosted() ? (
<Callout className={"max-w-xl mt-5"} variant={"info"}>
NetBird&apos;s Reverse Proxy is currently in beta and available at
no cost during this period. Features, functionality, and pricing are
subject to change upon release.
</Callout>
) : (
<Callout className={"max-w-xl mt-5"} variant={"info"}>
NetBird&apos;s Reverse Proxy is currently in beta. <br /> Features
and functionality are subject to change upon release.
</Callout>
)}
</div>
<RestrictedAccess
page={"Services"}
hasAccess={permission?.services?.read}
>
<ReverseProxiesProvider>
<Suspense fallback={<SkeletonTable />}>
<ReverseProxyTable headingTarget={portalTarget} />
</Suspense>
</ReverseProxiesProvider>
</RestrictedAccess>
</PageContainer>
);
}
Loaded 20 of 229 files, more files were not shown because too many files have changed in this diff. Show more