Compare commits

...
5 Commits
Author SHA1 Message Date
Eduard Gert 9a401733b3 Fix toggle for p2p policies (#501) 2025-10-31 13:21:23 +01:00
Eduard Gert 07b6895380 Sync SSH & RDP changes (#495) 2025-10-16 14:44:26 +02:00
Eduard Gert 9e2e38764e Add control center (#494)
* Add control center

* Update rdp doc link
2025-10-09 11:26:21 +02:00
Maycon Santos d9fb379abf Enable connect buttons (#493) 2025-10-06 16:23:00 -03:00
Eduard Gert 831673d0d6 Sync with cloud (#491)
implements a "Sync with cloud" functionality that includes various UI improvements, code refactoring, and component extractions. The changes focus on enhancing the user interface, improving code organization, and adding new features for remote access and activity tracking.

- Refactors inline components into reusable shared components
- Adds new activity tracking for group operations
- Updates remote access configuration and UI components
- Enhances styling and layout for better user experience
2025-10-03 14:37:11 +02:00
89 changed files with 6280 additions and 848 deletions

No files matched your search

+1 -2
View File
@@ -71,7 +71,6 @@ jobs:
images: ${{ env.IMAGE_NAME }}
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.NB_DOCKER_USER }}
@@ -82,7 +81,7 @@ jobs:
with:
context: .
file: docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
push: true
platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1
View File
@@ -26,6 +26,7 @@ The dashboard makes it possible to:
- NextJS
- ReactJS
- Tailwind CSS
- [React Flow](https://reactflow.dev/) for the Control Center
- Auth0
- Nginx
- Docker
+2 -1
View File
@@ -13,5 +13,6 @@
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID"
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID",
"wasmPath": "$NETBIRD_WASM_PATH"
}
+4
View File
@@ -7,6 +7,10 @@ server {
root /usr/share/nginx/html;
default_type application/wasm;
}
location = /ironrdp-pkg/ironrdp_web_bg.wasm {
root /usr/share/nginx/html;
default_type application/wasm;
}
location / {
try_files $uri $uri.html $uri/ =404;
+2 -1
View File
@@ -61,11 +61,12 @@ export NETBIRD_GOOGLE_ANALYTICS_ID=${NETBIRD_GOOGLE_ANALYTICS_ID}
export NETBIRD_GOOGLE_TAG_MANAGER_ID=${NETBIRD_GOOGLE_TAG_MANAGER_ID}
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
export NETBIRD_WASM_PATH=${NETBIRD_WASM_PATH:-https://pkgs.netbird.io/wasm/client}
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# replace ENVs in the config
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS"
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
+815 -9
View File
File diff suppressed because it is too large. Load diff
+6 -1
View File
@@ -14,6 +14,7 @@
},
"dependencies": {
"@axa-fr/react-oidc": "^7.22.18",
"@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",
@@ -35,6 +36,7 @@
"@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",
@@ -42,6 +44,7 @@
"@types/react-window": "^1.8.8",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"@xyflow/react": "^12.8.4",
"autoprefixer": "^10",
"chart.js": "^4.4.8",
"chroma-js": "^3.1.2",
@@ -49,8 +52,10 @@
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"crypto-js": "^4.2.0",
"d3": "^7.9.0",
"date-fns": "^2.30.0",
"dayjs": "^1.11.10",
"elkjs": "^0.10.0",
"eslint": "^8",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
@@ -60,7 +65,7 @@
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"lucide-react": "^0.481.0",
"lucide-react": "^0.539.0",
"next": "^14.2.28",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Control Center - ${globalMetaTitle}`,
};
export default BlankLayout;
File diff suppressed because it is too large. Load diff
+13 -1
View File
@@ -1,13 +1,14 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import { Callout } from "@components/Callout";
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 { ArrowUpRightIcon, ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
import PeersProvider from "@/contexts/PeersProvider";
@@ -59,6 +60,17 @@ export default function NetworkRoutes() {
</InlineLink>
in our documentation.
</Paragraph>
<Callout className={"max-w-xl mt-3"} variant={"warning"}>
<span>
We recommend using the new Networks concept to easier visualise
and manage access to your resources.{" "}
<InlineLink href={"/networks"}>
Go to Networks
<ArrowUpRightIcon size={14} />
</InlineLink>
</span>
</Callout>
</div>
<RestrictedAccess hasAccess={permission.routes.read}>
+83 -32
View File
@@ -1,7 +1,15 @@
"use client";
import Breadcrumbs from "@components/Breadcrumbs";
import Button from "@components/Button";
import Card from "@components/Card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@components/DropdownMenu";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import Separator from "@components/Separator";
@@ -12,12 +20,14 @@ import { cn } from "@utils/helpers";
import {
ArrowUpRightIcon,
HelpCircle,
MoreVertical,
PencilLineIcon,
ServerIcon,
ShieldCheckIcon,
ShieldXIcon,
Trash2,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
@@ -25,8 +35,10 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
import { Network } from "@/interfaces/Network";
import PageContainer from "@/layouts/PageContainer";
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
import NetworkModal from "@/modules/networks/NetworkModal";
import { NetworkProvider } from "@/modules/networks/NetworkProvider";
import {
NetworkProvider,
useNetworksContext,
} from "@/modules/networks/NetworkProvider";
import { ResourcesSection } from "@/modules/networks/resources/ResourcesSection";
import { NetworkRoutingPeersSection } from "@/modules/networks/routing-peers/NetworkRoutingPeersSection";
@@ -77,35 +89,24 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
<div className={"flex justify-between max-w-6xl"}>
<div
className={cn(
"flex items-center",
!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}
/>
{permission.networks.update && (
<button
className={
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
}
onClick={() => setNetworkModal(true)}
>
<PencilLineIcon size={18} />
</button>
)}
<NetworkModal
open={networkModal}
setOpen={setNetworkModal}
onUpdated={() => {
mutate(`/networks/${network.id}`);
}}
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>
<NetworkProvider network={network}>
<NetworkActions />
</NetworkProvider>
</div>
</div>
@@ -124,6 +125,56 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
);
}
function NetworkActions() {
const { permission } = usePermissions();
const { deleteNetwork, openEditNetworkModal, network } = useNetworksContext();
const router = useRouter();
if (!network) return;
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger
asChild={true}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<Button variant={"secondary"} className={"!px-3"}>
<MoreVertical size={16} className={"shrink-0"} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-auto" align="end">
<DropdownMenuItem
onClick={() => openEditNetworkModal(network)}
disabled={!permission.networks.update}
>
<div className={"flex gap-3 items-center"}>
<PencilLineIcon size={14} className={"shrink-0"} />
Rename
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() =>
deleteNetwork(network).then(() => router.push("/networks"))
}
variant={"danger"}
disabled={!permission.networks.delete}
>
<div className={"flex gap-3 items-center"}>
<Trash2 size={14} className={"shrink-0"} />
Delete
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
const isHighlyAvailable = !!(
network?.routing_peers_count && network?.routing_peers_count >= 2
@@ -154,7 +205,7 @@ function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
const policyCount = network.policies?.length ?? 0;
return (
<Card>
<Card className={"w-full lg:w-1/2"}>
<Card.List>
<Card.ListItem
tooltip={false}
+31 -56
View File
@@ -4,8 +4,6 @@ import Breadcrumbs from "@components/Breadcrumbs";
import Button from "@components/Button";
import { Callout } from "@components/Callout";
import Card from "@components/Card";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
import FullTooltip from "@components/FullTooltip";
import HelpText from "@components/HelpText";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
@@ -38,12 +36,10 @@ import {
FlagIcon,
Globe,
History,
LockIcon,
MapPin,
MonitorSmartphoneIcon,
NetworkIcon,
PencilIcon,
TerminalSquare,
TimerResetIcon,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
@@ -66,8 +62,9 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
import { PeerExpirationToggle } from "@/modules/peer/PeerExpirationToggle";
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
export default function PeerPage() {
const queryParameter = useSearchParams();
@@ -83,9 +80,8 @@ export default function PeerPage() {
const peerKey = useMemo(() => {
let id = peer?.id ?? "";
let ssh = peer?.ssh_enabled ? "1" : "0";
let expiration = peer?.login_expiration_enabled ? "1" : "0";
return `${id}-${ssh}-${expiration}`;
return `${id}-${expiration}`;
}, [peer]);
if (isRestricted) {
@@ -107,7 +103,7 @@ export default function PeerPage() {
);
return peer && !isLoading ? (
<PeerProvider peer={peer} key={peerId}>
<PeerProvider peer={peer} key={peerId} isPeerDetailPage={true}>
<PeerOverview key={peerKey} />
</PeerProvider>
) : (
@@ -141,8 +137,7 @@ function PeerOverview() {
const PeerGeneralInformation = () => {
const router = useRouter();
const { mutate } = useSWRConfig();
const { peer, user, peerGroups, openSSHDialog, update } = usePeer();
const [ssh, setSsh] = useState(peer.ssh_enabled);
const { peer, user, peerGroups, update } = usePeer();
const [name, setName] = useState(peer.name);
const [showEditNameModal, setShowEditNameModal] = useState(false);
const [loginExpiration, setLoginExpiration] = useState(
@@ -161,7 +156,6 @@ const PeerGeneralInformation = () => {
* Detect if there are changes in the peer information, if there are changes, then enable the save button.
*/
const { hasChanges, updateRef: updateHasChangedRef } = useHasChanges([
ssh,
selectedGroups,
loginExpiration,
inactivityExpiration,
@@ -174,7 +168,6 @@ const PeerGeneralInformation = () => {
if (permission.peers.update) {
const updateRequest = update({
name: newName ?? name,
ssh,
loginExpiration,
inactivityExpiration,
});
@@ -190,7 +183,6 @@ const PeerGeneralInformation = () => {
mutate("/peers/" + peer.id);
mutate("/groups");
updateHasChangedRef([
ssh,
selectedGroups,
loginExpiration,
inactivityExpiration,
@@ -314,51 +306,17 @@ const PeerGeneralInformation = () => {
)}
</div>
<FullTooltip
content={
<div
className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}
>
<LockIcon size={14} />
<span>
{`You don't have the required permissions to update this
setting.`}
</span>
</div>
}
interactive={false}
className={"w-full block"}
disabled={permission.peers.update}
>
<FancyToggleSwitch
value={ssh}
disabled={!permission.peers.update}
onChange={(set) =>
!set
? setSsh(false)
: openSSHDialog().then((confirm) => setSsh(confirm))
}
label={
<>
<TerminalSquare size={16} />
SSH Access
</>
}
helpText={
"Enable the SSH server on this peer to access the machine via an secure shell."
}
/>
</FullTooltip>
<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>
{/* 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>
@@ -582,6 +540,23 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
/>
)}
{peer.created_at && (
<Card.ListItem
label={
<>
<CalendarDays size={16} />
Registered on
</>
}
value={
dayjs(peer.created_at).format("D MMMM, YYYY [at] h:mm A") +
" (" +
dayjs().to(peer.created_at) +
")"
}
/>
)}
<Card.ListItem
label={
<>
+4 -3
View File
@@ -4,6 +4,7 @@ import { notify } from "@components/Notification";
import FullScreenLoading from "@components/ui/FullScreenLoading";
import { IconCircleX } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import { Loader2Icon } from "lucide-react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import type { Peer } from "@/interfaces/Peer";
@@ -19,7 +20,6 @@ import {
NetBirdStatus,
useNetBirdClient,
} from "@/modules/remote-access/useNetBirdClient";
import { cn } from "@utils/helpers";
export default function RDPPage() {
const { peerId } = useRDPQueryParams();
@@ -31,7 +31,7 @@ export default function RDPPage() {
} = useFetchApi<Peer>(`/peers/${peerId}`, true, false, !!peerId);
return (
<div className={"w-screen h-screen overflow-hidden"}>
<div className={"w-screen h-screen overflow-hidden fixed inset-0"}>
{peerId && peer && !isLoading ? (
<RDPSession key={peer.id} peer={peer} />
) : (
@@ -55,7 +55,7 @@ function RDPSession({ peer }: Props) {
useEffect(() => {
document.title = `${peer.name} - ${peer.ip} - RDP`;
}, []);
}, [peer.ip, peer.name, connected, rdp]);
const sendErrorNotification = (title: string, message: string) => {
notify({
@@ -104,6 +104,7 @@ function RDPSession({ peer }: Props) {
port: credentials.port,
username: credentials.username,
password: credentials.password,
domain: credentials.domain,
width: window.innerWidth,
height: window.innerHeight,
});
+5 -2
View File
@@ -2,6 +2,7 @@
import { PageNotFound } from "@components/ui/PageNotFound";
import useFetchApi, { ErrorResponse } from "@utils/api";
import { isNativeSSHSupported } from "@utils/version";
import { CircleXIcon, InfoIcon, Loader2Icon } from "lucide-react";
import React, { useEffect, useRef } from "react";
import type { Peer } from "@/interfaces/Peer";
@@ -86,7 +87,8 @@ function SSHTerminal({ username, port, peer }: Props) {
if (isSSHConnected || isSSHConnecting) return;
connected.current = false;
try {
const rules = [`tcp/${port}`];
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
const rules = [`tcp/${aclPort}`];
await client?.connectTemporary(peer.id, rules);
await ssh({
hostname: peer.ip,
@@ -107,7 +109,8 @@ function SSHTerminal({ username, port, peer }: Props) {
if (connected.current) return;
connected.current = true;
try {
const rules = [`tcp/${port}`];
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
const rules = [`tcp/${aclPort}`];
await client?.connectTemporary(peer.id, rules);
const res = await ssh({
hostname: peer.ip,
+7 -1
View File
@@ -167,4 +167,10 @@ p {
.xterm-viewport {
@apply m-0 p-0 box-border;
}
}
/* Control Center */
.react-flow__node-groupNode .selected{
@apply border-netbird;
}
+2 -1
View File
@@ -35,6 +35,7 @@ export default function NotFound() {
}
const Redirect = ({ url, queryParams }: Props) => {
useRedirect("/peers" + (queryParams && `?${queryParams}`));
const params = queryParams && `?${queryParams}`;
useRedirect(url == "/" ? `/peers${params}` : `${url}${params}`);
return <FullScreenLoading />;
};
+22
View File
@@ -0,0 +1,22 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function ControlCenterIcon(props: IconProps) {
return (
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
{...iconProperties(props)}
>
<path d="M5 3a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5Zm0 12a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2H5Zm12 0a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-2Zm0-12a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-2Z" />
<path
fillRule="evenodd"
d="M10 6.5a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1ZM10 18a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm-4-4a1 1 0 0 1-1-1v-2a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1Zm12 0a1 1 0 0 1-1-1v-2a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1Z"
clipRule="evenodd"
/>
</svg>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

+2
View File
@@ -58,6 +58,8 @@ export default function OIDCProvider({ children }: Props) {
"utm_content",
"utm_campaign",
"hs_id",
"page",
"page_size",
"user",
"port",
];
+1 -1
View File
@@ -34,7 +34,7 @@ export const buttonVariants = cva(
secondary: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
"dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
],
secondaryLighter: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
Loaded 20 of 89 files, more files were not shown because too many files have changed in this diff. Show more