mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8c6d44452 |
No files matched your search
Generated
+10
-19
@@ -58,6 +58,7 @@
|
||||
"ip-address": "^10.1.0",
|
||||
"ip-cidr": "^3.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "16.1.7",
|
||||
@@ -85,6 +86,7 @@
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@types/chroma-js": "^3.1.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"postcss": "^8",
|
||||
@@ -168,7 +170,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -2998,6 +2999,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -3031,7 +3039,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
||||
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -3041,7 +3048,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -3100,7 +3106,6 @@
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
@@ -3581,8 +3586,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.10.0",
|
||||
@@ -3621,7 +3625,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4044,7 +4047,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4667,7 +4669,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -5196,7 +5197,6 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5394,7 +5394,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -6688,7 +6687,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -7446,7 +7444,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -7654,7 +7651,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7695,7 +7691,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -8590,7 +8585,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -8757,7 +8751,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -8923,7 +8916,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -9251,7 +9243,6 @@
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"ip-address": "^10.1.0",
|
||||
"ip-cidr": "^3.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "16.1.7",
|
||||
@@ -96,6 +97,7 @@
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@types/chroma-js": "^3.1.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"postcss": "^8",
|
||||
|
||||
+1
-1
@@ -3,6 +3,6 @@ import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Servers - ${globalMetaTitle}`,
|
||||
title: `Inspection Policies - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
@@ -0,0 +1,61 @@
|
||||
"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 useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon, ShieldCheck } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import PoliciesProvider from "@/contexts/PoliciesProvider";
|
||||
import { InspectionPolicy } from "@/interfaces/Network";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const InspectionPoliciesTable = lazy(
|
||||
() =>
|
||||
import(
|
||||
"@/modules/networks/inspection/InspectionPoliciesTable"
|
||||
),
|
||||
);
|
||||
|
||||
export default function InspectionPoliciesPage() {
|
||||
const { permission } = usePermissions();
|
||||
const { data: policies, isLoading } =
|
||||
useFetchApi<InspectionPolicy[]>("/inspection-policies");
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/access-control"}
|
||||
label={"Access Control"}
|
||||
icon={<AccessControlIcon size={14} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/access-control/inspection-policies"}
|
||||
label={"Inspection Policies"}
|
||||
icon={<ShieldCheck size={15} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>Inspection Policies</h1>
|
||||
<Paragraph>
|
||||
Create reusable inspection rule sets for transparent proxy traffic
|
||||
inspection. Attach them to access control policies to inspect traffic
|
||||
flowing through routing peers.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<PoliciesProvider>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<InspectionPoliciesTable
|
||||
policies={policies ?? []}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Suspense>
|
||||
</PoliciesProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { ExternalLinkIcon } from "lucide-react";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import React, { useMemo } from "react";
|
||||
import ActivityIcon from "@/assets/icons/ActivityIcon";
|
||||
import PeersProvider from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import ServerPaginationProvider from "@/contexts/ServerPaginationProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
@@ -73,9 +72,7 @@ export default function ProxyEventsPage() {
|
||||
defaultPageSize={10}
|
||||
defaultFilters={defaultFilters}
|
||||
>
|
||||
<PeersProvider>
|
||||
<ReverseProxyEventsTable headingTarget={portalTarget} />
|
||||
</PeersProvider>
|
||||
<ReverseProxyEventsTable headingTarget={portalTarget} />
|
||||
</ServerPaginationProvider>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
|
||||
@@ -526,7 +526,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"NetBird IP Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} className={"shrink-0"} />
|
||||
<MapPin size={16} />
|
||||
NetBird IP Address
|
||||
</>
|
||||
}
|
||||
@@ -547,7 +547,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"NetBird IPv6 Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} className={"shrink-0"} />
|
||||
<MapPin size={16} />
|
||||
NetBird IPv6 Address
|
||||
</>
|
||||
}
|
||||
@@ -567,7 +567,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"Public IP Address"}
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} className={"shrink-0"} />
|
||||
<NetworkIcon size={16} />
|
||||
Public IP Address
|
||||
</>
|
||||
}
|
||||
@@ -579,7 +579,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"DNS label"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} className={"shrink-0"} />
|
||||
<Globe size={16} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
@@ -597,7 +597,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"Hostname"}
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} className={"shrink-0"} />
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Hostname
|
||||
</>
|
||||
}
|
||||
@@ -607,7 +607,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<FlagIcon size={16} className={"shrink-0"} />
|
||||
<FlagIcon size={16} />
|
||||
Region
|
||||
</>
|
||||
}
|
||||
@@ -637,7 +637,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Cpu size={16} className={"shrink-0"} />
|
||||
<Cpu size={16} />
|
||||
Operating System
|
||||
</>
|
||||
}
|
||||
@@ -648,7 +648,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Barcode size={16} className={"shrink-0"} />
|
||||
<Barcode size={16} />
|
||||
Serial Number
|
||||
</>
|
||||
}
|
||||
@@ -660,7 +660,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<CalendarDays size={16} className={"shrink-0"} />
|
||||
<CalendarDays size={16} />
|
||||
Registered on
|
||||
</>
|
||||
}
|
||||
@@ -676,7 +676,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<History size={16} className={"shrink-0"} />
|
||||
<History size={16} />
|
||||
Last seen
|
||||
</>
|
||||
}
|
||||
@@ -693,7 +693,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} className={"shrink-0"} />
|
||||
<NetBirdIcon size={16} />
|
||||
Agent Version
|
||||
</>
|
||||
}
|
||||
@@ -704,7 +704,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} className={"shrink-0"} />
|
||||
<NetBirdIcon size={16} />
|
||||
UI Version
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,116 @@
|
||||
import { redirect } from "next/navigation";
|
||||
"use client";
|
||||
|
||||
export default function PeersIndex() {
|
||||
redirect("/peers/users");
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import PeersProvider, { usePeers } from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
|
||||
|
||||
const PeersTable = lazy(() => import("@/modules/peers/PeersTable"));
|
||||
|
||||
export default function Peers() {
|
||||
const { isRestricted } = usePermissions();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{isRestricted ? (
|
||||
<PeersBlockedView />
|
||||
) : (
|
||||
<PeersProvider>
|
||||
<PeersView />
|
||||
</PeersProvider>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function PeersView() {
|
||||
const { peers, isLoading } = usePeers();
|
||||
const { users } = useUsers();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
const peersWithUser = peers?.map((peer) => {
|
||||
if (!users) return peer;
|
||||
return {
|
||||
...peer,
|
||||
user: users?.find((user) => user.id === peer.user_id),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/peers"}
|
||||
label={"Peers"}
|
||||
icon={<PeerIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Peers</h1>
|
||||
<Paragraph>
|
||||
A list of all machines and devices connected to your private network.
|
||||
Use this view to manage peers.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/add-machines-to-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Peers
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable
|
||||
isLoading={isLoading}
|
||||
peers={peersWithUser}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PeersBlockedView() {
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>Add new device to your network</h1>
|
||||
<Paragraph className={"inline"}>
|
||||
To get started, install NetBird and log in using your email account.
|
||||
After that you should be connected. If you have further questions
|
||||
check out our{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Installation Guide
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<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 stepper-bg-variant"
|
||||
}
|
||||
>
|
||||
<SetupModalContent header={false} footer={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense, useMemo } from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import PeersProvider, { usePeers } from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
|
||||
|
||||
const PeersTable = lazy(() => import("@/modules/peers/PeersTable"));
|
||||
|
||||
export default function ServersPage() {
|
||||
const { isRestricted } = usePermissions();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{isRestricted ? (
|
||||
<ServersBlockedView />
|
||||
) : (
|
||||
<PeersProvider>
|
||||
<ServersView />
|
||||
</PeersProvider>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function ServersView() {
|
||||
const { peers, isLoading: isPeersLoading } = usePeers();
|
||||
const { users, isLoading: isUsersLoading } = useUsers();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
// The kind filter classifies peers by whether their owner is a real
|
||||
// user vs a service/no-user, so we must wait until both peers and
|
||||
// users have loaded before joining them — otherwise peers temporarily
|
||||
// render with peer.user === undefined and get misclassified.
|
||||
const isLoading = isPeersLoading || isUsersLoading;
|
||||
const peersWithUser = useMemo(() => {
|
||||
if (!peers || !users) return undefined;
|
||||
return peers.map((peer) => ({
|
||||
...peer,
|
||||
user: users.find((u) => u.id === peer.user_id),
|
||||
}));
|
||||
}, [peers, users]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
label={"Peers"}
|
||||
icon={<PeerIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/peers/servers"}
|
||||
label={"Servers"}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Servers</h1>
|
||||
<Paragraph>
|
||||
Servers, VMs, autonomous agents and other unattended machines with no
|
||||
user behind them, typically enrolled with a{" "}
|
||||
<InlineLink href={"/settings?tab=setup-keys"}>setup key.</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable
|
||||
isLoading={isLoading}
|
||||
peers={peersWithUser}
|
||||
headingTarget={portalTarget}
|
||||
kind={"servers"}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ServersBlockedView() {
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>Add new server to your network</h1>
|
||||
<Paragraph className={"inline"}>
|
||||
To get started, install NetBird on the server and enroll it using a
|
||||
setup key. If you have further questions check out our{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Installation Guide
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<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 stepper-bg-variant"
|
||||
}
|
||||
>
|
||||
<SetupModalContent header={false} footer={false} isUserDevice={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `User Devices - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
@@ -1,122 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense, useMemo } from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import PeersProvider, { usePeers } from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
|
||||
|
||||
const PeersTable = lazy(() => import("@/modules/peers/PeersTable"));
|
||||
|
||||
export default function UserDevicesPage() {
|
||||
const { isRestricted } = usePermissions();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{isRestricted ? (
|
||||
<UserDevicesBlockedView />
|
||||
) : (
|
||||
<PeersProvider>
|
||||
<UserDevicesView />
|
||||
</PeersProvider>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function UserDevicesView() {
|
||||
const { peers, isLoading: isPeersLoading } = usePeers();
|
||||
const { users, isLoading: isUsersLoading } = useUsers();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
// The kind filter classifies peers by whether their owner is a real
|
||||
// user vs a service/no-user, so we must wait until both peers and
|
||||
// users have loaded before joining them — otherwise peers temporarily
|
||||
// render with peer.user === undefined and get misclassified.
|
||||
const isLoading = isPeersLoading || isUsersLoading;
|
||||
const peersWithUser = useMemo(() => {
|
||||
if (!peers || !users) return undefined;
|
||||
return peers.map((peer) => ({
|
||||
...peer,
|
||||
user: users.find((u) => u.id === peer.user_id),
|
||||
}));
|
||||
}, [peers, users]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
label={"Peers"}
|
||||
icon={<PeerIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/peers/users"}
|
||||
label={"User Devices"}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>User Devices</h1>
|
||||
<Paragraph>
|
||||
Laptops, phones and other personal devices with a user behind them,
|
||||
typically added when the user signs in with SSO.{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/add-machines-to-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable
|
||||
isLoading={isLoading}
|
||||
peers={peersWithUser}
|
||||
headingTarget={portalTarget}
|
||||
kind={"users"}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UserDevicesBlockedView() {
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>Add new device to your network</h1>
|
||||
<Paragraph className={"inline"}>
|
||||
To get started, install NetBird and log in using your email account.
|
||||
After that you should be connected. If you have further questions
|
||||
check out our{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Installation Guide
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<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 stepper-bg-variant"
|
||||
}
|
||||
>
|
||||
<SetupModalContent header={false} footer={false} isUserDevice />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Clusters - Reverse Proxy - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
@@ -1,66 +0,0 @@
|
||||
"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 { REVERSE_PROXY_CLUSTERS_DOCS_LINK } from "@/interfaces/ReverseProxy";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const ClustersTable = lazy(
|
||||
() => import("@/modules/reverse-proxy/clusters/ClustersTable"),
|
||||
);
|
||||
|
||||
export default function ReverseProxyClustersPage() {
|
||||
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/clusters"}
|
||||
label={"Clusters"}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Clusters</h1>
|
||||
<Paragraph>
|
||||
Proxy clusters that route inbound traffic to your services. Shared
|
||||
clusters are deployed at the server level; account clusters are
|
||||
self-hosted on your own infrastructure.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink href={REVERSE_PROXY_CLUSTERS_DOCS_LINK} target={"_blank"}>
|
||||
Clusters
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess
|
||||
page={"Clusters"}
|
||||
hasAccess={permission?.services?.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ClustersTable headingTarget={portalTarget} />
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
@@ -24,7 +23,6 @@ import DangerZoneTab from "@/modules/settings/DangerZoneTab";
|
||||
import IdentityProvidersTab from "@/modules/settings/IdentityProvidersTab";
|
||||
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
|
||||
import PermissionsTab from "@/modules/settings/PermissionsTab";
|
||||
import SetupKeysTab from "@/modules/settings/SetupKeysTab";
|
||||
import GroupsSettings from "@/modules/settings/GroupsSettings";
|
||||
|
||||
export default function NetBirdSettings() {
|
||||
@@ -57,12 +55,6 @@ export default function NetBirdSettings() {
|
||||
<ShieldIcon size={14} />
|
||||
Authentication
|
||||
</VerticalTabs.Trigger>
|
||||
{permission.setup_keys.read && (
|
||||
<VerticalTabs.Trigger value="setup-keys">
|
||||
<SetupKeysIcon size={14} />
|
||||
Setup Keys
|
||||
</VerticalTabs.Trigger>
|
||||
)}
|
||||
{account?.settings?.embedded_idp_enabled &&
|
||||
permission?.identity_providers?.read && (
|
||||
<VerticalTabs.Trigger value="identity-providers">
|
||||
@@ -97,7 +89,6 @@ export default function NetBirdSettings() {
|
||||
>
|
||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||
{account && <AuthenticationTab account={account} />}
|
||||
{permission.setup_keys.read && <SetupKeysTab />}
|
||||
{account?.settings?.embedded_idp_enabled &&
|
||||
permission.identity_providers.read && <IdentityProvidersTab />}
|
||||
{account && <PermissionsTab account={account} />}
|
||||
|
||||
@@ -1,5 +1,90 @@
|
||||
import { redirect } from "next/navigation";
|
||||
"use client";
|
||||
|
||||
export default function SetupKeysIndex() {
|
||||
redirect("/settings?tab=setup-keys");
|
||||
}
|
||||
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 useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense, useMemo } from "react";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { SetupKey } from "@/interfaces/SetupKey";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const SetupKeysTable = lazy(
|
||||
() => import("@/modules/setup-keys/SetupKeysTable"),
|
||||
);
|
||||
|
||||
export default function SetupKeys() {
|
||||
const { data: setupKeys, isLoading } = useFetchApi<SetupKey[]>("/setup-keys");
|
||||
const { permission } = usePermissions();
|
||||
const { groups } = useGroups();
|
||||
|
||||
const setupKeysWithGroups = useMemo(() => {
|
||||
if (!setupKeys) return [];
|
||||
return setupKeys?.map((setupKey) => {
|
||||
if (!setupKey.auto_groups) return setupKey;
|
||||
if (!groups) return setupKey;
|
||||
return {
|
||||
...setupKey,
|
||||
groups: setupKey.auto_groups
|
||||
?.map((group) => {
|
||||
return groups.find((g) => g.id === group) || undefined;
|
||||
})
|
||||
.filter((group) => group !== undefined) as Group[],
|
||||
};
|
||||
});
|
||||
}, [setupKeys, groups]);
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/setup-keys"}
|
||||
label={"Setup Keys"}
|
||||
icon={<SetupKeysIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Setup Keys</h1>
|
||||
<Paragraph>
|
||||
Setup keys are pre-authentication keys that allow to register new
|
||||
machines in your network.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Setup Keys
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess
|
||||
page={"Setup Keys"}
|
||||
hasAccess={permission.setup_keys.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<SetupKeysTable
|
||||
headingTarget={portalTarget}
|
||||
setupKeys={setupKeysWithGroups}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
/**
|
||||
* Tests for globals.css CSS variable definitions.
|
||||
*
|
||||
* The PR introduced a two-ramp CSS-variable system for nb-gray:
|
||||
* - :root defines the INVERTED ramp (light mode — high numbers = light,
|
||||
* low numbers = dark text)
|
||||
* - .dark defines the ORIGINAL ramp (dark mode — original values)
|
||||
*
|
||||
* These tests parse the raw CSS file as a string and verify:
|
||||
* 1. :root defines every nb-gray custom property.
|
||||
* 2. .dark defines every nb-gray custom property.
|
||||
* 3. The light and dark ramps are genuinely different (inverted) for key stops.
|
||||
* 4. All values are space-separated RGB triples (not hex, not rgb()).
|
||||
* 5. h1, h2, p now use theme-agnostic text classes (not dark:-prefixed).
|
||||
*
|
||||
* Run with: npx tsx src/app/globals-css.test.ts
|
||||
*/
|
||||
|
||||
import { readFileSync } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dir = dirname(__filename);
|
||||
const css = readFileSync(join(__dir, "globals.css"), "utf-8");
|
||||
|
||||
// ─── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extract the value of a CSS custom property from a given block of CSS text.
|
||||
* Returns the trimmed raw value string, or null if not found.
|
||||
*/
|
||||
function extractVar(cssText: string, varName: string): string | null {
|
||||
// matches "--varName: VALUE;"
|
||||
const regex = new RegExp(`${varName}\\s*:\\s*([^;]+);`);
|
||||
const match = cssText.match(regex);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the text content between the first occurrence of `selector {` and
|
||||
* its matching closing `}`. NaĂŻve but sufficient for the flat blocks in
|
||||
* globals.css.
|
||||
*/
|
||||
function extractBlock(cssText: string, selector: string): string {
|
||||
const start = cssText.indexOf(selector);
|
||||
if (start === -1) return "";
|
||||
const brace = cssText.indexOf("{", start);
|
||||
if (brace === -1) return "";
|
||||
let depth = 1;
|
||||
let i = brace + 1;
|
||||
while (i < cssText.length && depth > 0) {
|
||||
if (cssText[i] === "{") depth++;
|
||||
else if (cssText[i] === "}") depth--;
|
||||
i++;
|
||||
}
|
||||
return cssText.slice(brace + 1, i - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a string looks like a space-separated RGB triple, e.g. "255 255 255".
|
||||
*/
|
||||
function isRgbTriple(value: string): boolean {
|
||||
return /^\d{1,3}\s+\d{1,3}\s+\d{1,3}$/.test(value.trim());
|
||||
}
|
||||
|
||||
// ─── setup ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const rootBlock = extractBlock(css, ":root");
|
||||
const darkBlock = extractBlock(css, ".dark");
|
||||
|
||||
const STOPS = [
|
||||
"DEFAULT",
|
||||
"50",
|
||||
"100",
|
||||
"200",
|
||||
"250",
|
||||
"300",
|
||||
"350",
|
||||
"400",
|
||||
"500",
|
||||
"600",
|
||||
"700",
|
||||
"800",
|
||||
"850",
|
||||
"900",
|
||||
"910",
|
||||
"920",
|
||||
"925",
|
||||
"930",
|
||||
"935",
|
||||
"940",
|
||||
"950",
|
||||
"960",
|
||||
] as const;
|
||||
|
||||
type Check = { desc: string; ok: boolean };
|
||||
const checks: Check[] = [];
|
||||
|
||||
// ─── 1. :root block exists ─────────────────────────────────────────────────
|
||||
checks.push({
|
||||
desc: ":root block is present in globals.css",
|
||||
ok: rootBlock.length > 0,
|
||||
});
|
||||
|
||||
// ─── 2. .dark block exists ─────────────────────────────────────────────────
|
||||
checks.push({
|
||||
desc: ".dark block is present in globals.css",
|
||||
ok: darkBlock.length > 0,
|
||||
});
|
||||
|
||||
// ─── 3. :root defines all nb-gray stops as RGB triples ─────────────────────
|
||||
for (const stop of STOPS) {
|
||||
const varName =
|
||||
stop === "DEFAULT" ? "--nb-gray-DEFAULT" : `--nb-gray-${stop}`;
|
||||
const value = extractVar(rootBlock, varName);
|
||||
checks.push({
|
||||
desc: `[light/:root] ${varName} is defined as an RGB triple`,
|
||||
ok: value !== null && isRgbTriple(value),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 4. .dark defines all nb-gray stops as RGB triples ─────────────────────
|
||||
for (const stop of STOPS) {
|
||||
const varName =
|
||||
stop === "DEFAULT" ? "--nb-gray-DEFAULT" : `--nb-gray-${stop}`;
|
||||
const value = extractVar(darkBlock, varName);
|
||||
checks.push({
|
||||
desc: `[dark/.dark] ${varName} is defined as an RGB triple`,
|
||||
ok: value !== null && isRgbTriple(value),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 5. Ramps are inverted: light-mode high stops ≠dark-mode high stops ───
|
||||
// e.g. in light mode --nb-gray-900 should be near-white (high channels),
|
||||
// in dark mode --nb-gray-900 should be near-dark (low channels).
|
||||
{
|
||||
const lightDefault = extractVar(rootBlock, "--nb-gray-DEFAULT");
|
||||
const darkDefault = extractVar(darkBlock, "--nb-gray-DEFAULT");
|
||||
checks.push({
|
||||
desc: "DEFAULT stop differs between light and dark mode",
|
||||
ok: lightDefault !== null && darkDefault !== null && lightDefault !== darkDefault,
|
||||
});
|
||||
|
||||
// Light mode DEFAULT should be near-white (all channels high)
|
||||
if (lightDefault) {
|
||||
const channels = lightDefault.split(/\s+/).map(Number);
|
||||
checks.push({
|
||||
desc: "light mode DEFAULT is near-white (all channels >= 240)",
|
||||
ok: channels.every((c) => c >= 240),
|
||||
});
|
||||
}
|
||||
|
||||
// Dark mode DEFAULT should be near-black (all channels low)
|
||||
if (darkDefault) {
|
||||
const channels = darkDefault.split(/\s+/).map(Number);
|
||||
checks.push({
|
||||
desc: "dark mode DEFAULT is near-black (all channels <= 50)",
|
||||
ok: channels.every((c) => c <= 50),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 6. Light mode 50-stop is dark text (low channels) ─────────────────────
|
||||
{
|
||||
const light50 = extractVar(rootBlock, "--nb-gray-50");
|
||||
if (light50) {
|
||||
const channels = light50.split(/\s+/).map(Number);
|
||||
checks.push({
|
||||
desc: "light mode nb-gray-50 is near-black text (all channels <= 50)",
|
||||
ok: channels.every((c) => c <= 50),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 7. Dark mode 50-stop is bright text (high channels) ───────────────────
|
||||
{
|
||||
const dark50 = extractVar(darkBlock, "--nb-gray-50");
|
||||
if (dark50) {
|
||||
const channels = dark50.split(/\s+/).map(Number);
|
||||
checks.push({
|
||||
desc: "dark mode nb-gray-50 is near-white text (all channels >= 220)",
|
||||
ok: channels.every((c) => c >= 220),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 8. No hex values in the CSS variable declarations ─────────────────────
|
||||
{
|
||||
const hexInRoot = /#[0-9a-fA-F]{3,8}/.test(rootBlock);
|
||||
const hexInDark = /#[0-9a-fA-F]{3,8}/.test(darkBlock);
|
||||
checks.push({
|
||||
desc: ":root block contains no hard-coded hex colour values",
|
||||
ok: !hexInRoot,
|
||||
});
|
||||
checks.push({
|
||||
desc: ".dark block contains no hard-coded hex colour values",
|
||||
ok: !hexInDark,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 9. h1, h2, p use theme-agnostic nb-gray classes ─────────────────────
|
||||
{
|
||||
checks.push({
|
||||
desc: "h1 uses text-nb-gray-100 (not dark:text-nb-gray-100 or text-gray-700)",
|
||||
ok:
|
||||
css.includes("text-nb-gray-100") &&
|
||||
!css.includes("dark:text-nb-gray-100") &&
|
||||
!css.match(/h1[^}]*text-gray-700/),
|
||||
});
|
||||
checks.push({
|
||||
desc: "p uses text-nb-gray-200 (not dark:text-zinc-50 or dark:text-gray-700)",
|
||||
ok:
|
||||
css.includes("text-nb-gray-200") &&
|
||||
!css.includes("dark:text-zinc-50") &&
|
||||
!css.includes("text-gray-700"),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── 10. timescape input focus uses text-nb-gray-50 ────────────────────────
|
||||
{
|
||||
checks.push({
|
||||
desc: ".timescape input:focus uses text-nb-gray-50 (not text-white)",
|
||||
ok: css.includes("text-nb-gray-50") && !css.match(/timescape[^}]*text-white/),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── run ───────────────────────────────────────────────────────────────────
|
||||
|
||||
console.log("=== globals.css CSS-variable definitions ===");
|
||||
let failures = 0;
|
||||
for (const { desc, ok } of checks) {
|
||||
if (!ok) failures++;
|
||||
console.log(`${ok ? "âś“" : "âś—"} ${desc}`);
|
||||
}
|
||||
|
||||
console.log(`\n${failures} test(s) failed`);
|
||||
process.exit(failures > 0 ? 1 : 0);
|
||||
@@ -23,7 +23,6 @@ export const idpIcon = (
|
||||
zitadel: <ZitadelIcon size={size} />,
|
||||
authentik: <AuthentikIcon size={size} />,
|
||||
keycloak: <KeycloakIcon size={size} />,
|
||||
adfs: <MicrosoftIcon size={size} />,
|
||||
oidc: <KeyRound size={size} className="text-nb-gray-400" />,
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,237 +0,0 @@
|
||||
/**
|
||||
* Tests for buttonVariants — the cva variant helper exported from Button.tsx.
|
||||
*
|
||||
* These tests cover the light-mode additions introduced in the light-mode draft
|
||||
* PR. The key changes were:
|
||||
* - danger-outline: added explicit light-mode styles (bg-transparent, text-red-600, etc.)
|
||||
* - danger-text: made fully theme-agnostic (dropped the dark:-prefixed classes)
|
||||
* - default-outline: made fully theme-agnostic (dropped the dark:-prefixed classes)
|
||||
* - danger: added explicit light-mode enabled/disabled styles
|
||||
*
|
||||
* Run with: npx tsx src/components/Button.test.ts
|
||||
*/
|
||||
|
||||
import { buttonVariants } from "./Button.js";
|
||||
|
||||
type Case = { desc: string; classes: string[]; notClasses?: string[] };
|
||||
|
||||
function run(name: string, cases: Case[], fn: () => string): number {
|
||||
console.log(`\n=== ${name} ===`);
|
||||
let failures = 0;
|
||||
const result = fn();
|
||||
for (const { desc, classes, notClasses = [] } of cases) {
|
||||
for (const cls of classes) {
|
||||
const ok = result.includes(cls);
|
||||
if (!ok) failures++;
|
||||
console.log(
|
||||
`${ok ? "âś“" : "âś—"} [${desc}] contains "${cls}"` +
|
||||
(ok ? "" : ` (actual: "${result}")`),
|
||||
);
|
||||
}
|
||||
for (const cls of notClasses) {
|
||||
const ok = !result.includes(cls);
|
||||
if (!ok) failures++;
|
||||
console.log(
|
||||
`${ok ? "âś“" : "âś—"} [${desc}] does NOT contain "${cls}"` +
|
||||
(ok ? "" : ` (actual: "${result}")`),
|
||||
);
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
let failures = 0;
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// danger-outline variant
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
failures += run(
|
||||
"buttonVariants — danger-outline",
|
||||
[
|
||||
{
|
||||
desc: "light-mode base",
|
||||
classes: [
|
||||
"bg-transparent",
|
||||
"text-red-600",
|
||||
"border-red-300",
|
||||
"hover:bg-red-50",
|
||||
"hover:border-red-400",
|
||||
"focus:ring-red-500/30",
|
||||
],
|
||||
},
|
||||
{
|
||||
desc: "dark-mode overrides preserved",
|
||||
classes: [
|
||||
"dark:bg-transparent",
|
||||
"dark:text-red-500",
|
||||
"dark:border-transparent",
|
||||
],
|
||||
},
|
||||
],
|
||||
() => buttonVariants({ variant: "danger-outline" }),
|
||||
);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// danger-text variant
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
failures += run(
|
||||
"buttonVariants — danger-text",
|
||||
[
|
||||
{
|
||||
desc: "theme-agnostic text",
|
||||
classes: ["text-red-500", "hover:text-red-600"],
|
||||
},
|
||||
{
|
||||
desc: "no padding / shadow",
|
||||
classes: ["border-transparent", "!px-0", "!shadow-none", "!py-0"],
|
||||
},
|
||||
{
|
||||
desc: "focus ring",
|
||||
classes: ["focus:ring-red-500/30"],
|
||||
},
|
||||
{
|
||||
desc: "ring offset is theme-agnostic (no dark: prefix)",
|
||||
classes: ["ring-offset-nb-gray-950/50"],
|
||||
notClasses: ["dark:ring-offset-neutral-950/50"],
|
||||
},
|
||||
],
|
||||
() => buttonVariants({ variant: "danger-text" }),
|
||||
);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// default-outline variant
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
failures += run(
|
||||
"buttonVariants — default-outline",
|
||||
[
|
||||
{
|
||||
desc: "ring offset and focus ring are theme-agnostic",
|
||||
classes: ["ring-offset-nb-gray-950/50", "focus:ring-nb-gray-500/20"],
|
||||
},
|
||||
{
|
||||
desc: "light/dark-agnostic base styles",
|
||||
classes: [
|
||||
"bg-transparent",
|
||||
"text-nb-gray-400",
|
||||
"border-transparent",
|
||||
"hover:text-nb-gray-50",
|
||||
"hover:bg-nb-gray-900/30",
|
||||
"hover:border-nb-gray-800/50",
|
||||
],
|
||||
},
|
||||
{
|
||||
desc: "data-state=open styles are theme-agnostic",
|
||||
classes: [
|
||||
"data-[state=open]:text-nb-gray-50",
|
||||
"data-[state=open]:bg-nb-gray-900/30",
|
||||
"data-[state=open]:border-nb-gray-800/50",
|
||||
],
|
||||
},
|
||||
{
|
||||
desc: "no leftover dark:-only prefix variants for core styles",
|
||||
notClasses: [
|
||||
"dark:ring-offset-nb-gray-950/50",
|
||||
"dark:focus:ring-nb-gray-500/20",
|
||||
"dark:bg-transparent",
|
||||
"dark:text-nb-gray-400",
|
||||
"data-[state=open]:dark:text-nb-gray-50",
|
||||
],
|
||||
},
|
||||
],
|
||||
() => buttonVariants({ variant: "default-outline" }),
|
||||
);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// danger variant
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
failures += run(
|
||||
"buttonVariants — danger",
|
||||
[
|
||||
{
|
||||
desc: "light-mode enabled state",
|
||||
classes: [
|
||||
"enabled:bg-red-600",
|
||||
"enabled:text-white",
|
||||
"enabled:hover:bg-red-700",
|
||||
"enabled:focus:bg-red-700",
|
||||
"enabled:focus:ring-red-400/50",
|
||||
],
|
||||
},
|
||||
{
|
||||
desc: "light-mode disabled state",
|
||||
classes: [
|
||||
"disabled:bg-red-300",
|
||||
"disabled:text-white",
|
||||
"border-red-600",
|
||||
],
|
||||
},
|
||||
{
|
||||
desc: "dark-mode overrides preserved",
|
||||
classes: [
|
||||
"dark:bg-red-600",
|
||||
"dark:text-red-100",
|
||||
"hover:dark:bg-red-700",
|
||||
],
|
||||
},
|
||||
],
|
||||
() => buttonVariants({ variant: "danger" }),
|
||||
);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Unchanged variants — regression check: ensure they still work
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
failures += run(
|
||||
"buttonVariants — primary (regression)",
|
||||
[
|
||||
{
|
||||
desc: "primary still has netbird colour",
|
||||
classes: ["enabled:dark:bg-netbird", "enabled:bg-netbird"],
|
||||
},
|
||||
],
|
||||
() => buttonVariants({ variant: "primary" }),
|
||||
);
|
||||
|
||||
failures += run(
|
||||
"buttonVariants — default (regression)",
|
||||
[
|
||||
{
|
||||
desc: "default variant still renders",
|
||||
classes: ["bg-white", "text-gray-900"],
|
||||
},
|
||||
],
|
||||
() => buttonVariants({ variant: "default" }),
|
||||
);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Size and rounding defaults
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
failures += run(
|
||||
"buttonVariants — default size and rounding",
|
||||
[
|
||||
{
|
||||
desc: "default size is md",
|
||||
classes: ["py-2.5", "px-4"],
|
||||
},
|
||||
{
|
||||
desc: "default rounded is true",
|
||||
classes: ["rounded-md"],
|
||||
},
|
||||
],
|
||||
() => buttonVariants({}),
|
||||
);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
// Edge: danger-outline with extra className override doesn't lose base classes
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const cls = buttonVariants({ variant: "danger-outline" });
|
||||
const ok =
|
||||
cls.includes("text-red-600") && cls.includes("border-red-300");
|
||||
if (!ok) failures++;
|
||||
console.log(
|
||||
`\n=== danger-outline boundary ===\n${ok ? "âś“" : "âś—"} light-mode base classes present when called without extra options`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`\n${failures} test(s) failed`);
|
||||
process.exit(failures > 0 ? 1 : 0);
|
||||
@@ -50,11 +50,11 @@ function CardListItem({
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
"flex justify-between px-4 border-b border-nb-gray-900 py-3.5 last:border-b-0 items-center h-full",
|
||||
"flex justify-between px-4 border-b border-nb-gray-900 py-4 last:border-b-0 items-center h-full",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={"flex gap-2.5 items-center text-[0.84rem]"}>{label}</div>
|
||||
<div className={"flex gap-2.5 items-center text-sm"}>{label}</div>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<CardTextItem
|
||||
label={label}
|
||||
@@ -100,7 +100,7 @@ const CardTextItem = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-right text-nb-gray-400 text-[0.84rem] flex items-center gap-2",
|
||||
"text-right text-nb-gray-400 text-sm flex items-center gap-2",
|
||||
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
|
||||
)}
|
||||
onClick={() =>
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import useCopyToClipboard from "@hooks/useCopyToClipboard";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Copy } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
type CardTableProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTable({ children, className }: CardTableProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-nb-gray-940 rounded-md border border-nb-gray-900 w-full overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<table className={"w-full border-collapse text-sm"}>{children}</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTableHeader({ children, className }: CardTableProps) {
|
||||
return (
|
||||
<thead>
|
||||
<tr
|
||||
className={cn(
|
||||
"border-b border-nb-gray-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
type CardTableHeaderCellProps = {
|
||||
children: React.ReactNode;
|
||||
width?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTableHeaderCell({
|
||||
children,
|
||||
width,
|
||||
className,
|
||||
}: CardTableHeaderCellProps) {
|
||||
return (
|
||||
<th
|
||||
className={cn(
|
||||
"px-4 py-2.5 text-left text-sm font-normal",
|
||||
className,
|
||||
)}
|
||||
style={width ? { width } : undefined}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTableBody({ children, className }: CardTableProps) {
|
||||
return <tbody className={className}>{children}</tbody>;
|
||||
}
|
||||
|
||||
type CardTableRowProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTableRow({ children, className }: CardTableRowProps) {
|
||||
return (
|
||||
<tr
|
||||
className={cn(
|
||||
"border-b border-nb-gray-900 last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
type CardTableCellProps = {
|
||||
children: React.ReactNode;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
width?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTableCell({
|
||||
children,
|
||||
copy = false,
|
||||
copyText,
|
||||
width,
|
||||
className,
|
||||
}: CardTableCellProps) {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(copyText ?? "");
|
||||
return (
|
||||
<td
|
||||
className={cn("px-4 py-3", className)}
|
||||
style={width ? { width } : undefined}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"text-nb-gray-400 text-sm flex items-center gap-2",
|
||||
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
|
||||
)}
|
||||
onClick={() =>
|
||||
copy &&
|
||||
copyToClipBoard(`${copyText} has been copied to clipboard.`)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
{copy && <Copy size={13} className={"shrink-0"} />}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
CardTable.Header = CardTableHeader;
|
||||
CardTable.HeaderCell = CardTableHeaderCell;
|
||||
CardTable.Body = CardTableBody;
|
||||
CardTable.Row = CardTableRow;
|
||||
CardTable.Cell = CardTableCell;
|
||||
|
||||
export default CardTable;
|
||||
Loaded 20 of 107 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user