Compare commits
1 Commits
localize-z
...
rainycy-sn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
783294ce55 |
10
.omo/run-continuation/ses_10af25468ffezWZn49GbDsWmkw.json
Normal file
10
.omo/run-continuation/ses_10af25468ffezWZn49GbDsWmkw.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sessionID": "ses_10af25468ffezWZn49GbDsWmkw",
|
||||
"updatedAt": "2026-06-23T15:17:44.982Z",
|
||||
"sources": {
|
||||
"background-task": {
|
||||
"state": "idle",
|
||||
"updatedAt": "2026-06-23T15:17:44.982Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
.omo/run-continuation/ses_10af30226ffeXZKbIMUr35P91Y.json
Normal file
10
.omo/run-continuation/ses_10af30226ffeXZKbIMUr35P91Y.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sessionID": "ses_10af30226ffeXZKbIMUr35P91Y",
|
||||
"updatedAt": "2026-06-23T17:30:43.458Z",
|
||||
"sources": {
|
||||
"background-task": {
|
||||
"state": "idle",
|
||||
"updatedAt": "2026-06-23T17:30:43.458Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
136
package-lock.json
generated
136
package-lock.json
generated
@@ -32,7 +32,7 @@
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/lodash": "4.17.24",
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
@@ -47,7 +47,6 @@
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^1.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^2.30.0",
|
||||
@@ -56,18 +55,16 @@
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"framer-motion": "^12.29.2",
|
||||
"ip-address": "^10.2.0",
|
||||
"ip-address": "^10.1.0",
|
||||
"ip-cidr": "^3.1.0",
|
||||
"js-cookie": "^3.0.7",
|
||||
"lodash": "4.18.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "16.1.7",
|
||||
"next-intl": "^4.13.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-confetti-explosion": "^3.0.3",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-ga4": "^2.1.0",
|
||||
@@ -87,7 +84,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@types/chroma-js": "^3.1.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"eslint": "^9.39.1",
|
||||
@@ -1742,22 +1738,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
@@ -3582,9 +3562,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
|
||||
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
@@ -4870,24 +4850,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -6798,9 +6760,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
@@ -7296,10 +7258,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
|
||||
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
||||
"license": "MIT"
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
@@ -8119,53 +8084,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/po-parser": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
|
||||
@@ -8412,26 +8330,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-chartjs-2": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz",
|
||||
"integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^4.1.1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-confetti-explosion": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-confetti-explosion/-/react-confetti-explosion-3.0.3.tgz",
|
||||
"integrity": "sha512-ow5ns/1ttzXsIlbbfJmWJNiyQK8lTHBL6lRSUXGaK44K/3NIMngR57Ja96l+D6txTeFhfe0BfXGvORMxhtRDng==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-day-picker": {
|
||||
"version": "9.13.0",
|
||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz",
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations();
|
||||
return {
|
||||
title: `${t("navigation.accessControl")} - ${globalMetaTitle}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Access Control - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations();
|
||||
return {
|
||||
title: `${t("navigation.controlCenter")} - ${globalMetaTitle}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Control Center - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations();
|
||||
return {
|
||||
title: `${t("dnsZones")} - ${globalMetaTitle}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Zones - DNS - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations();
|
||||
return {
|
||||
title: `${t("navigation.groups")} - ${globalMetaTitle}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Groups - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations();
|
||||
return {
|
||||
title: `${t("networkRoutes")} - ${globalMetaTitle}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Network Routes - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
|
||||
@@ -205,8 +205,6 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
|
||||
}
|
||||
|
||||
function NetworkActions() {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
const { deleteNetwork, openEditNetworkModal, network } = useNetworksContext();
|
||||
const router = useRouter();
|
||||
@@ -234,7 +232,7 @@ function NetworkActions() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<PencilLineIcon size={14} className={"shrink-0"} />
|
||||
{t("renameNetwork")}
|
||||
Rename
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -249,7 +247,7 @@ function NetworkActions() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
{tCommon("delete")}
|
||||
Delete
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -267,19 +265,27 @@ function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
|
||||
const disabledText = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{t("highAvailabilityInactiveText", { status: tCommon("inactive") })}
|
||||
High availability is currently{" "}
|
||||
<span className={"text-yellow-400 font-medium"}>
|
||||
{tCommon("inactive")}
|
||||
</span>{" "}
|
||||
for this network.
|
||||
</>
|
||||
),
|
||||
[t, tCommon],
|
||||
[tCommon],
|
||||
);
|
||||
|
||||
const enabledText = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{t("highAvailabilityActiveText", { status: tCommon("active") })}
|
||||
High availability is{" "}
|
||||
<span className={"text-green-500 font-medium"}>
|
||||
{tCommon("active")}
|
||||
</span>{" "}
|
||||
for this network.
|
||||
</>
|
||||
),
|
||||
[t, tCommon],
|
||||
[tCommon],
|
||||
);
|
||||
|
||||
const policyCount = network.policies?.length ?? 0;
|
||||
@@ -292,7 +298,7 @@ function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
|
||||
label={
|
||||
<>
|
||||
<ServerIcon size={16} />
|
||||
{t("highAvailability")}
|
||||
High Availability
|
||||
</>
|
||||
}
|
||||
value={
|
||||
@@ -303,11 +309,13 @@ function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
|
||||
{isHighlyAvailable ? enabledText : disabledText}
|
||||
{isHighlyAvailable ? (
|
||||
<div className={"inline-flex mt-2"}>
|
||||
{t("highAvailabilityHelpActive")}
|
||||
You can add more routing peers to increase the
|
||||
availability of this network.
|
||||
</div>
|
||||
) : (
|
||||
<div className={"inline-flex mt-2"}>
|
||||
{t("highAvailabilityHelpInactive")}
|
||||
Go ahead and add more routing peers or groups with routing
|
||||
peers to enable high availability for this network.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations();
|
||||
return {
|
||||
title: `${t("navigation.peers")} - ${globalMetaTitle}`,
|
||||
};
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Peers - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
|
||||
@@ -10,14 +10,12 @@ import useFetchApi from "@utils/api";
|
||||
import { isNetBirdCloud } from "@utils/netbird";
|
||||
import { ExternalLinkIcon, User2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { lazy, Suspense } from "react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import TeamIcon from "@/assets/icons/TeamIcon";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { User } from "@/interfaces/User";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { AccountMfaCard } from "@/cloud/mfa/AccountMFACard";
|
||||
import { IdentityProviderCard } from "@/modules/integrations/idp-sync/IdentityProviderCard";
|
||||
|
||||
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import Button from "@components/Button";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import loadConfig from "@utils/config";
|
||||
import { ArrowRightIcon, RefreshCw } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
@@ -16,9 +15,6 @@ export default function ErrorPage() {
|
||||
const { logout, isAuthenticated } = useOidc();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const t = useTranslations("errors");
|
||||
const tAuth = useTranslations("auth");
|
||||
const tCommon = useTranslations("common");
|
||||
const [error, setError] = useState<{
|
||||
code: number;
|
||||
message: string;
|
||||
@@ -62,19 +58,19 @@ export default function ErrorPage() {
|
||||
error?.message?.toLowerCase().includes("pending approval");
|
||||
|
||||
const getTitle = () => {
|
||||
if (isBlockedUser) return t("userAccountBlocked");
|
||||
if (isPendingApproval) return t("userApprovalPending");
|
||||
return t("accessError");
|
||||
if (isBlockedUser) return "User Account Blocked";
|
||||
if (isPendingApproval) return "User Approval Pending";
|
||||
return "Access Error";
|
||||
};
|
||||
|
||||
const getDescription = () => {
|
||||
if (isBlockedUser) {
|
||||
return t("accessBlockedDescription");
|
||||
return "Your access has been blocked by the NetBird account administrator, possibly due to new user approval requirements or security policies. Please contact your administrator to regain access.";
|
||||
}
|
||||
if (isPendingApproval) {
|
||||
return t("pendingApprovalDescription");
|
||||
return "Your account is pending approval from an administrator. Please wait for approval before accessing the dashboard.";
|
||||
}
|
||||
return t("accessGenericDescription");
|
||||
return "An error occurred while trying to access the dashboard. Please try again or contact your administrator.";
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -98,19 +94,19 @@ export default function ErrorPage() {
|
||||
)}
|
||||
|
||||
<Paragraph className="text-center mt-2 text-sm">
|
||||
{t("contactAdminDescription")}
|
||||
If you believe this is an error, please contact your administrator.
|
||||
</Paragraph>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{!isBlockedUser && !isPendingApproval && (
|
||||
<Button variant="default-outline" size="sm" onClick={handleRetry}>
|
||||
<RefreshCw size={16} className="mr-2" />
|
||||
{tCommon("tryAgain")}
|
||||
Try Again
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button variant="primary" size="sm" onClick={handleLogout}>
|
||||
{isBlockedUser || isPendingApproval ? tAuth("signOut") : tCommon("logout")}
|
||||
{isBlockedUser || isPendingApproval ? "Sign Out" : "Logout"}
|
||||
<ArrowRightIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { X } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
@@ -32,28 +31,25 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const t = useTranslations("common");
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full dark:border-neutral-800 dark:bg-neutral-950",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">{t("close")}</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
});
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full dark:border-neutral-800 dark:bg-neutral-950",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
|
||||
@@ -79,7 +79,6 @@ interface MultiSelectProps {
|
||||
showRoutes?: boolean;
|
||||
disabledGroups?: Group[];
|
||||
"data-testid"?: string;
|
||||
dataCy?: string;
|
||||
showResourceCounter?: boolean;
|
||||
showResources?: boolean;
|
||||
showPeers?: boolean;
|
||||
@@ -121,7 +120,6 @@ export function PeerGroupSelector({
|
||||
showRoutes = false,
|
||||
disabledGroups,
|
||||
"data-testid": dataTestId = "group-selector-dropdown",
|
||||
dataCy,
|
||||
showResourceCounter = true,
|
||||
showResources = false,
|
||||
showPeers = false,
|
||||
@@ -396,7 +394,6 @@ export function PeerGroupSelector({
|
||||
)}
|
||||
disabled={disabled}
|
||||
data-testid={dataTestId}
|
||||
data-cy={dataCy}
|
||||
ref={inputRef}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -2,7 +2,6 @@ import FullTooltip from "@components/FullTooltip";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { uniqBy } from "lodash";
|
||||
import { RouteIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
@@ -12,7 +11,6 @@ type Props = {
|
||||
group_id: string;
|
||||
};
|
||||
export const AccessControlGroupCount = ({ group_id }: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const { data, isLoading } = useFetchApi<Route[]>("/routes");
|
||||
|
||||
const routes = useMemo(() => {
|
||||
@@ -62,7 +60,7 @@ export const AccessControlGroupCount = ({ group_id }: Props) => {
|
||||
}
|
||||
>
|
||||
<RouteIcon size={14} className={"shrink-0"} />
|
||||
{t("routeCount", { count: routes.length })}
|
||||
{routes.length} Route(s)
|
||||
</div>
|
||||
</FullTooltip>
|
||||
) : null;
|
||||
|
||||
@@ -2,7 +2,6 @@ import InlineLink from "@components/InlineLink";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import { ArrowRightIcon, XIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useAnnouncement } from "@/contexts/AnnouncementProvider";
|
||||
|
||||
@@ -37,7 +36,6 @@ const variants = cva(
|
||||
export type AnnouncementVariant = VariantProps<typeof variants>;
|
||||
|
||||
export const AnnouncementBanner = () => {
|
||||
const t = useTranslations("common");
|
||||
const { closeAnnouncement, announcements, setBannerHeight } =
|
||||
useAnnouncement();
|
||||
const announcement = announcements?.find((a) => a.isOpen);
|
||||
@@ -87,7 +85,7 @@ export const AnnouncementBanner = () => {
|
||||
variants({ inlineLink: announcement.variant }),
|
||||
)}
|
||||
>
|
||||
{announcement.linkText || t("learnMore")}
|
||||
{announcement.linkText || "Learn more"}
|
||||
<ArrowRightIcon size={14} />
|
||||
</InlineLink>
|
||||
)}
|
||||
|
||||
@@ -9,11 +9,9 @@ import {
|
||||
} from "@components/DropdownMenu";
|
||||
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function DarkModeToggle() {
|
||||
const t = useTranslations("theme");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
@@ -44,14 +42,14 @@ export default function DarkModeToggle() {
|
||||
disabled={true}
|
||||
>
|
||||
<SunIcon size={16} />
|
||||
{t("light")}
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setTheme("dark")}
|
||||
className={"flex gap-2"}
|
||||
>
|
||||
<MoonIcon size={16} />
|
||||
{t("dark")}
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
disabled={true}
|
||||
@@ -59,7 +57,7 @@ export default function DarkModeToggle() {
|
||||
className={"flex gap-2"}
|
||||
>
|
||||
<MonitorIcon size={16} />
|
||||
{t("system")}
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -2,7 +2,6 @@ import Badge from "@components/Badge";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { EyeIcon, FolderGit2, SquarePen } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
@@ -25,7 +24,6 @@ export default function GroupBadgeWithEditPeers({
|
||||
useSave = true,
|
||||
onPeerAssignmentChange,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const isNew = !group?.id;
|
||||
const [editGroupPeersModal, setEditGroupPeersModal] = useState(false);
|
||||
const { dropdownOptions, addDropdownOptions, updateGroupDropdown } =
|
||||
@@ -89,7 +87,7 @@ export default function GroupBadgeWithEditPeers({
|
||||
"text-[7px] relative -top-[0px] leading-[0] bg-green-900 border border-green-500/20 py-1.5 px-1 rounded-[3px] text-green-400"
|
||||
}
|
||||
>
|
||||
{t("new")}
|
||||
NEW
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -106,7 +104,7 @@ export default function GroupBadgeWithEditPeers({
|
||||
>
|
||||
{peerCount}
|
||||
</span>{" "}
|
||||
{t("peers")}{" "}
|
||||
Peers{" "}
|
||||
</span>
|
||||
{isAllGroup ? (
|
||||
<EyeIcon size={11} className={"shrink-0"} />
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
MessagesSquareIcon,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import Button from "@components/Button";
|
||||
import { cn } from "@utils/helpers";
|
||||
@@ -26,7 +25,6 @@ import SlackIcon from "@/assets/icons/SlackIcon";
|
||||
import { isNetBirdCloud } from "@utils/netbird";
|
||||
|
||||
export default function HelpAndSupportButton() {
|
||||
const tNav = useTranslations("navigation");
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -51,7 +49,7 @@ export default function HelpAndSupportButton() {
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1 px-1">
|
||||
<div className="text-sm font-normal leading-none text-nb-gray-200 py-1">
|
||||
{tNav("helpAndSupport")}
|
||||
Help and Support
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
@@ -64,7 +62,7 @@ export default function HelpAndSupportButton() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<BookText size={14} />
|
||||
{tNav("documentation")}
|
||||
Documentation
|
||||
</div>
|
||||
<DropdownMenuShortcut>
|
||||
<ArrowUpRightIcon size={16} />
|
||||
@@ -78,7 +76,7 @@ export default function HelpAndSupportButton() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<TriangleAlert size={14} />
|
||||
{tNav("troubleshooting")}
|
||||
Troubleshooting
|
||||
</div>
|
||||
<DropdownMenuShortcut>
|
||||
<ArrowUpRightIcon size={16} />
|
||||
@@ -104,7 +102,7 @@ export default function HelpAndSupportButton() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<MessagesSquareIcon size={14} />
|
||||
{tNav("forum")}
|
||||
NetBird Forum
|
||||
</div>
|
||||
<DropdownMenuShortcut>
|
||||
<ArrowUpRightIcon size={16} />
|
||||
@@ -118,7 +116,7 @@ export default function HelpAndSupportButton() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<SlackIcon size={14} />
|
||||
{tNav("slack")}
|
||||
NetBird Slack
|
||||
</div>
|
||||
<DropdownMenuShortcut>
|
||||
<ArrowUpRightIcon size={16} />
|
||||
@@ -135,7 +133,7 @@ export default function HelpAndSupportButton() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<MessageSquareShare size={14} />
|
||||
{tNav("feedback")}
|
||||
Feedback
|
||||
</div>
|
||||
<DropdownMenuShortcut>
|
||||
<ArrowUpRightIcon size={16} />
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Input } from "@components/Input";
|
||||
import { validator } from "@utils/helpers";
|
||||
import { uniqueId } from "lodash";
|
||||
import { GlobeIcon, MinusCircleIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Domain } from "@/interfaces/Domain";
|
||||
@@ -48,7 +47,6 @@ export default function InputDomain({
|
||||
allowWildcard = true,
|
||||
showRemoveButton = true,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const [name, setName] = useState(value?.name || "");
|
||||
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -66,7 +64,7 @@ export default function InputDomain({
|
||||
preventLeadingAndTrailingDots,
|
||||
});
|
||||
if (!valid) {
|
||||
return t("validDomainError");
|
||||
return "Please enter a valid domain, e.g. example.com or intra.example.com";
|
||||
}
|
||||
}, [name]);
|
||||
|
||||
@@ -82,7 +80,7 @@ export default function InputDomain({
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={<GlobeIcon size={15} />}
|
||||
placeholder={t("domainPlaceholder")}
|
||||
placeholder={"e.g., example.com"}
|
||||
maxWidthClass={"w-full"}
|
||||
data-testid={"domain-input"}
|
||||
value={name}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import Button from "@components/Button";
|
||||
import { Modal, ModalTrigger } from "@components/modal/Modal";
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useState } from "react";
|
||||
import SetupModal from "@/modules/setup-netbird-modal/SetupModal";
|
||||
|
||||
export function InstallNetBirdButton() {
|
||||
const t = useTranslations("common");
|
||||
const [installModal, setInstallModal] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -14,7 +12,7 @@ export function InstallNetBirdButton() {
|
||||
<ModalTrigger asChild>
|
||||
<Button variant={"secondary"} size={"sm"}>
|
||||
<DownloadIcon size={16} />
|
||||
{t("installNetBird")}
|
||||
Install NetBird
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<SetupModal />
|
||||
|
||||
@@ -14,9 +14,15 @@ import { cn } from "@utils/helpers";
|
||||
import { CheckIcon, GlobeIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useLocale } from "@/contexts/LocaleProvider";
|
||||
import { locales, type Locale, LOCALE_LABELS } from "@/i18n/config";
|
||||
import { locales, type Locale } from "@/i18n/config";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/** Human-readable label for each locale, shown in the switcher. */
|
||||
const LOCALE_LABELS: Record<Locale, string> = {
|
||||
en: "English",
|
||||
zh: "中文",
|
||||
};
|
||||
|
||||
/**
|
||||
* Header control that switches the active locale. Writes the choice to the
|
||||
* `NEXT_LOCALE` cookie (via {@link useLocale}) so it persists across reloads
|
||||
@@ -64,7 +70,7 @@ export default function LocaleSwitcher() {
|
||||
<DropdownMenuContent className="w-48" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="text-sm font-normal leading-none text-nb-gray-200 py-1 px-1">
|
||||
{t("language")}
|
||||
Language
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import Badge from "@components/Badge";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/Tooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
loginExpired: boolean;
|
||||
};
|
||||
export default function LoginExpiredBadge({ loginExpired }: Props) {
|
||||
const t = useTranslations("peers");
|
||||
|
||||
return loginExpired ? (
|
||||
<Tooltip delayDuration={1}>
|
||||
<TooltipTrigger>
|
||||
<Badge variant={"red"} className={"px-2"}>
|
||||
<AlertTriangle size={12} />
|
||||
{t("loginRequired")}
|
||||
Login required
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className={"text-neutral-300 text-xs leading-1.5"}>
|
||||
{t("loginExpiredTooltip")}
|
||||
This peer is offline and needs to be <br />
|
||||
re-authenticated because its login has expired.
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -6,15 +6,12 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@components/Tooltip";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
domains: string[];
|
||||
};
|
||||
export default function MultipleDomains({ domains }: Props) {
|
||||
const t = useTranslations("common");
|
||||
|
||||
if (domains.length === 0) {
|
||||
return (
|
||||
<Badge
|
||||
@@ -22,7 +19,7 @@ export default function MultipleDomains({ domains }: Props) {
|
||||
className={"uppercase tracking-wider font-medium"}
|
||||
>
|
||||
<GlobeIcon size={10} />
|
||||
{t("allDomains")}
|
||||
All
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +31,7 @@ export default function MultipleDomains({ domains }: Props) {
|
||||
<TooltipTrigger asChild={true}>
|
||||
<Badge variant={"blue-darker"} className={"cursor-help"}>
|
||||
<GlobeIcon size={10} />
|
||||
{t("domainCount", { count: domains.length })}
|
||||
{domains.length} Domains
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={"p-3"}>
|
||||
|
||||
@@ -41,8 +41,8 @@ type Props = {
|
||||
|
||||
export default function MultipleGroups({
|
||||
groups,
|
||||
label,
|
||||
description,
|
||||
label = "Assigned Groups",
|
||||
description = "Use groups to control what this peer can access",
|
||||
onClick,
|
||||
className,
|
||||
showResources = false,
|
||||
@@ -53,12 +53,8 @@ export default function MultipleGroups({
|
||||
countThreshold = 1,
|
||||
}: Readonly<Props>) {
|
||||
const tGroups = useTranslations("groups");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const resolvedLabel = label ?? tCommon("assignedGroups");
|
||||
const resolvedDescription = description ?? tCommon("assignedGroupsDescription");
|
||||
|
||||
if (!groups || groups?.length === 0) return <EmptyRow />;
|
||||
const orderedGroups = [...groups].sort((a, b) => {
|
||||
if (a.name === "All") return 1;
|
||||
@@ -143,7 +139,7 @@ export default function MultipleGroups({
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className={"text-sm font-medium text-left px-5 pt-3"}>
|
||||
{resolvedLabel}
|
||||
{label}
|
||||
</div>
|
||||
<ScrollArea
|
||||
className={
|
||||
|
||||
@@ -2,7 +2,6 @@ import Card from "@components/Card";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { FilterX } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
|
||||
@@ -16,15 +15,11 @@ type Props = {
|
||||
|
||||
export default function NoResultsCard({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
title = "Could not find any results",
|
||||
description = "We couldn't find any results. Please try a different search term or change your filters.",
|
||||
children,
|
||||
className,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("table");
|
||||
|
||||
const displayTitle = title || t("noResultsCardTitle");
|
||||
const displayDescription = description || t("noResultsDescription");
|
||||
return (
|
||||
<div className={cn("px-8 mt-8", className)}>
|
||||
<Card className={"w-full relative overflow-hidden"}>
|
||||
@@ -55,11 +50,9 @@ export default function NoResultsCard({
|
||||
{icon || <FilterX size={24} />}
|
||||
</div>
|
||||
<div className={"text-center"}>
|
||||
<h1 className={"text-2xl font-medium max-w-lg mx-auto"}>
|
||||
{displayTitle}
|
||||
</h1>
|
||||
<h1 className={"text-2xl font-medium max-w-lg mx-auto"}>{title}</h1>
|
||||
<Paragraph className={"justify-center my-2 !text-nb-gray-400"}>
|
||||
{displayDescription}
|
||||
{description}
|
||||
</Paragraph>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@ import Card from "@components/Card";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SquareIcon from "@components/SquareIcon";
|
||||
import { CircleAlertIcon, Undo2Icon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
@@ -13,14 +12,12 @@ type Props = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
export const PageNotFound = ({ title, description }: Props) => {
|
||||
const t = useTranslations("pageNotFound");
|
||||
const tCommon = useTranslations("common");
|
||||
export const PageNotFound = ({
|
||||
title = "The requested page was not found",
|
||||
description = "The page you are attempting to access cannot be found. Please verify the URL or return to the dashboard to continue browsing.",
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const displayTitle = title || t("title");
|
||||
const displayDescription = description || t("description");
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"px-8"}>
|
||||
@@ -69,10 +66,10 @@ export const PageNotFound = ({ title, description }: Props) => {
|
||||
"text-3xl font-medium mx-auto mt-3 capitalize"
|
||||
}
|
||||
>
|
||||
{displayTitle}
|
||||
{title}
|
||||
</h1>
|
||||
<Paragraph className={"justify-center my-3 max-w-xl"}>
|
||||
{displayDescription}
|
||||
{description}
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
@@ -80,7 +77,7 @@ export const PageNotFound = ({ title, description }: Props) => {
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<Undo2Icon size={15} className={"shrink-0"} />
|
||||
{tCommon("goBack")}
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Badge, { BadgeVariants } from "@components/Badge";
|
||||
import { cn, singularize } from "@utils/helpers";
|
||||
import { MonitorSmartphoneIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
@@ -21,7 +20,6 @@ export default function PeerCountBadge({
|
||||
className,
|
||||
disableRedirect = false,
|
||||
}: Props) {
|
||||
const t = useTranslations("common");
|
||||
const router = useRouter();
|
||||
const { dropdownOptions, groups } = useGroups();
|
||||
|
||||
@@ -63,7 +61,7 @@ export default function PeerCountBadge({
|
||||
useHover={canRedirect}
|
||||
>
|
||||
<MonitorSmartphoneIcon size={12} />
|
||||
{t("peerCount", { count: peerCount })}
|
||||
{singularize("Peers", peerCount, true)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Badge, { BadgeVariants } from "@components/Badge";
|
||||
import { cn, singularize } from "@utils/helpers";
|
||||
import { LayersIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
@@ -16,7 +15,6 @@ export default function ResourceCountBadge({
|
||||
group,
|
||||
disableRedirect = false,
|
||||
}: Props) {
|
||||
const t = useTranslations("common");
|
||||
const router = useRouter();
|
||||
const hasId = !!group?.id;
|
||||
|
||||
@@ -34,7 +32,7 @@ export default function ResourceCountBadge({
|
||||
useHover={hasId}
|
||||
>
|
||||
<LayersIcon size={12} />
|
||||
{t("resourceCount", { count: group?.resources_count ?? 0 })}
|
||||
{singularize("Resources", group?.resources_count, true)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -138,7 +137,6 @@ export const SlidingTabsBackTrigger = ({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const t = useTranslations("common");
|
||||
const { back } = useSlidingTabContext();
|
||||
return (
|
||||
<div
|
||||
@@ -150,7 +148,7 @@ export const SlidingTabsBackTrigger = ({
|
||||
className={"flex gap-2 items-center select-none cursor-pointer"}
|
||||
>
|
||||
<ChevronLeft size={18} />
|
||||
{t("back")}
|
||||
Back
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
|
||||
const smallBadgeVariants = cva("", {
|
||||
@@ -29,19 +28,17 @@ type Props = {
|
||||
} & VariantProps<typeof smallBadgeVariants>;
|
||||
|
||||
export const SmallBadge = ({
|
||||
text,
|
||||
text = "NEW",
|
||||
className,
|
||||
textClassName,
|
||||
variant = "green",
|
||||
children,
|
||||
size = "default",
|
||||
}: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const resolvedText = text ?? t("new");
|
||||
return (
|
||||
<span className={cn(smallBadgeVariants({ variant, size }), className)}>
|
||||
{children}
|
||||
<span className={cn("relative top-[0.4px]", textClassName)}>{resolvedText}</span>
|
||||
<span className={cn("relative top-[0.4px]", textClassName)}>{text}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { UserAvatar } from "@components/ui/UserAvatar";
|
||||
import { CreditCardIcon, KeyRound, LogOutIcon, User2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
@@ -26,9 +25,6 @@ import { isNetBirdCloud } from "@utils/netbird";
|
||||
import { Modal } from "@components/modal/Modal";
|
||||
|
||||
export default function UserDropdown() {
|
||||
const t = useTranslations("userDropdown");
|
||||
const tCommon = useTranslations("common");
|
||||
const tSettings = useTranslations("settings");
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false);
|
||||
const { user } = useApplicationContext();
|
||||
@@ -110,7 +106,7 @@ export default function UserDropdown() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<KeyRound size={14} />
|
||||
{tSettings("changePassword")}
|
||||
Change Password
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
@@ -118,7 +114,7 @@ export default function UserDropdown() {
|
||||
<DropdownMenuItem onClick={logout}>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<LogOutIcon size={14} />
|
||||
{tCommon("logout")}
|
||||
Log out
|
||||
</div>
|
||||
<DropdownMenuShortcut>
|
||||
{isMac ? "⇧⌘L" : "⇧ ⊞ L"}
|
||||
@@ -131,7 +127,6 @@ export default function UserDropdown() {
|
||||
}
|
||||
|
||||
const ProfileSettingsDropdownItem = ({ onClick }: { onClick: () => void }) => {
|
||||
const t = useTranslations("userDropdown");
|
||||
const { isMSPInTenantContext } = useMSP();
|
||||
const { permission } = usePermissions();
|
||||
|
||||
@@ -142,14 +137,13 @@ const ProfileSettingsDropdownItem = ({ onClick }: { onClick: () => void }) => {
|
||||
<DropdownMenuItem onClick={onClick}>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<User2 size={14} />
|
||||
{t("profileSettings")}
|
||||
Profile Settings
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const PlansAndBillingDropdownItem = ({ onClick }: { onClick: () => void }) => {
|
||||
const t = useTranslations("userDropdown");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { isAccountWithMSPParent } = useMSP();
|
||||
@@ -160,7 +154,7 @@ const PlansAndBillingDropdownItem = ({ onClick }: { onClick: () => void }) => {
|
||||
<DropdownMenuItem onClick={onClick}>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<CreditCardIcon size={14} />
|
||||
{t("plansAndBilling")}
|
||||
Plans & Billing
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function PeerProvider({
|
||||
: peer.login_expiration_enabled,
|
||||
inactivity_expiration_enabled:
|
||||
props?.inactivityExpiration == undefined
|
||||
? peer.inactivity_expiration_enabled
|
||||
? undefined
|
||||
: props.inactivityExpiration,
|
||||
approval_required:
|
||||
props?.approval_required == undefined
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Shared by:
|
||||
* - {@link './request.ts'} (next-intl plugin, build-time)
|
||||
* - {@link './detection.ts'} (cookie + browser locale detection)
|
||||
* - {@link './routing.ts'} (next-intl routing definition)
|
||||
* - {@link '../contexts/LocaleProvider.tsx'} (runtime provider)
|
||||
*
|
||||
* NOTE: This app uses `output: "export"` (static export), so there is no
|
||||
@@ -24,16 +25,10 @@ export type Locale = (typeof locales)[number];
|
||||
/** Locale used when no preference is stored or detectable. */
|
||||
export const defaultLocale: Locale = "en";
|
||||
|
||||
/** Human-readable label for each locale, shown in selectors and switchers. */
|
||||
export const LOCALE_LABELS: Record<Locale, string> = {
|
||||
en: "English",
|
||||
zh: "中文",
|
||||
};
|
||||
|
||||
/** Message catalog keyed by locale. */
|
||||
export const messages: Record<Locale, typeof en> = {
|
||||
en,
|
||||
zh,
|
||||
zh: zh as unknown as typeof en,
|
||||
};
|
||||
|
||||
/** Cookie name used to persist the user's locale choice. */
|
||||
|
||||
9
src/i18n/global.d.ts
vendored
9
src/i18n/global.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
import en from "./messages/en";
|
||||
|
||||
type Messages = typeof en;
|
||||
|
||||
declare global {
|
||||
interface IntlMessages extends Messages {}
|
||||
}
|
||||
|
||||
export {};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
10
src/i18n/navigation.ts
Normal file
10
src/i18n/navigation.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createNavigation } from "next-intl/navigation";
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ["en", "zh"],
|
||||
defaultLocale: "en",
|
||||
});
|
||||
|
||||
export const { Link, redirect, usePathname, useRouter } =
|
||||
createNavigation(routing);
|
||||
8
src/i18n/routing.ts
Normal file
8
src/i18n/routing.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
|
||||
import { defaultLocale, locales } from "./config";
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales,
|
||||
defaultLocale,
|
||||
});
|
||||
@@ -251,7 +251,7 @@ label={t('team')}
|
||||
<MSPNavigationItem />
|
||||
<SidebarItem
|
||||
icon={<IntegrationIcon />}
|
||||
label={t("integrations")}
|
||||
label="Integrations"
|
||||
href={"/integrations"}
|
||||
exactPathMatch={true}
|
||||
visible={
|
||||
@@ -315,7 +315,7 @@ label={t('activity')}
|
||||
visible={permission.events.read}
|
||||
/>
|
||||
<SidebarItem
|
||||
label={t("trafficEvents")}
|
||||
label="Traffic Events"
|
||||
isChild
|
||||
href={"/events/traffic"}
|
||||
exactPathMatch={true}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import {
|
||||
@@ -11,7 +9,6 @@ import {
|
||||
} from "@components/Select";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { ShieldHalfIcon, ShieldUserIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
value: "full" | "limited";
|
||||
@@ -19,7 +16,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SSHAccessType = ({ value, onChange }: Props) => {
|
||||
const t = useTranslations("policies");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
return (
|
||||
@@ -38,15 +34,15 @@ export const SSHAccessType = ({ value, onChange }: Props) => {
|
||||
) : (
|
||||
<ShieldHalfIcon size={15} className={"text-nb-gray-300 shrink-0"} />
|
||||
)}
|
||||
<SelectValue placeholder={t("sshAccessPlaceholder")} />
|
||||
<SelectValue placeholder="Select ssh access type..." />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent data-testid={"ssh-access-selection"}>
|
||||
<SelectItem value="full" className={"whitespace-nowrap"}>
|
||||
{t("sshFullAccess")}
|
||||
Full Access
|
||||
</SelectItem>
|
||||
<SelectItem value="limited" className={"whitespace-nowrap"}>
|
||||
{t("sshLimitedAccess")}
|
||||
Limited Access
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useMemo } from "react";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
@@ -10,7 +8,6 @@ import { useUsers } from "@/contexts/UsersProvider";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Callout } from "@components/Callout";
|
||||
import { SSHUsernameSelector } from "@/modules/access-control/ssh/SSHUsernameSelector";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
sourceGroups?: Group[];
|
||||
@@ -25,7 +22,6 @@ export function SSHAuthorizedGroups({
|
||||
setAuthorizedGroups,
|
||||
accessType,
|
||||
}: Props) {
|
||||
const t = useTranslations("policies");
|
||||
const isEmpty =
|
||||
!authorizedGroups || Object.keys(authorizedGroups).length === 0;
|
||||
|
||||
@@ -65,7 +61,9 @@ export function SSHAuthorizedGroups({
|
||||
icon={<InfoIcon size={14} className={"shrink-0 relative top-[3px]"} />}
|
||||
className="mt-3 py-[.75rem]"
|
||||
>
|
||||
{t("sshNoSourceGroups")}
|
||||
You have not added any source groups yet, please add source groups in
|
||||
order to specify which user group has access to which system users on
|
||||
the destination machines.
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import { Callout } from "@components/Callout";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
@@ -20,7 +18,6 @@ import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useElementSize } from "@/hooks/useElementSize";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface MultiSelectProps {
|
||||
values?: string[];
|
||||
@@ -35,7 +32,6 @@ export function SSHUsernameSelector({
|
||||
disabled = false,
|
||||
popoverWidth = "auto",
|
||||
}: Readonly<MultiSelectProps>) {
|
||||
const t = useTranslations("policies");
|
||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
||||
@@ -92,7 +88,7 @@ export function SSHUsernameSelector({
|
||||
{values?.length === 0 && (
|
||||
<Badge variant={"gray"} className={"font-normal py-1"}>
|
||||
<CircleUserIcon size={12} className={"shrink-0"} />
|
||||
{t("sshAllLocalUsers")}
|
||||
All Local Users
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -151,7 +147,7 @@ export function SSHUsernameSelector({
|
||||
ref={searchRef}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
placeholder={t("sshUsernamePlaceholder")}
|
||||
placeholder={"E.g., root, ec2-user, ubuntu"}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
@@ -206,7 +202,10 @@ export function SSHUsernameSelector({
|
||||
<div
|
||||
className={"text-neutral-500 dark:text-nb-gray-300"}
|
||||
>
|
||||
{t("sshAddUsernameByPressing", { key: "Enter" })}
|
||||
Add username by pressing{" "}
|
||||
<span className={"font-bold text-netbird"}>
|
||||
{"'Enter'"}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { ToggleSwitch } from "@components/ToggleSwitch";
|
||||
import React, { useMemo } from "react";
|
||||
import { mutate } from "swr";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { usePolicies } from "@/contexts/PoliciesProvider";
|
||||
import { Policy } from "@/interfaces/Policy";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
policy: Policy;
|
||||
};
|
||||
export default function AccessControlActiveCell({ policy }: Readonly<Props>) {
|
||||
const t = useTranslations("policies");
|
||||
const { updatePolicy, serializeRules } = usePolicies();
|
||||
const { permission } = usePermissions();
|
||||
|
||||
@@ -27,7 +23,9 @@ export default function AccessControlActiveCell({ policy }: Readonly<Props>) {
|
||||
() => {
|
||||
mutate("/policies");
|
||||
},
|
||||
enabled ? t("policyEnabledSuccess") : t("policyDisabledSuccess"),
|
||||
enabled
|
||||
? "The rule was successfully enabled"
|
||||
: "The rule was successfully disabled",
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import { IconCirclePlus } from "@tabler/icons-react";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import React from "react";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { Policy } from "@/interfaces/Policy";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
policy: Policy;
|
||||
};
|
||||
export default function AccessControlPostureCheckCell({ policy }: Props) {
|
||||
const t = useTranslations("policies");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const isDisabled = !permission.policies.create || !permission.policies.update;
|
||||
@@ -22,7 +18,7 @@ export default function AccessControlPostureCheckCell({ policy }: Props) {
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"gray"} useHover={true}>
|
||||
<ShieldCheck size={14} className={"text-green-500"} />
|
||||
{t("postureCheckCount", { count: policy.source_posture_checks.length })}
|
||||
{policy.source_posture_checks.length} Posture Check(s)
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
@@ -36,7 +32,7 @@ export default function AccessControlPostureCheckCell({ policy }: Props) {
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<IconCirclePlus size={14} />
|
||||
{t("addPostureCheck")}
|
||||
Add Posture Check
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import {
|
||||
@@ -13,7 +11,6 @@ import React, { useMemo } from "react";
|
||||
import { Policy } from "@/interfaces/Policy";
|
||||
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
|
||||
import { parsePortsToStrings } from "@/modules/access-control/useAccessControl";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
// AccessControlProtoPortsCell — single column combining the protocol
|
||||
// and ports indicators. Protocol is always shown. Ports rendering:
|
||||
@@ -27,8 +24,6 @@ type Props = {
|
||||
export default function AccessControlProtoPortsCell({
|
||||
policy,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("policies");
|
||||
const tCommon = useTranslations("common");
|
||||
const rule = useMemo(() => {
|
||||
if (policy.rules.length > 0) return policy.rules[0];
|
||||
return undefined;
|
||||
@@ -57,7 +52,7 @@ export default function AccessControlProtoPortsCell({
|
||||
<FullTooltip
|
||||
interactive={false}
|
||||
content={
|
||||
<span className={"text-xs text-nb-gray-100"}>{t("netbirdSshTooltip")}</span>
|
||||
<span className={"text-xs text-nb-gray-100"}>NETBIRD-SSH</span>
|
||||
}
|
||||
>
|
||||
<span className={"cursor-help"}>{protocolBadge}</span>
|
||||
@@ -75,7 +70,7 @@ export default function AccessControlProtoPortsCell({
|
||||
variant={"gray"}
|
||||
className={"uppercase tracking-wider font-medium"}
|
||||
>
|
||||
{tCommon("all")}
|
||||
All
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -97,7 +92,7 @@ export default function AccessControlProtoPortsCell({
|
||||
"px-3 whitespace-nowrap uppercase tracking-wider font-medium"
|
||||
}
|
||||
>
|
||||
{t("nPorts", { count: allPorts.length })}
|
||||
{allPorts.length} Ports
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
@@ -36,7 +34,6 @@ import GetStartedTest from "@components/ui/GetStartedTest";
|
||||
import type { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import { removeAllSpaces } from "@utils/helpers";
|
||||
import { ClockFadingIcon, ExternalLinkIcon, PlusCircle } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -64,160 +61,155 @@ type Props = {
|
||||
isGroupPage?: boolean;
|
||||
};
|
||||
|
||||
function createAccessControlTableColumns(
|
||||
t: ReturnType<typeof useTranslations<"policies">>,
|
||||
tCommon: ReturnType<typeof useTranslations<"common">>,
|
||||
): ColumnDef<Policy>[] {
|
||||
return [
|
||||
{
|
||||
id: "name",
|
||||
accessorFn: (row) => removeAllSpaces(row?.name),
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("name")}</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "text",
|
||||
filterFn: "fuzzy",
|
||||
cell: ({ cell }) => <AccessControlNameCell policy={cell.row.original} />,
|
||||
export const AccessControlTableColumns: ColumnDef<Policy>[] = [
|
||||
{
|
||||
id: "name",
|
||||
accessorFn: (row) => removeAllSpaces(row?.name),
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Name</DataTableHeader>;
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
accessorFn: (row) => removeAllSpaces(row?.description),
|
||||
sortingFn: "text",
|
||||
filterFn: "fuzzy",
|
||||
sortingFn: "text",
|
||||
filterFn: "fuzzy",
|
||||
cell: ({ cell }) => <AccessControlNameCell policy={cell.row.original} />,
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
accessorFn: (row) => removeAllSpaces(row?.description),
|
||||
sortingFn: "text",
|
||||
filterFn: "fuzzy",
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
accessorKey: "enabled",
|
||||
accessorFn: (row) => row.enabled,
|
||||
sortingFn: "basic",
|
||||
},
|
||||
{
|
||||
id: "sources",
|
||||
accessorFn: (row) => {
|
||||
try {
|
||||
return row.rules[0].sources?.length || 0;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
accessorKey: "enabled",
|
||||
accessorFn: (row) => row.enabled,
|
||||
sortingFn: "basic",
|
||||
sortingFn: "basic",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Sources</DataTableHeader>;
|
||||
},
|
||||
{
|
||||
id: "sources",
|
||||
accessorFn: (row) => {
|
||||
try {
|
||||
return row.rules[0].sources?.length || 0;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
sortingFn: "basic",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("sources")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ cell }) => <AccessControlSourcesCell policy={cell.row.original} />,
|
||||
cell: ({ cell }) => <AccessControlSourcesCell policy={cell.row.original} />,
|
||||
},
|
||||
{
|
||||
id: "direction",
|
||||
accessorFn: (row) => {
|
||||
try {
|
||||
return row.rules[0].bidirectional || true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
{
|
||||
id: "direction",
|
||||
accessorFn: (row) => {
|
||||
try {
|
||||
return row.rules[0].bidirectional || true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
sortingFn: "basic",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("direction")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ cell }) => (
|
||||
<AccessControlDirectionCell policy={cell.row.original} />
|
||||
),
|
||||
sortingFn: "basic",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Direction</DataTableHeader>;
|
||||
},
|
||||
{
|
||||
id: "destinations",
|
||||
accessorFn: (row) => {
|
||||
try {
|
||||
return row.rules[0].destinations?.length || 0;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
sortingFn: "basic",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("destinations")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ cell }) => (
|
||||
<AccessControlDestinationsCell policy={cell.row.original} />
|
||||
),
|
||||
cell: ({ cell }) => (
|
||||
<AccessControlDirectionCell policy={cell.row.original} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "destinations",
|
||||
accessorFn: (row) => {
|
||||
try {
|
||||
return row.rules[0].destinations?.length || 0;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
sortingFn: "basic",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Destinations</DataTableHeader>;
|
||||
},
|
||||
cell: ({ cell }) => (
|
||||
<AccessControlDestinationsCell policy={cell.row.original} />
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
id: "proto_ports",
|
||||
accessorFn: (row) => row.rules?.[0]?.protocol || "",
|
||||
sortingFn: "text",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("protoPorts")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ cell }) => (
|
||||
<AccessControlProtoPortsCell policy={cell.row.original} />
|
||||
),
|
||||
{
|
||||
id: "proto_ports",
|
||||
accessorFn: (row) => row.rules?.[0]?.protocol || "",
|
||||
sortingFn: "text",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Proto & Ports</DataTableHeader>;
|
||||
},
|
||||
{
|
||||
id: "id",
|
||||
accessorKey: "id",
|
||||
filterFn: "exactMatch",
|
||||
cell: ({ cell }) => (
|
||||
<AccessControlProtoPortsCell policy={cell.row.original} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "id",
|
||||
accessorKey: "id",
|
||||
filterFn: "exactMatch",
|
||||
},
|
||||
// Hidden filter columns powering the consolidated Filters UI.
|
||||
{
|
||||
id: "source_group_names",
|
||||
accessorFn: (row) => {
|
||||
const sources = row.rules?.[0]?.sources;
|
||||
if (!sources) return [];
|
||||
return (sources as { name?: string }[])
|
||||
.map((s) => s?.name)
|
||||
.filter((n): n is string => !!n);
|
||||
},
|
||||
// Hidden filter columns powering the consolidated Filters UI.
|
||||
{
|
||||
id: "source_group_names",
|
||||
accessorFn: (row) => {
|
||||
const sources = row.rules?.[0]?.sources;
|
||||
if (!sources) return [];
|
||||
return (sources as { name?: string }[])
|
||||
.map((s) => s?.name)
|
||||
.filter((n): n is string => !!n);
|
||||
},
|
||||
filterFn: "arrIncludesSome",
|
||||
filterFn: "arrIncludesSome",
|
||||
},
|
||||
{
|
||||
id: "destination_group_names",
|
||||
accessorFn: (row) => {
|
||||
const destinations = row.rules?.[0]?.destinations;
|
||||
if (!destinations) return [];
|
||||
return (destinations as { name?: string }[])
|
||||
.map((d) => d?.name)
|
||||
.filter((n): n is string => !!n);
|
||||
},
|
||||
{
|
||||
id: "destination_group_names",
|
||||
accessorFn: (row) => {
|
||||
const destinations = row.rules?.[0]?.destinations;
|
||||
if (!destinations) return [];
|
||||
return (destinations as { name?: string }[])
|
||||
.map((d) => d?.name)
|
||||
.filter((n): n is string => !!n);
|
||||
},
|
||||
filterFn: "arrIncludesSome",
|
||||
filterFn: "arrIncludesSome",
|
||||
},
|
||||
{
|
||||
id: "protocol_filter",
|
||||
accessorFn: (row) => [row.rules?.[0]?.protocol || "all"],
|
||||
filterFn: "arrIncludesSome",
|
||||
},
|
||||
{
|
||||
id: "ports_filter",
|
||||
accessorFn: (row) => {
|
||||
const rule = row.rules?.[0];
|
||||
const ports = rule?.ports || [];
|
||||
const ranges = (rule?.port_ranges || []).map(
|
||||
(r) => `${r.start}-${r.end}`,
|
||||
);
|
||||
return [...ports, ...ranges].join(" ");
|
||||
},
|
||||
{
|
||||
id: "protocol_filter",
|
||||
accessorFn: (row) => [row.rules?.[0]?.protocol || "all"],
|
||||
filterFn: "arrIncludesSome",
|
||||
},
|
||||
{
|
||||
id: "ports_filter",
|
||||
accessorFn: (row) => {
|
||||
const rule = row.rules?.[0];
|
||||
const ports = rule?.ports || [];
|
||||
const ranges = (rule?.port_ranges || []).map(
|
||||
(r) => `${r.start}-${r.end}`,
|
||||
);
|
||||
return [...ports, ...ranges].join(" ");
|
||||
},
|
||||
filterFn: "includesString",
|
||||
},
|
||||
{
|
||||
id: "has_posture_checks",
|
||||
accessorFn: (row) =>
|
||||
(row.source_posture_checks?.length ?? 0) > 0 ? "with" : "without",
|
||||
filterFn: "equalsString",
|
||||
},
|
||||
{
|
||||
id: "direction_filter",
|
||||
accessorFn: (row) => !!row.rules?.[0]?.bidirectional,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ cell }) => <AccessControlActionCell policy={cell.row.original} />,
|
||||
},
|
||||
];
|
||||
}
|
||||
filterFn: "includesString",
|
||||
},
|
||||
{
|
||||
id: "has_posture_checks",
|
||||
accessorFn: (row) =>
|
||||
(row.source_posture_checks?.length ?? 0) > 0 ? "with" : "without",
|
||||
filterFn: "equalsString",
|
||||
},
|
||||
{
|
||||
id: "direction_filter",
|
||||
accessorFn: (row) => !!row.rules?.[0]?.bidirectional,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ cell }) => <AccessControlActionCell policy={cell.row.original} />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function AccessControlTable({
|
||||
policies,
|
||||
@@ -249,8 +241,6 @@ export default function AccessControlTable({
|
||||
|
||||
const [firewallGPTOpen, setFirewallGPTOpen] = useState(false);
|
||||
|
||||
const t = useTranslations("policies");
|
||||
const tCommon = useTranslations("common");
|
||||
const [showTemporaryPolicies, setShowTemporaryPolicies] = useState(false);
|
||||
|
||||
const withTemporaryPolicies = useCallback(
|
||||
@@ -290,37 +280,37 @@ export default function AccessControlTable({
|
||||
// Inactive ButtonGroup. Routed through the consolidated Filters UI.
|
||||
const statusOptions = useMemo<RadioOption<boolean | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: tCommon("all"), dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: tCommon("enabled"), dotClass: "bg-green-500" },
|
||||
{ value: false, label: tCommon("disabled"), dotClass: "bg-nb-gray-700" },
|
||||
{ value: undefined, label: "All", dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: "Enabled", dotClass: "bg-green-500" },
|
||||
{ value: false, label: "Disabled", dotClass: "bg-nb-gray-700" },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const protocolOptions = useMemo<CheckboxOption<string>[]>(
|
||||
() => [
|
||||
{ value: "tcp", label: t("tcp") },
|
||||
{ value: "udp", label: t("udp") },
|
||||
{ value: "icmp", label: t("icmp") },
|
||||
{ value: "netbird-ssh", label: t("netbirdSsh") },
|
||||
{ value: "tcp", label: "TCP" },
|
||||
{ value: "udp", label: "UDP" },
|
||||
{ value: "icmp", label: "ICMP" },
|
||||
{ value: "netbird-ssh", label: "NetBird SSH" },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const postureOptions = useMemo<RadioOption<string | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: tCommon("all") },
|
||||
{ value: "with", label: t("filterWith") },
|
||||
{ value: "without", label: t("filterWithout") },
|
||||
{ value: undefined, label: "All" },
|
||||
{ value: "with", label: "With" },
|
||||
{ value: "without", label: "Without" },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const directionOptions = useMemo<RadioOption<boolean | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: tCommon("all") },
|
||||
{ value: true, label: t("bidirectional") },
|
||||
{ value: false, label: t("oneWay") },
|
||||
{ value: undefined, label: "All" },
|
||||
{ value: true, label: "Bidirectional" },
|
||||
{ value: false, label: "One-way" },
|
||||
],
|
||||
[],
|
||||
);
|
||||
@@ -352,7 +342,7 @@ export default function AccessControlTable({
|
||||
() => [
|
||||
{
|
||||
id: "enabled",
|
||||
label: tCommon("status"),
|
||||
label: "Status",
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as boolean | undefined}
|
||||
@@ -366,7 +356,7 @@ export default function AccessControlTable({
|
||||
},
|
||||
{
|
||||
id: "source_group_names",
|
||||
label: t("sources"),
|
||||
label: "Sources",
|
||||
renderPicker: (p) => (
|
||||
<GroupsPicker
|
||||
value={p.value as string[] | undefined}
|
||||
@@ -379,7 +369,7 @@ export default function AccessControlTable({
|
||||
},
|
||||
{
|
||||
id: "destination_group_names",
|
||||
label: t("destinations"),
|
||||
label: "Destinations",
|
||||
renderPicker: (p) => (
|
||||
<GroupsPicker
|
||||
value={p.value as string[] | undefined}
|
||||
@@ -392,7 +382,7 @@ export default function AccessControlTable({
|
||||
},
|
||||
{
|
||||
id: "direction_filter",
|
||||
label: t("direction"),
|
||||
label: "Direction",
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as boolean | undefined}
|
||||
@@ -406,7 +396,7 @@ export default function AccessControlTable({
|
||||
},
|
||||
{
|
||||
id: "protocol_filter",
|
||||
label: t("protocol"),
|
||||
label: "Protocol",
|
||||
renderPicker: (p) => (
|
||||
<CheckboxListPicker
|
||||
value={p.value as string[] | undefined}
|
||||
@@ -419,25 +409,25 @@ export default function AccessControlTable({
|
||||
formatCheckboxChip(
|
||||
v as string[] | undefined,
|
||||
protocolOptions,
|
||||
t("protocols"),
|
||||
"protocols",
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "ports_filter",
|
||||
label: t("filterPort"),
|
||||
label: "Port",
|
||||
renderPicker: (p) => (
|
||||
<TextInputPicker
|
||||
value={p.value as string | undefined}
|
||||
onChange={p.onChange}
|
||||
close={p.close}
|
||||
placeholder={t("portsPlaceholder")}
|
||||
placeholder={"e.g. 443"}
|
||||
/>
|
||||
),
|
||||
formatChip: (v) => formatTextChip(v as string | undefined),
|
||||
},
|
||||
{
|
||||
id: "has_posture_checks",
|
||||
label: t("filterPostureChecks"),
|
||||
label: "Posture Checks",
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as string | undefined}
|
||||
@@ -456,8 +446,6 @@ export default function AccessControlTable({
|
||||
postureOptions,
|
||||
directionOptions,
|
||||
tableGroups,
|
||||
t,
|
||||
tCommon,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -494,12 +482,12 @@ export default function AccessControlTable({
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
text={t("tableHeading")}
|
||||
text={"Access Control Policies"}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
initialPageSize={25}
|
||||
showResetFilterButton={false}
|
||||
columns={createAccessControlTableColumns(t, tCommon)}
|
||||
columns={AccessControlTableColumns}
|
||||
aboveTable={(table) => (
|
||||
<TableFilterChips table={table} filters={filterDefs} />
|
||||
)}
|
||||
@@ -522,13 +510,15 @@ export default function AccessControlTable({
|
||||
setEditModal(true);
|
||||
setCurrentCellClicked(cell);
|
||||
}}
|
||||
searchPlaceholder={t("searchByNameAndDescription")}
|
||||
searchPlaceholder={"Search by name and description..."}
|
||||
getStartedCard={
|
||||
isGroupPage ? (
|
||||
<NoResults
|
||||
className={"py-4"}
|
||||
title={t("noPoliciesForGroup")}
|
||||
description={t("noPoliciesForGroupDescription")}
|
||||
title={"This group is not used within any policies yet"}
|
||||
description={
|
||||
"Assign this group as either a source or destination inside a policy to see them listed here."
|
||||
}
|
||||
icon={
|
||||
<AccessControlIcon size={20} className={"fill-nb-gray-300"} />
|
||||
}
|
||||
@@ -541,7 +531,7 @@ export default function AccessControlTable({
|
||||
disabled={!permission.policies.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
{t("addPolicy")}
|
||||
Add Policy
|
||||
</Button>
|
||||
</AccessControlModal>
|
||||
</div>
|
||||
@@ -560,8 +550,10 @@ export default function AccessControlTable({
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={t("createNewPolicy")}
|
||||
description={t("createNewPolicyDescription")}
|
||||
title={"Create New Policy"}
|
||||
description={
|
||||
"It looks like you don't have any policies yet. Policies can allow connections by specific protocol and ports."
|
||||
}
|
||||
button={
|
||||
<div className={"flex gap-4 items-center justify-center"}>
|
||||
<FirewallGPTButton onClick={() => setFirewallGPTOpen(true)} />
|
||||
@@ -571,21 +563,21 @@ export default function AccessControlTable({
|
||||
disabled={!permission.policies.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
{t("addPolicy")}
|
||||
Add Policy
|
||||
</Button>
|
||||
</AccessControlModal>
|
||||
</div>
|
||||
}
|
||||
learnMore={
|
||||
<>
|
||||
{t("learnMoreAbout")}
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/manage-network-access"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("accessControls")}
|
||||
Access Controls
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
@@ -606,7 +598,7 @@ export default function AccessControlTable({
|
||||
data-testid="open-add-policy"
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
{t("addPolicy")}
|
||||
Add Policy
|
||||
</Button>
|
||||
</AccessControlModal>
|
||||
</div>
|
||||
@@ -636,7 +628,9 @@ export default function AccessControlTable({
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"max-w-sm text-xs"}>
|
||||
{t("temporaryPoliciesTooltip")}
|
||||
Show temporary policies created by the NetBird browser
|
||||
client. These policies are ephemeral and will be deleted
|
||||
automatically after a short period of time.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -4,7 +4,6 @@ import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import { merge, orderBy, uniqBy } from "lodash";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { usePolicies } from "@/contexts/PoliciesProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import {
|
||||
@@ -44,7 +43,6 @@ export const useAccessControl = ({
|
||||
initialPorts,
|
||||
initialDestinationResource,
|
||||
}: Props = {}) => {
|
||||
const t = useTranslations("policies");
|
||||
const { data: allPostureChecks, isLoading: isPostureChecksLoading } =
|
||||
useFetchApi<PostureCheck[]>("/posture-checks");
|
||||
|
||||
@@ -322,13 +320,13 @@ export const useAccessControl = ({
|
||||
mutate("/policies");
|
||||
onSuccess && onSuccess(p);
|
||||
},
|
||||
t("policySaveSuccess"),
|
||||
"The policy was successfully saved",
|
||||
);
|
||||
} else {
|
||||
notify({
|
||||
title: t("createPolicyTitle"),
|
||||
description: t("createPolicySuccess"),
|
||||
loadingMessage: t("createPolicyLoading"),
|
||||
title: "Create Access Control Policy",
|
||||
description: "Policy was created successfully.",
|
||||
loadingMessage: "Creating your policy...",
|
||||
promise: policyRequest.post(policyObj).then((policy) => {
|
||||
mutate("/policies");
|
||||
onSuccess && onSuccess(policy);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { cn, generateColorFromUser } from "@utils/helpers";
|
||||
import dayjs from "dayjs";
|
||||
import { AlertCircle, ArrowUpRight, Cog, PlusIcon, XIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useMemo } from "react";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
||||
@@ -24,7 +23,6 @@ const ActionIcons: Record<ActionColor, React.ReactNode> = {
|
||||
|
||||
export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
|
||||
const { users } = useUsers();
|
||||
const t = useTranslations("activity");
|
||||
|
||||
const getActivityUser = () => {
|
||||
let user;
|
||||
@@ -97,7 +95,7 @@ export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
|
||||
|
||||
<span className={"text-sm text-nb-gray-200"}>
|
||||
<TextWithTooltip
|
||||
text={user?.name || user?.id || t("system")}
|
||||
text={user?.name || user?.id || "System"}
|
||||
maxChars={20}
|
||||
/>
|
||||
</span>
|
||||
@@ -107,7 +105,7 @@ export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
|
||||
{isExternal && (
|
||||
<span className={"flex items-center"}>
|
||||
<SmallBadge
|
||||
text={t("external")}
|
||||
text={"External"}
|
||||
variant={"sky"}
|
||||
className={
|
||||
"text-[10px] py-[0.2rem] px-1.5 rounded-full leading-none -top-0"
|
||||
|
||||
@@ -7,7 +7,6 @@ import { cn } from "@utils/helpers";
|
||||
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
||||
import { trim, uniqBy } from "lodash";
|
||||
import { ChevronsUpDown, Layers, SearchIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useElementSize } from "@/hooks/useElementSize";
|
||||
@@ -28,7 +27,6 @@ export function ActivityEventCodeSelector({
|
||||
popoverWidth = 400,
|
||||
events,
|
||||
}: MultiSelectProps) {
|
||||
const t = useTranslations("activity");
|
||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -52,7 +50,7 @@ export function ActivityEventCodeSelector({
|
||||
activity_code: event.activity_code,
|
||||
activity: event.activity,
|
||||
group: event.activity_code.startsWith("service.user")
|
||||
? t("serviceUser")
|
||||
? "Service User"
|
||||
: event.activity_code.split(".")[0],
|
||||
};
|
||||
});
|
||||
@@ -83,9 +81,9 @@ export function ActivityEventCodeSelector({
|
||||
<Layers size={16} className={"shrink-0"} />
|
||||
<div className={"w-full flex justify-between"}>
|
||||
{values.length > 0 ? (
|
||||
<div>{t("eventCount", { count: values.length })}</div>
|
||||
<div>{values.length} Event(s)</div>
|
||||
) : (
|
||||
t("allEventTypes")
|
||||
"All Event Types"
|
||||
)}
|
||||
<div className={"pl-2"}>
|
||||
<ChevronsUpDown size={18} className={"shrink-0"} />
|
||||
@@ -124,7 +122,7 @@ export function ActivityEventCodeSelector({
|
||||
ref={searchRef}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
placeholder={t("searchEvent")}
|
||||
placeholder={"Search event..."}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function ActivityTable({
|
||||
events={events ?? []}
|
||||
/>
|
||||
),
|
||||
formatChip: (v) => formatActivityTypeChip(v as string[] | undefined, t),
|
||||
formatChip: (v) => formatActivityTypeChip(v as string[] | undefined),
|
||||
},
|
||||
{
|
||||
id: "initiator_email",
|
||||
|
||||
@@ -7,7 +7,6 @@ import { cn } from "@utils/helpers";
|
||||
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
||||
import { trim, uniqBy } from "lodash";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
||||
@@ -35,7 +34,6 @@ export function ActivityTypePicker({
|
||||
onChange,
|
||||
events,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("activity");
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const selected = value ?? [];
|
||||
|
||||
@@ -45,7 +43,7 @@ export function ActivityTypePicker({
|
||||
activity_code: event.activity_code,
|
||||
activity: event.activity,
|
||||
group: event.activity_code.startsWith("service.user")
|
||||
? t("serviceUser")
|
||||
? "Service User"
|
||||
: event.activity_code.split(".")[0],
|
||||
}));
|
||||
return items.reduce<Record<string, GroupedItem[]>>((acc, item) => {
|
||||
@@ -82,7 +80,7 @@ export function ActivityTypePicker({
|
||||
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-9",
|
||||
)}
|
||||
ref={searchRef}
|
||||
placeholder={t("searchEvent")}
|
||||
placeholder={"Search event..."}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
@@ -152,9 +150,8 @@ export function ActivityTypePicker({
|
||||
|
||||
export function formatActivityTypeChip(
|
||||
value: string[] | undefined,
|
||||
t?: (key: string, params?: Record<string, any>) => string,
|
||||
): string | null {
|
||||
if (!value || value.length === 0) return null;
|
||||
if (value.length === 1) return value[0];
|
||||
return t ? t("typeCount", { count: value.length }) : `${value.length} types`;
|
||||
return `${value.length} types`;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useSearch } from "@hooks/useSearch";
|
||||
import { generateColorFromString } from "@utils/helpers";
|
||||
import { sortBy, uniqBy } from "lodash";
|
||||
import { ChevronsUpDown, Cog, UserCircle2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useElementSize } from "@/hooks/useElementSize";
|
||||
@@ -49,12 +48,11 @@ export function UsersDropdownSelector({
|
||||
popoverWidth = 250,
|
||||
options,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("activity");
|
||||
const [filteredItems, search, setSearch] = useSearch(
|
||||
options.concat({
|
||||
id: "all-users",
|
||||
name: t("allUsers"),
|
||||
email: t("includeAllUsers"),
|
||||
name: "All Users",
|
||||
email: "Include all users",
|
||||
}),
|
||||
searchPredicate,
|
||||
{ filter: true, debounce: 150 },
|
||||
@@ -109,7 +107,7 @@ export function UsersDropdownSelector({
|
||||
{!selectedUser ? (
|
||||
<React.Fragment>
|
||||
<UserCircle2 size={16} />
|
||||
{t("allUsers")}
|
||||
All Users
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
@@ -138,7 +136,7 @@ export function UsersDropdownSelector({
|
||||
<TextWithTooltip
|
||||
text={
|
||||
selectedUser?.email === "NetBird"
|
||||
? t("system")
|
||||
? "System"
|
||||
: selectedUser?.name
|
||||
}
|
||||
maxChars={20}
|
||||
@@ -167,14 +165,14 @@ export function UsersDropdownSelector({
|
||||
<DropdownInput
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
placeholder={t("searchUser")}
|
||||
placeholder={"Search user..."}
|
||||
hideEnterIcon={true}
|
||||
/>
|
||||
|
||||
{options.length == 0 && !search && (
|
||||
<div className={"max-w-xs mx-auto"}>
|
||||
<DropdownInfoText>
|
||||
{t("noUsersAvailable")}
|
||||
{"No users available to select."}
|
||||
</DropdownInfoText>
|
||||
</div>
|
||||
)}
|
||||
@@ -182,7 +180,7 @@ export function UsersDropdownSelector({
|
||||
{filteredItems.length == 0 && search != "" && (
|
||||
<div className={"px-10"}>
|
||||
<DropdownInfoText>
|
||||
{t("noUsersMatching")}
|
||||
There are no users matching your search.
|
||||
</DropdownInfoText>
|
||||
</div>
|
||||
)}
|
||||
@@ -229,7 +227,7 @@ export function UsersDropdownSelector({
|
||||
>
|
||||
<TextWithTooltip
|
||||
text={
|
||||
isSystemUser ? t("system") : user?.name || user?.id
|
||||
isSystemUser ? "System" : user?.name || user?.id
|
||||
}
|
||||
maxChars={20}
|
||||
/>
|
||||
@@ -248,7 +246,7 @@ export function UsersDropdownSelector({
|
||||
{user.external && (
|
||||
<span className={"flex items-center ml-auto relative"}>
|
||||
<SmallBadge
|
||||
text={t("external")}
|
||||
text={"External"}
|
||||
variant={"sky"}
|
||||
className={
|
||||
"text-[8.5px] py-[0.15rem] px-[.32rem] leading-none rounded-full -top-0"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import HelpText from "@components/HelpText";
|
||||
@@ -115,9 +113,9 @@ export function NameserverModalContent({
|
||||
|
||||
const update = async (groupIds: string[]) => {
|
||||
notify({
|
||||
title: t("updateNameserverNotify"),
|
||||
description: t("nameserverUpdatedSuccess"),
|
||||
loadingMessage: t("updatingNameserver"),
|
||||
title: "Update Nameserver",
|
||||
description: "Nameserver was updated successfully.",
|
||||
loadingMessage: "Updating your nameserver...",
|
||||
promise: nsRequest
|
||||
.put(
|
||||
{
|
||||
@@ -141,9 +139,9 @@ export function NameserverModalContent({
|
||||
|
||||
const create = async (groupIds: string[]) => {
|
||||
notify({
|
||||
title: t("createNameserver"),
|
||||
description: t("nameserverCreatedSuccess"),
|
||||
loadingMessage: t("creatingNameserver"),
|
||||
title: "Create Nameserver",
|
||||
description: "Nameserver was created successfully.",
|
||||
loadingMessage: "Creating your nameserver...",
|
||||
promise: nsRequest
|
||||
.post({
|
||||
name: name,
|
||||
@@ -225,7 +223,7 @@ export function NameserverModalContent({
|
||||
}, [domains]);
|
||||
|
||||
const nameLengthError = useMemo(() => {
|
||||
if (name.length > 40) return t("nameLengthError");
|
||||
if (name.length > 40) return "Name should be less than 40 characters";
|
||||
return "";
|
||||
}, [name]);
|
||||
|
||||
@@ -583,7 +581,6 @@ function NameserverInput({
|
||||
onError?: (error: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}>) {
|
||||
const t = useTranslations("dns");
|
||||
const [ip, setIP] = useState(value.ip);
|
||||
const [port, setPort] = useState<string>(value.port.toString());
|
||||
|
||||
@@ -604,7 +601,7 @@ function NameserverInput({
|
||||
const validCIDR = cidr.isValidAddress(ip);
|
||||
if (!validCIDR) {
|
||||
onError && onError(true);
|
||||
return t("validIPError");
|
||||
return "Please enter a valid IP, e.g., 192.168.1.0";
|
||||
}
|
||||
onError && onError(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -620,7 +617,7 @@ function NameserverInput({
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={"IP"}
|
||||
placeholder={t("ipPlaceholder")}
|
||||
placeholder={"e.g., 172.16.0.0"}
|
||||
maxWidthClass={"w-full"}
|
||||
value={ip}
|
||||
className={"font-mono !text-[13px]"}
|
||||
@@ -633,7 +630,7 @@ function NameserverInput({
|
||||
|
||||
<Input
|
||||
maxWidthClass={"min-w-[150px] max-w-[150px]"}
|
||||
customPrefix={t("port")}
|
||||
customPrefix={"Port"}
|
||||
placeholder={"53"}
|
||||
value={port}
|
||||
type={"number"}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Modal, ModalContent, ModalTrigger } from "@components/modal/Modal";
|
||||
import { cn } from "@utils/helpers";
|
||||
@@ -12,7 +10,6 @@ import Quad9Logo from "@/assets/nameservers/quad9.svg";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { NameserverGroup, NameserverPresets } from "@/interfaces/Nameserver";
|
||||
import NameserverModal from "@/modules/dns/nameservers/NameserverModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -66,9 +63,6 @@ type ModalProps = {
|
||||
export function NameserverTemplateModalContent({
|
||||
onePresetSelection,
|
||||
}: Readonly<ModalProps>) {
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-xl"} showClose={true}>
|
||||
<div className={"px-8 py-3 flex flex-col gap-6 mt-4"}>
|
||||
@@ -106,8 +100,10 @@ export function NameserverTemplateModalContent({
|
||||
<NameserverTemplate
|
||||
onClick={() => onePresetSelection(NameserverPresets.Default)}
|
||||
icon={<GlobeIcon size={30} className={"text-netbird"} />}
|
||||
title={t("customDNS")}
|
||||
description={t("customDNSDescription")}
|
||||
title={"Custom DNS"}
|
||||
description={
|
||||
"Use custom nameservers to resolve domains in your network. You can either use a public DNS or your own nameservers."
|
||||
}
|
||||
data-testid="nameserver-preset-custom"
|
||||
/>
|
||||
</div>
|
||||
@@ -135,7 +131,6 @@ function NameserverTemplate({
|
||||
hrefTitle?: string;
|
||||
"data-testid"?: string;
|
||||
}>) {
|
||||
const tCommon = useTranslations("common");
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
@@ -170,7 +165,7 @@ function NameserverTemplate({
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{hrefTitle || tCommon("learnMore")}
|
||||
{hrefTitle || "Learn more"}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -17,7 +15,6 @@ import { useSWRConfig } from "swr";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { NameserverGroup } from "@/interfaces/Nameserver";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
ns: NameserverGroup;
|
||||
@@ -28,8 +25,6 @@ export default function NameserverActionCell({ ns }: Readonly<Props>) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const { permission } = usePermissions();
|
||||
const [open, setOpen] = useState(false);
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
const canUpdate = permission.nameservers.update;
|
||||
const canDelete = permission.nameservers.delete;
|
||||
@@ -38,10 +33,11 @@ export default function NameserverActionCell({ ns }: Readonly<Props>) {
|
||||
const enabled = !ns.enabled;
|
||||
notify({
|
||||
title: ns.name,
|
||||
description: t("nameserverToggleSuccess", {
|
||||
status: enabled ? tCommon("enabled").toLowerCase() : tCommon("disabled").toLowerCase(),
|
||||
}),
|
||||
loadingMessage: t("nameserverToggleLoading"),
|
||||
description:
|
||||
"Nameserver was successfully" +
|
||||
(enabled ? " enabled" : " disabled") +
|
||||
".",
|
||||
loadingMessage: "Updating your nameserver...",
|
||||
promise: nsRequest
|
||||
.put(
|
||||
{
|
||||
@@ -64,21 +60,22 @@ export default function NameserverActionCell({ ns }: Readonly<Props>) {
|
||||
|
||||
const deleteRule = async () => {
|
||||
notify({
|
||||
title: tCommon("delete") + " " + ns.name,
|
||||
description: t("nameserverDeletedSuccess"),
|
||||
title: "Nameserver " + ns.name,
|
||||
description: "The nameserver was successfully removed.",
|
||||
promise: nsRequest.del("", `/${ns.id}`).then(() => {
|
||||
mutate("/dns/nameservers");
|
||||
}),
|
||||
loadingMessage: t("deletingNameserver"),
|
||||
loadingMessage: "Deleting the nameserver...",
|
||||
});
|
||||
};
|
||||
|
||||
const openConfirm = async () => {
|
||||
const choice = await confirm({
|
||||
title: t("confirmDeleteNameserverTitle", { name: ns.name }),
|
||||
description: t("confirmDeleteNameserver"),
|
||||
confirmText: tCommon("delete"),
|
||||
cancelText: tCommon("cancel"),
|
||||
title: `Delete '${ns.name}'?`,
|
||||
description:
|
||||
"Are you sure you want to delete this nameserver? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
if (!choice) return;
|
||||
@@ -98,7 +95,7 @@ export default function NameserverActionCell({ ns }: Readonly<Props>) {
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className={"!px-3"}
|
||||
aria-label={t("nameserverActionsAria")}
|
||||
aria-label={"Nameserver actions"}
|
||||
data-testid={"nameserver-actions"}
|
||||
>
|
||||
<MoreVertical size={16} className={"shrink-0"} />
|
||||
@@ -115,7 +112,7 @@ export default function NameserverActionCell({ ns }: Readonly<Props>) {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<PowerIcon size={14} className={"shrink-0"} />
|
||||
{ns.enabled ? t("disable") : t("enable")}
|
||||
{ns.enabled ? "Disable" : "Enable"}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -127,7 +124,7 @@ export default function NameserverActionCell({ ns }: Readonly<Props>) {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
{tCommon("delete")}
|
||||
Delete
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
@@ -42,13 +40,12 @@ import NameserverDistributionGroupsCell from "@/modules/dns/nameservers/table/Na
|
||||
import NameserverMatchDomainsCell from "@/modules/dns/nameservers/table/NameserverMatchDomainsCell";
|
||||
import NameserverNameCell from "@/modules/dns/nameservers/table/NameserverNameCell";
|
||||
import NameserverNameserversCell from "@/modules/dns/nameservers/table/NameserverNameserversCell";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const getColumns = (t: (key: string, values?: any) => string, tCommon: (key: string, values?: any) => string): ColumnDef<NameserverGroup>[] => [
|
||||
export const NameserverGroupTableColumns: ColumnDef<NameserverGroup>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{tCommon("name")}</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>Name</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => <NameserverNameCell ns={row.original} />,
|
||||
@@ -73,7 +70,7 @@ const getColumns = (t: (key: string, values?: any) => string, tCommon: (key: str
|
||||
accessorFn: (row) => row.domains?.length || 0,
|
||||
id: "domains",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("matchDomains")}</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>Match Domains</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <NameserverMatchDomainsCell ns={row.original} />,
|
||||
},
|
||||
@@ -81,7 +78,7 @@ const getColumns = (t: (key: string, values?: any) => string, tCommon: (key: str
|
||||
accessorFn: (row) => row.nameservers?.length || 0,
|
||||
id: "nameservers",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("nameservers")}</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>Nameservers</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <NameserverNameserversCell ns={row.original} />,
|
||||
},
|
||||
@@ -89,7 +86,7 @@ const getColumns = (t: (key: string, values?: any) => string, tCommon: (key: str
|
||||
accessorFn: (row) => row.groups?.length || 0,
|
||||
id: "groups",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{tCommon("distributionGroups")}</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>Groups</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <NameserverDistributionGroupsCell ns={row.original} />,
|
||||
},
|
||||
@@ -126,8 +123,6 @@ export default function NameserverGroupTable({
|
||||
const path = usePathname();
|
||||
const { permission } = usePermissions();
|
||||
const { groups } = useGroups();
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
const nameserverGroupsWithNames = useMemo(() => {
|
||||
if (!nameserverGroups) return [];
|
||||
@@ -167,18 +162,18 @@ export default function NameserverGroupTable({
|
||||
|
||||
const statusOptions = useMemo<RadioOption<boolean | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: tCommon("all"), dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: tCommon("active"), dotClass: "bg-green-500" },
|
||||
{ value: false, label: tCommon("inactive"), dotClass: "bg-nb-gray-700" },
|
||||
{ value: undefined, label: "All", dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: "Active", dotClass: "bg-green-500" },
|
||||
{ value: false, label: "Inactive", dotClass: "bg-nb-gray-700" },
|
||||
],
|
||||
[tCommon],
|
||||
[],
|
||||
);
|
||||
|
||||
const filterDefs = useMemo<TableFilterDef[]>(
|
||||
() => [
|
||||
{
|
||||
id: "enabled",
|
||||
label: tCommon("status"),
|
||||
label: "Status",
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as boolean | undefined}
|
||||
@@ -192,7 +187,7 @@ export default function NameserverGroupTable({
|
||||
},
|
||||
{
|
||||
id: "group_names_filter",
|
||||
label: tCommon("distributionGroups"),
|
||||
label: "Groups",
|
||||
renderPicker: (p) => (
|
||||
<GroupsPicker
|
||||
value={p.value as string[] | undefined}
|
||||
@@ -204,7 +199,7 @@ export default function NameserverGroupTable({
|
||||
formatChip: (v) => formatGroupsChip(v as string[] | undefined),
|
||||
},
|
||||
],
|
||||
[statusOptions, tableGroups, tCommon],
|
||||
[statusOptions, tableGroups],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -220,7 +215,7 @@ export default function NameserverGroupTable({
|
||||
<DataTable
|
||||
headingTarget={headingTarget}
|
||||
isLoading={isLoading}
|
||||
text={t("nameservers")}
|
||||
text={"Network Routes"}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
wrapperComponent={isGroupPage ? Card : undefined}
|
||||
@@ -248,16 +243,18 @@ export default function NameserverGroupTable({
|
||||
setEditModal(true);
|
||||
setCurrentCellClicked(cell);
|
||||
}}
|
||||
columns={getColumns(t, tCommon)}
|
||||
columns={NameserverGroupTableColumns}
|
||||
data={nameserverGroupsWithNames}
|
||||
searchPlaceholder={t("searchNameserverPlaceholder")}
|
||||
searchPlaceholder={"Search by name, domains or nameservers..."}
|
||||
getStartedCard={
|
||||
isGroupPage ? (
|
||||
<NoResults
|
||||
icon={<DNSIcon className={"fill-nb-gray-200"} size={20} />}
|
||||
className={"py-4"}
|
||||
title={t("noNameserversGroupTitle")}
|
||||
description={t("noNameserversGroupDesc")}
|
||||
title={"This group is not used within any nameservers yet"}
|
||||
description={
|
||||
"Assign this group as a distribution group in your nameservers to see them listed here."
|
||||
}
|
||||
>
|
||||
<NameserverTemplateModal distributionGroups={distributionGroups}>
|
||||
<Button
|
||||
@@ -266,7 +263,7 @@ export default function NameserverGroupTable({
|
||||
disabled={!permission.nameservers.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
{t("createNameserver")}
|
||||
Add Nameserver
|
||||
</Button>
|
||||
</NameserverTemplateModal>
|
||||
</NoResults>
|
||||
@@ -279,8 +276,10 @@ export default function NameserverGroupTable({
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={t("createNameserver")}
|
||||
description={t("noNameserversGetStartedDesc")}
|
||||
title={"Create Nameserver"}
|
||||
description={
|
||||
"It looks like you don't have any nameservers. Get started by adding one to your network. Select a predefined or add your custom nameservers."
|
||||
}
|
||||
button={
|
||||
<div className={"flex flex-col"}>
|
||||
<div>
|
||||
@@ -294,7 +293,7 @@ export default function NameserverGroupTable({
|
||||
data-testid="open-add-nameserver"
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
{t("createNameserver")}
|
||||
Add Nameserver
|
||||
</Button>
|
||||
</NameserverTemplateModal>
|
||||
</div>
|
||||
@@ -302,14 +301,14 @@ export default function NameserverGroupTable({
|
||||
}
|
||||
learnMore={
|
||||
<>
|
||||
{t("learnMoreAbout")}
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/manage-dns-in-your-network"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("dns")}
|
||||
DNS
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
@@ -328,7 +327,7 @@ export default function NameserverGroupTable({
|
||||
data-testid="open-add-nameserver"
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
{t("createNameserver")}
|
||||
Add Nameserver
|
||||
</Button>
|
||||
</NameserverTemplateModal>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -10,13 +8,11 @@ import {
|
||||
import { Server } from "lucide-react";
|
||||
import React from "react";
|
||||
import { NameserverGroup } from "@/interfaces/Nameserver";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
ns: NameserverGroup;
|
||||
};
|
||||
export default function NameserverNameserversCell({ ns }: Props) {
|
||||
const t = useTranslations("dns");
|
||||
const nameservers = ns.nameservers ?? [];
|
||||
|
||||
if (nameservers.length > 3) {
|
||||
@@ -26,7 +22,7 @@ export default function NameserverNameserversCell({ ns }: Props) {
|
||||
<TooltipTrigger asChild={true}>
|
||||
<Badge variant={"gray"} className={"font-mono cursor-help"}>
|
||||
<Server size={10} className={"mr-1"} />
|
||||
{t("serverCount", { count: nameservers.length })}
|
||||
{nameservers.length} Servers
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={"p-3"}>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
@@ -110,7 +108,7 @@ export function DNSRecordModalContent({
|
||||
allowOnlyTld: true,
|
||||
});
|
||||
if (!valid) {
|
||||
return tCommon("validDomainError");
|
||||
return "Please enter a valid domain, e.g. example.com or intra.example.com";
|
||||
}
|
||||
}, [domain]);
|
||||
|
||||
@@ -118,7 +116,7 @@ export function DNSRecordModalContent({
|
||||
if (recordValue === "" || type !== "A") return "";
|
||||
const valid = Address4.isValid(recordValue);
|
||||
if (!valid) {
|
||||
return t("validIPv4Error");
|
||||
return "Please enter a valid IPv4 address, e.g. 192.168.1.1";
|
||||
}
|
||||
}, [recordValue, type]);
|
||||
|
||||
@@ -126,7 +124,7 @@ export function DNSRecordModalContent({
|
||||
if (recordValue === "" || type !== "AAAA") return "";
|
||||
const valid = Address6.isValid(recordValue);
|
||||
if (!valid) {
|
||||
return t("validIPv6Error");
|
||||
return "Please enter a valid IPv6 address, e.g. 2001:0db8:85a3::8a2e:0370:7334";
|
||||
}
|
||||
}, [recordValue, type]);
|
||||
|
||||
@@ -137,7 +135,7 @@ export function DNSRecordModalContent({
|
||||
allowOnlyTld: false,
|
||||
});
|
||||
if (!valid) {
|
||||
return t("validCnameError");
|
||||
return "Please enter a valid domain, e.g. example.com or server.example.com";
|
||||
}
|
||||
}, [recordValue, type]);
|
||||
|
||||
@@ -178,8 +176,8 @@ export function DNSRecordModalContent({
|
||||
title={record ? t("updateDNSRecord") : t("addDNSRecord")}
|
||||
description={
|
||||
record
|
||||
? t("updateRecordDesc", { zone: zone.domain })
|
||||
: t("addRecordDesc", { zone: zone.domain })
|
||||
? `Update record of '${zone.domain}' zone`
|
||||
: `Add new record to the '${zone.domain}' zone`
|
||||
}
|
||||
icon={<GlobeIcon size={16} />}
|
||||
/>
|
||||
@@ -244,7 +242,7 @@ export function DNSRecordModalContent({
|
||||
<Label>{t("ipv4Address")}</Label>
|
||||
<Input
|
||||
className={"mt-1.5 font-mono text-[0.82rem]"}
|
||||
placeholder={t("ipv4Placeholder")}
|
||||
placeholder={"192.168.1.1"}
|
||||
errorTooltip={false}
|
||||
errorTooltipPosition={"top"}
|
||||
error={ipv4Error}
|
||||
@@ -261,7 +259,7 @@ export function DNSRecordModalContent({
|
||||
<Label>{t("ipv6Address")}</Label>
|
||||
<Input
|
||||
className={"mt-1.5 font-mono text-[0.82rem]"}
|
||||
placeholder={t("ipv6Placeholder")}
|
||||
placeholder={"2001:0db8:85a3::8a2e:0370:7334"}
|
||||
errorTooltip={false}
|
||||
errorTooltipPosition={"top"}
|
||||
error={ipv6Error}
|
||||
@@ -278,7 +276,7 @@ export function DNSRecordModalContent({
|
||||
<Label>{t("targetDomain")}</Label>
|
||||
<Input
|
||||
className={"mt-1.5"}
|
||||
placeholder={t("cnamePlaceholder")}
|
||||
placeholder={"e.g., example.com or intra.example.com"}
|
||||
errorTooltip={false}
|
||||
errorTooltipPosition={"top"}
|
||||
error={cnameError}
|
||||
@@ -304,16 +302,16 @@ export function DNSRecordModalContent({
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="60">{getTTLLabel(60, t)}</SelectItem>
|
||||
<SelectItem value="120">{getTTLLabel(120, t)}</SelectItem>
|
||||
<SelectItem value="300">{getTTLLabel(300, t)}</SelectItem>
|
||||
<SelectItem value="600">{getTTLLabel(600, t)}</SelectItem>
|
||||
<SelectItem value="900">{getTTLLabel(900, t)}</SelectItem>
|
||||
<SelectItem value="1800">{getTTLLabel(1800, t)}</SelectItem>
|
||||
<SelectItem value="3600">{getTTLLabel(3600, t)}</SelectItem>
|
||||
<SelectItem value="7200">{getTTLLabel(7200, t)}</SelectItem>
|
||||
<SelectItem value="43200">{getTTLLabel(43200, t)}</SelectItem>
|
||||
<SelectItem value="86400">{getTTLLabel(86400, t)}</SelectItem>
|
||||
<SelectItem value="60">{getTTLLabel(60)}</SelectItem>
|
||||
<SelectItem value="120">{getTTLLabel(120)}</SelectItem>
|
||||
<SelectItem value="300">{getTTLLabel(300)}</SelectItem>
|
||||
<SelectItem value="600">{getTTLLabel(600)}</SelectItem>
|
||||
<SelectItem value="900">{getTTLLabel(900)}</SelectItem>
|
||||
<SelectItem value="1800">{getTTLLabel(1800)}</SelectItem>
|
||||
<SelectItem value="3600">{getTTLLabel(3600)}</SelectItem>
|
||||
<SelectItem value="7200">{getTTLLabel(7200)}</SelectItem>
|
||||
<SelectItem value="43200">{getTTLLabel(43200)}</SelectItem>
|
||||
<SelectItem value="86400">{getTTLLabel(86400)}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -352,22 +350,16 @@ export function DNSRecordModalContent({
|
||||
);
|
||||
}
|
||||
|
||||
export const getTTLLabel = (seconds: number, t?: (key: string, values?: any) => string): string => {
|
||||
const s = t ? t("sec") : "Sec.";
|
||||
const m = t ? t("min") : "Min.";
|
||||
const h = t ? t("hour") : "Hour";
|
||||
const hs = t ? t("hours") : "Hours";
|
||||
const d = t ? t("day") : "Day";
|
||||
const ds = t ? t("days") : "Days";
|
||||
if (seconds < 60) return `${seconds} ${s}`;
|
||||
export const getTTLLabel = (seconds: number): string => {
|
||||
if (seconds < 60) return `${seconds} Sec.`;
|
||||
if (seconds < 3600) {
|
||||
const minutes = seconds / 60;
|
||||
return `${minutes} ${m}`;
|
||||
return minutes === 1 ? "1 Min." : `${minutes} Min.`;
|
||||
}
|
||||
if (seconds < 86400) {
|
||||
const hours = seconds / 3600;
|
||||
return `${hours} ${hs}`;
|
||||
return hours === 1 ? "1 Hour" : `${hours} Hours`;
|
||||
}
|
||||
const days = seconds / 86400;
|
||||
return `${days} ${ds}`;
|
||||
return days === 1 ? "1 Day" : `${days} Days`;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import HelpText from "@components/HelpText";
|
||||
@@ -90,8 +88,6 @@ export function DNSZoneModalContent({
|
||||
initial: initialDistributionGroups ?? zone?.distribution_groups ?? [],
|
||||
});
|
||||
|
||||
const t = useTranslations("dns");
|
||||
|
||||
const domainError = useMemo(() => {
|
||||
if (domain == "") return "";
|
||||
const valid = validator.isValidDomain(domain, {
|
||||
@@ -100,9 +96,9 @@ export function DNSZoneModalContent({
|
||||
preventLeadingAndTrailingDots: true,
|
||||
});
|
||||
if (!valid) {
|
||||
return t("validDomainErrorZone");
|
||||
return "Please enter a valid domain, e.g. internal, company.internal or intra.example.com";
|
||||
}
|
||||
}, [domain, t]);
|
||||
}, [domain]);
|
||||
|
||||
const handleOnSubmit = async () => {
|
||||
return saveGroups().then((distributionGroups) => {
|
||||
@@ -131,6 +127,7 @@ export function DNSZoneModalContent({
|
||||
|
||||
const canUpdateOrCreate = !domainError && groups?.length > 0 && domain !== "";
|
||||
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { notify } from "@components/Notification";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import * as React from "react";
|
||||
@@ -10,7 +8,6 @@ import { DNSRecord, DNSZone } from "@/interfaces/DNS";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import DNSRecordModal from "@/modules/dns/zones/DNSRecordModal";
|
||||
import DNSZoneModal from "@/modules/dns/zones/DNSZoneModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
@@ -44,8 +41,6 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
const [initialDistributionGroups, setInitialDistributionGroups] =
|
||||
useState<Group[]>();
|
||||
const { confirm } = useDialog();
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
const createZone = async (zone: DNSZone): Promise<DNSZone> => {
|
||||
const promise = zoneRequest.post(zone).then((zone) => {
|
||||
@@ -54,10 +49,10 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
});
|
||||
|
||||
notify({
|
||||
title: t("notifyZoneAddedTitle", { name: zone.domain }),
|
||||
description: t("notifyZoneAddedDesc"),
|
||||
title: `DNS Zone '${zone.domain}'`,
|
||||
description: `DNS Zone was added successfully.`,
|
||||
promise: promise,
|
||||
loadingMessage: t("notifyZoneAddedLoading"),
|
||||
loadingMessage: "Adding DNS Zone...",
|
||||
});
|
||||
|
||||
return promise;
|
||||
@@ -71,10 +66,10 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
});
|
||||
|
||||
notify({
|
||||
title: t("notifyZoneUpdatedTitle", { name: zone.domain }),
|
||||
description: t("notifyZoneUpdatedDesc"),
|
||||
title: `DNS Zone '${zone.domain}'`,
|
||||
description: `DNS Zone was updated successfully.`,
|
||||
promise: promise,
|
||||
loadingMessage: t("notifyZoneUpdatedLoading"),
|
||||
loadingMessage: "Updating DNS Zone...",
|
||||
});
|
||||
|
||||
return promise;
|
||||
@@ -84,10 +79,11 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
if (!zone?.id) return Promise.reject("Can not delete DNS Zone without ID");
|
||||
|
||||
const choice = await confirm({
|
||||
title: t("confirmDeleteZoneTitle", { name: zone.domain }),
|
||||
description: t("confirmDeleteZoneDesc"),
|
||||
confirmText: tCommon("delete"),
|
||||
cancelText: tCommon("cancel"),
|
||||
title: `Delete zone '${zone.domain}'?`,
|
||||
description:
|
||||
"Are you sure you want to delete this zone? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
maxWidthClass: "max-w-md",
|
||||
});
|
||||
@@ -99,10 +95,10 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
});
|
||||
|
||||
notify({
|
||||
title: t("notifyZoneDeletedTitle", { name: zone.domain }),
|
||||
description: t("notifyZoneDeletedDesc"),
|
||||
title: `DNS Zone '${zone.domain}'`,
|
||||
description: `DNS Zone was deleted successfully.`,
|
||||
promise: promise,
|
||||
loadingMessage: t("notifyZoneDeletedLoading"),
|
||||
loadingMessage: "Deleting DNS Zone...",
|
||||
});
|
||||
|
||||
return promise;
|
||||
@@ -122,13 +118,10 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
});
|
||||
|
||||
notify({
|
||||
title: t("notifyRecordAddedTitle", {
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
}),
|
||||
description: t("notifyRecordAddedDesc"),
|
||||
title: `${record.type} Record '${record.name}'`,
|
||||
description: `DNS Record was added successfully.`,
|
||||
promise: promise,
|
||||
loadingMessage: t("notifyRecordAddedLoading"),
|
||||
loadingMessage: "Adding DNS Record...",
|
||||
});
|
||||
|
||||
return promise;
|
||||
@@ -150,13 +143,10 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
});
|
||||
|
||||
notify({
|
||||
title: t("notifyRecordUpdatedTitle", {
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
}),
|
||||
description: t("notifyRecordUpdatedDesc"),
|
||||
title: `${record.type} Record '${record.name}'`,
|
||||
description: `DNS Record was updated successfully.`,
|
||||
promise: promise,
|
||||
loadingMessage: t("notifyRecordUpdatedLoading"),
|
||||
loadingMessage: "Updating DNS Record...",
|
||||
});
|
||||
|
||||
return promise;
|
||||
@@ -172,10 +162,11 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
return Promise.reject("Can not delete DNS Record without ID");
|
||||
|
||||
const choice = await confirm({
|
||||
title: t("confirmDeleteRecordTitle", { name: record.name }),
|
||||
description: t("confirmDeleteRecordDesc"),
|
||||
confirmText: tCommon("delete"),
|
||||
cancelText: tCommon("cancel"),
|
||||
title: `Delete record '${record.name}'?`,
|
||||
description:
|
||||
"Are you sure you want to delete this record? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
maxWidthClass: "max-w-md",
|
||||
});
|
||||
@@ -189,13 +180,10 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
});
|
||||
|
||||
notify({
|
||||
title: t("notifyRecordDeletedTitle", {
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
}),
|
||||
description: t("notifyRecordDeletedDesc"),
|
||||
title: `${record.type} Record '${record.name}'`,
|
||||
description: `DNS Record was deleted successfully.`,
|
||||
promise: promise,
|
||||
loadingMessage: t("notifyRecordDeletedLoading"),
|
||||
loadingMessage: "Deleting DNS Record...",
|
||||
});
|
||||
|
||||
return promise;
|
||||
@@ -215,10 +203,11 @@ export const DNSZonesProvider = ({ children }: Props) => {
|
||||
|
||||
const askForRecord = async (zone: DNSZone) => {
|
||||
const choice = await confirm({
|
||||
title: t("askForRecordTitle", { name: zone.name }),
|
||||
description: t("askForRecordDesc"),
|
||||
confirmText: t("addDNSRecord"),
|
||||
cancelText: t("askForRecordCancel"),
|
||||
title: `Add new record to '${zone.name}'?`,
|
||||
description:
|
||||
"Add either an A, AAAA or a CNAME record to control domain name resolution for your network.",
|
||||
confirmText: "Add Record",
|
||||
cancelText: "Later",
|
||||
type: "default",
|
||||
maxWidthClass: "max-w-md",
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import { PenSquare, Trash2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
@@ -7,7 +5,6 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { DNSRecord } from "@/interfaces/DNS";
|
||||
import { useDNSZones } from "@/modules/dns/zones/DNSZonesProvider";
|
||||
import { useDNSZone } from "@/modules/dns/zones/records/DNSRecordsTable";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
record: DNSRecord;
|
||||
@@ -17,7 +14,6 @@ export const DNSRecordActionCell = ({ record }: Props) => {
|
||||
const { permission } = usePermissions();
|
||||
const { deleteRecord, openRecordModal } = useDNSZones();
|
||||
const zone = useDNSZone();
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
return (
|
||||
<div className={"flex justify-end pr-4"}>
|
||||
@@ -29,7 +25,7 @@ export const DNSRecordActionCell = ({ record }: Props) => {
|
||||
data-testid="edit-dns-record"
|
||||
>
|
||||
<PenSquare size={16} />
|
||||
{tCommon("edit")}
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
@@ -39,7 +35,7 @@ export const DNSRecordActionCell = ({ record }: Props) => {
|
||||
data-testid="delete-dns-record"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
{tCommon("delete")}
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { ClockIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { DNSRecord } from "@/interfaces/DNS";
|
||||
import { getTTLLabel } from "@/modules/dns/zones/DNSRecordModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
record: DNSRecord;
|
||||
};
|
||||
|
||||
export const DNSRecordTimeToLiveCell = ({ record }: Props) => {
|
||||
const t = useTranslations("dns");
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
@@ -19,7 +15,7 @@ export const DNSRecordTimeToLiveCell = ({ record }: Props) => {
|
||||
}
|
||||
>
|
||||
<ClockIcon size={14} />
|
||||
{getTTLLabel(record.ttl, t)}
|
||||
{getTTLLabel(record.ttl)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { DataTable } from "@components/table/DataTable";
|
||||
import DataTableHeader from "@components/table/DataTableHeader";
|
||||
import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
@@ -10,38 +8,37 @@ import { DNSRecordContentCell } from "@/modules/dns/zones/records/DNSRecordConte
|
||||
import { DNSRecordNameCell } from "@/modules/dns/zones/records/DNSRecordNameCell";
|
||||
import { DNSRecordTimeToLiveCell } from "@/modules/dns/zones/records/DNSRecordTimeToLiveCell";
|
||||
import { DNSRecordTypeCell } from "@/modules/dns/zones/records/DNSRecordTypeCell";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
zone: DNSZone;
|
||||
};
|
||||
|
||||
const getColumns = (t: (key: string, values?: any) => string, tCommon: (key: string, values?: any) => string): ColumnDef<DNSRecord>[] => [
|
||||
export const DNSRecordsTableColumns: ColumnDef<DNSRecord>[] = [
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{tCommon("type")}</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>Type</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <DNSRecordTypeCell record={row.original} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("hostname")}</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>Hostname</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <DNSRecordNameCell record={row.original} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "content",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("contentColumn")}</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>Content</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <DNSRecordContentCell record={row.original} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "ttl",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("ttl")}</DataTableHeader>;
|
||||
return <DataTableHeader column={column}>TTL</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <DNSRecordTimeToLiveCell record={row.original} />,
|
||||
},
|
||||
@@ -56,8 +53,6 @@ const ZoneContext = createContext({} as DNSZone);
|
||||
|
||||
export default function DNSRecordsTable({ zone }: Props) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
return (
|
||||
<ZoneContext.Provider value={zone}>
|
||||
@@ -70,13 +65,13 @@ export default function DNSRecordsTable({ zone }: Props) {
|
||||
rowClassName={"last:pb-10"}
|
||||
className={"bg-nb-gray-960 py-2"}
|
||||
inset={true}
|
||||
text={t("dnsRecords")}
|
||||
text={"DNS Records"}
|
||||
initialPageSize={zone?.records?.length}
|
||||
manualPagination={true}
|
||||
sorting={sorting}
|
||||
columnVisibility={{}}
|
||||
setSorting={setSorting}
|
||||
columns={getColumns(t, tCommon)}
|
||||
columns={DNSRecordsTableColumns}
|
||||
data={zone.records}
|
||||
/>
|
||||
</ZoneContext.Provider>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -14,7 +12,6 @@ import { useState } from "react";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { DNSZone } from "@/interfaces/DNS";
|
||||
import { useDNSZones } from "@/modules/dns/zones/DNSZonesProvider";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
zone: DNSZone;
|
||||
@@ -24,8 +21,6 @@ export const DNSZonesActionCell = ({ zone }: Props) => {
|
||||
const { permission } = usePermissions();
|
||||
const { openZoneModal, deleteZone, updateZone } = useDNSZones();
|
||||
const [open, setOpen] = useState(false);
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
return (
|
||||
<div className={"flex justify-end pr-4"}>
|
||||
@@ -40,7 +35,7 @@ export const DNSZonesActionCell = ({ zone }: Props) => {
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className={"!px-3"}
|
||||
aria-label={t("zoneActionsAria")}
|
||||
aria-label={"Zone actions"}
|
||||
data-testid="dns-zone-actions"
|
||||
>
|
||||
<MoreVertical size={16} className={"shrink-0"} />
|
||||
@@ -53,7 +48,7 @@ export const DNSZonesActionCell = ({ zone }: Props) => {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<SquarePenIcon size={14} className={"shrink-0"} />
|
||||
{tCommon("edit")}
|
||||
Edit
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -67,7 +62,7 @@ export const DNSZonesActionCell = ({ zone }: Props) => {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<PowerIcon size={14} className={"shrink-0"} />
|
||||
{zone.enabled ? t("disable") : t("enable")}
|
||||
{zone.enabled ? "Disable" : "Enable"}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -81,7 +76,7 @@ export const DNSZonesActionCell = ({ zone }: Props) => {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
{tCommon("delete")}
|
||||
Delete
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import Button from "@components/Button";
|
||||
import { GlobeIcon, PlusCircle } from "lucide-react";
|
||||
@@ -7,7 +5,6 @@ import * as React from "react";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { DNSZone } from "@/interfaces/DNS";
|
||||
import { useDNSZones } from "@/modules/dns/zones/DNSZonesProvider";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
zone: DNSZone;
|
||||
@@ -16,7 +13,6 @@ type Props = {
|
||||
export const DNSZonesRecordsCell = ({ zone }: Props) => {
|
||||
const { permission } = usePermissions();
|
||||
const { openRecordModal } = useDNSZones();
|
||||
const t = useTranslations("dns");
|
||||
|
||||
const recordsCount = zone?.records?.length ?? 0;
|
||||
|
||||
@@ -46,7 +42,7 @@ export const DNSZonesRecordsCell = ({ zone }: Props) => {
|
||||
data-testid="add-dns-record"
|
||||
>
|
||||
<PlusCircle size={12} />
|
||||
{t("addRecordBtn")}
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
@@ -42,13 +40,12 @@ import { DNSZonesSearchDomainCell } from "@/modules/dns/zones/table/DNSZonesSear
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import DNSZoneIcon from "@/assets/icons/DNSZoneIcon";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const getColumns = (t: (key: string, values?: any) => string, tCommon: (key: string, values?: any) => string): ColumnDef<DNSZone>[] => [
|
||||
export const DNSZonesColumns: ColumnDef<DNSZone>[] = [
|
||||
{
|
||||
accessorKey: "domain",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>{t("zoneColumn")}</DataTableHeader>
|
||||
<DataTableHeader column={column}>Zone</DataTableHeader>
|
||||
),
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => <DNSZonesNameCell zone={row.original} />,
|
||||
@@ -59,7 +56,7 @@ const getColumns = (t: (key: string, values?: any) => string, tCommon: (key: str
|
||||
{
|
||||
accessorKey: "records",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>{t("recordsColumn")}</DataTableHeader>
|
||||
<DataTableHeader column={column}>Records</DataTableHeader>
|
||||
),
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => <DNSZonesRecordsCell zone={row.original} />,
|
||||
@@ -67,7 +64,7 @@ const getColumns = (t: (key: string, values?: any) => string, tCommon: (key: str
|
||||
{
|
||||
accessorKey: "distribution_groups",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>{tCommon("distributionGroups")}</DataTableHeader>
|
||||
<DataTableHeader column={column}>Groups</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => <DNSZonesGroupCell zone={row.original} />,
|
||||
},
|
||||
@@ -80,7 +77,7 @@ const getColumns = (t: (key: string, values?: any) => string, tCommon: (key: str
|
||||
{
|
||||
accessorKey: "enable_search_domain",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>{t("searchDomainColumn")}</DataTableHeader>
|
||||
<DataTableHeader column={column}>Search Domain</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => <DNSZonesSearchDomainCell zone={row.original} />,
|
||||
},
|
||||
@@ -122,8 +119,6 @@ export default function DNSZonesTable({
|
||||
const { mutate } = useSWRConfig();
|
||||
const path = usePathname();
|
||||
const { groups } = useGroups();
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
// Default sorting state of the table
|
||||
const [sorting, setSorting] = useLocalStorage<SortingState>(
|
||||
@@ -168,18 +163,18 @@ export default function DNSZonesTable({
|
||||
|
||||
const statusOptions = useMemo<RadioOption<boolean | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: tCommon("all"), dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: tCommon("active"), dotClass: "bg-green-500" },
|
||||
{ value: false, label: tCommon("inactive"), dotClass: "bg-nb-gray-700" },
|
||||
{ value: undefined, label: "All", dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: "Active", dotClass: "bg-green-500" },
|
||||
{ value: false, label: "Inactive", dotClass: "bg-nb-gray-700" },
|
||||
],
|
||||
[tCommon],
|
||||
[],
|
||||
);
|
||||
|
||||
const filterDefs = useMemo<TableFilterDef[]>(
|
||||
() => [
|
||||
{
|
||||
id: "enabled",
|
||||
label: tCommon("status"),
|
||||
label: "Status",
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as boolean | undefined}
|
||||
@@ -193,7 +188,7 @@ export default function DNSZonesTable({
|
||||
},
|
||||
{
|
||||
id: "group_names_filter",
|
||||
label: tCommon("distributionGroups"),
|
||||
label: "Groups",
|
||||
renderPicker: (p) => (
|
||||
<GroupsPicker
|
||||
value={p.value as string[] | undefined}
|
||||
@@ -205,17 +200,17 @@ export default function DNSZonesTable({
|
||||
formatChip: (v) => formatGroupsChip(v as string[] | undefined),
|
||||
},
|
||||
],
|
||||
[statusOptions, tableGroups, tCommon],
|
||||
[statusOptions, tableGroups],
|
||||
);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
headingTarget={headingTarget}
|
||||
isLoading={isLoading}
|
||||
text={t("zones")}
|
||||
text={"DNS Zones"}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
columns={getColumns(t, tCommon)}
|
||||
columns={DNSZonesColumns}
|
||||
data={zonesWithGroups}
|
||||
useRowId={true}
|
||||
wrapperComponent={isGroupPage ? Card : undefined}
|
||||
@@ -227,7 +222,7 @@ export default function DNSZonesTable({
|
||||
keepStateInLocalStorage={!isGroupPage}
|
||||
initialPageSize={25}
|
||||
showResetFilterButton={false}
|
||||
searchPlaceholder={t("searchZonePlaceholder")}
|
||||
searchPlaceholder={"Search by domain, ip, content or group..."}
|
||||
aboveTable={(table) => (
|
||||
<TableFilterChips table={table} filters={filterDefs} />
|
||||
)}
|
||||
@@ -252,8 +247,10 @@ export default function DNSZonesTable({
|
||||
icon={<DNSZoneIcon className={"fill-nb-gray-200"} size={24} />}
|
||||
className={"py-4"}
|
||||
contentClassName={"max-w-lg"}
|
||||
title={t("noZonesGroupTitle")}
|
||||
description={t("noZonesGroupDesc")}
|
||||
title={"This group is not used within any zones yet"}
|
||||
description={
|
||||
"Assign this group as a distribution group in your zones to see them listed here."
|
||||
}
|
||||
>
|
||||
<div className={"gap-x-4 flex items-center justify-center mt-4"}>
|
||||
<AddZoneButton distributionGroups={distributionGroups} />
|
||||
@@ -268,8 +265,10 @@ export default function DNSZonesTable({
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={t("createZone")}
|
||||
description={t("noZonesGetStartedDesc")}
|
||||
title={"Create New Zone"}
|
||||
description={
|
||||
"It looks like you don't have any zones. Control domain name resolution for your network by adding a zone."
|
||||
}
|
||||
button={
|
||||
<div className={"gap-x-4 flex items-center justify-center"}>
|
||||
<AddZoneButton distributionGroups={distributionGroups} />
|
||||
@@ -277,9 +276,9 @@ export default function DNSZonesTable({
|
||||
}
|
||||
learnMore={
|
||||
<>
|
||||
{t("learnMoreAbout")}
|
||||
Learn more about
|
||||
<InlineLink href={DNS_ZONE_DOCS_LINK} target={"_blank"}>
|
||||
{t("dnsZones")}
|
||||
DNS Zones
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
@@ -332,7 +331,6 @@ type AddZoneButtonProps = {
|
||||
const AddZoneButton = ({ distributionGroups }: AddZoneButtonProps) => {
|
||||
const { permission } = usePermissions();
|
||||
const { openZoneModal } = useDNSZones();
|
||||
const t = useTranslations("dns");
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -343,7 +341,7 @@ const AddZoneButton = ({ distributionGroups }: AddZoneButtonProps) => {
|
||||
data-testid="add-dns-zone"
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
{t("addZone")}
|
||||
Add Zone
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -73,8 +73,8 @@ const Content = ({ network, onCreated, onUpdated }: ContentProps) => {
|
||||
const updateNetwork = async () => {
|
||||
notify({
|
||||
title: name,
|
||||
description: t("networkUpdated"),
|
||||
loadingMessage: t("networkUpdating"),
|
||||
description: "Network updated successfully.",
|
||||
loadingMessage: "Updating network...",
|
||||
promise: update({ name, description }, `/${network?.id}`).then((n) => {
|
||||
onUpdated?.(n);
|
||||
}),
|
||||
@@ -84,8 +84,8 @@ const Content = ({ network, onCreated, onUpdated }: ContentProps) => {
|
||||
const createNetwork = async () => {
|
||||
notify({
|
||||
title: name,
|
||||
description: t("networkCreated"),
|
||||
loadingMessage: t("networkCreating"),
|
||||
description: "Network created successfully.",
|
||||
loadingMessage: "Creating network...",
|
||||
promise: create({ name, description }).then((n) => {
|
||||
onCreated?.(n);
|
||||
}),
|
||||
@@ -100,7 +100,7 @@ const Content = ({ network, onCreated, onUpdated }: ContentProps) => {
|
||||
description={
|
||||
network
|
||||
? network.name
|
||||
: t("modalAccessDescription")
|
||||
: "Access internal resources in LANs and VPC by adding a network."
|
||||
}
|
||||
color={"netbird"}
|
||||
/>
|
||||
@@ -133,12 +133,12 @@ data-testid="network-description-input"
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
{t("learnMoreAbout")}
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/networks"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("title")}
|
||||
Networks
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Modal } from "@components/modal/Modal";
|
||||
import { notify } from "@components/Notification";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
@@ -76,8 +73,6 @@ export const NetworkProvider = ({
|
||||
onResourceDelete,
|
||||
onResourceUpdate,
|
||||
}: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const { mutate } = useSWRConfig();
|
||||
const { confirm } = useDialog();
|
||||
const deleteCall = useApiCall("/networks").del;
|
||||
@@ -200,29 +195,25 @@ export const NetworkProvider = ({
|
||||
if (!isMulti && action === "edit") return true;
|
||||
return confirm({
|
||||
title: isMulti ? (
|
||||
<>{t("multiResourceTitle")}</>
|
||||
<>This policy is used by multiple resources</>
|
||||
) : (
|
||||
<>
|
||||
{action === "edit"
|
||||
? t("editPolicyTitle", { name: policy.name })
|
||||
: t("deletePolicyTitle", { name: policy.name })}
|
||||
{action === "edit" ? "Edit" : "Delete"} policy '{policy.name}
|
||||
'?
|
||||
</>
|
||||
),
|
||||
description: isMulti
|
||||
? t("multiResourceDesc", {
|
||||
action: action === "edit" ? tCommon("edit") : tCommon("delete"),
|
||||
})
|
||||
? `This policy uses one or many resource group(s) as destinations. ${
|
||||
action === "edit" ? "Updating" : "Deleting"
|
||||
} this policy will also affect following resources:`
|
||||
: action === "delete"
|
||||
? t("deletePolicyDesc")
|
||||
? "Are you sure you want to delete this policy? This action cannot be undone."
|
||||
: undefined,
|
||||
children: isMulti ? (
|
||||
<AffectedResourceList resources={affectedResources} />
|
||||
) : undefined,
|
||||
confirmText:
|
||||
action === "edit"
|
||||
? t("editPolicy")
|
||||
: t("deletePolicy"),
|
||||
cancelText: tCommon("cancel"),
|
||||
confirmText: action === "edit" ? "Edit Policy" : "Delete Policy",
|
||||
cancelText: "Cancel",
|
||||
hideIcon: isMulti,
|
||||
type: action === "edit" ? "warning" : "danger",
|
||||
maxWidthClass: isMulti ? "max-w-lg" : undefined,
|
||||
@@ -231,10 +222,11 @@ export const NetworkProvider = ({
|
||||
|
||||
const deleteNetwork = async (network: Network) => {
|
||||
const choice = await confirm({
|
||||
title: t("confirmDeleteNetworkTitle", { name: network.name }),
|
||||
description: t("confirmDeleteNetworkDesc"),
|
||||
confirmText: tCommon("delete"),
|
||||
cancelText: tCommon("cancel"),
|
||||
title: `Delete network '${network.name}'?`,
|
||||
description:
|
||||
"Are you sure you want to delete this network? Every resource and routing peers will be removed from this network. This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
@@ -247,8 +239,8 @@ export const NetworkProvider = ({
|
||||
|
||||
notify({
|
||||
title: network.name,
|
||||
description: t("networkDeleted"),
|
||||
loadingMessage: t("networkDeleting"),
|
||||
description: "Network deleted successfully.",
|
||||
loadingMessage: "Deleting network...",
|
||||
promise,
|
||||
});
|
||||
|
||||
@@ -260,10 +252,11 @@ export const NetworkProvider = ({
|
||||
resource: NetworkResource,
|
||||
) => {
|
||||
const choice = await confirm({
|
||||
title: t("confirmDeleteResourceTitle", { name: resource.name }),
|
||||
description: t("confirmDeleteResourceDesc"),
|
||||
confirmText: tCommon("delete"),
|
||||
cancelText: tCommon("cancel"),
|
||||
title: `Delete resource '${resource.name}'?`,
|
||||
description:
|
||||
"Are you sure you want to delete this resource? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
@@ -271,8 +264,8 @@ export const NetworkProvider = ({
|
||||
|
||||
notify({
|
||||
title: resource.name,
|
||||
description: t("resourceDeleted"),
|
||||
loadingMessage: t("resourceDeleting"),
|
||||
description: "Resource deleted successfully.",
|
||||
loadingMessage: "Deleting resource...",
|
||||
promise: deleteCall({}, `/${network.id}/resources/${resource.id}`).then(
|
||||
() => {
|
||||
onResourceDelete?.();
|
||||
@@ -286,19 +279,19 @@ export const NetworkProvider = ({
|
||||
|
||||
const deleteRouter = async (network: Network, router: NetworkRouter) => {
|
||||
const choice = await confirm({
|
||||
title: t("confirmRemoveRouterTitle"),
|
||||
description: t("confirmRemoveRouterDesc"),
|
||||
confirmText: t("remove"),
|
||||
cancelText: tCommon("cancel"),
|
||||
title: `Remove this router?`,
|
||||
description: "Are you sure you want to remove this router?",
|
||||
confirmText: "Remove",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
if (!choice) return;
|
||||
|
||||
notify({
|
||||
title: t("removeRouter", { name: network.name }),
|
||||
description: t("routerRemoved"),
|
||||
loadingMessage: t("routerRemoving"),
|
||||
title: "Router of " + network.name,
|
||||
description: "Router deleted successfully.",
|
||||
loadingMessage: "Deleting router...",
|
||||
promise: deleteCall({}, `/${network.id}/routers/${router.id}`).then(
|
||||
() => {
|
||||
mutate(`/networks/${network.id}/routers`);
|
||||
@@ -309,10 +302,11 @@ export const NetworkProvider = ({
|
||||
|
||||
const askForRoutingPeer = async (network: Network) => {
|
||||
const choice = await confirm({
|
||||
title: t("confirmAddRoutingPeerTitle", { name: network.name }),
|
||||
description: t("confirmAddRoutingPeerDesc"),
|
||||
confirmText: t("confirmAddRoutingPeer"),
|
||||
cancelText: t("later"),
|
||||
title: `Add Routing Peer to '${network.name}'?`,
|
||||
description:
|
||||
"Without a routing peer, the resources inside this network will not be accessible by any peers.",
|
||||
confirmText: "Add Routing Peer",
|
||||
cancelText: "Later",
|
||||
type: "default",
|
||||
});
|
||||
if (!choice) return;
|
||||
@@ -321,10 +315,11 @@ export const NetworkProvider = ({
|
||||
|
||||
const askForResource = async (network: Network) => {
|
||||
const choice = await confirm({
|
||||
title: t("confirmAddResourceTitle", { name: network.name }),
|
||||
description: t("confirmAddResourceDesc"),
|
||||
confirmText: t("addResource"),
|
||||
cancelText: t("later"),
|
||||
title: `Add Resource to '${network.name}'?`,
|
||||
description:
|
||||
"Peers will be able to access your network resources once you add them.",
|
||||
confirmText: "Add Resource",
|
||||
cancelText: "Later",
|
||||
type: "default",
|
||||
});
|
||||
if (!choice) return;
|
||||
@@ -507,7 +502,6 @@ export const useNetworksContext = () => {
|
||||
};
|
||||
|
||||
function AffectedResourceList({ resources }: { resources: NetworkResource[] }) {
|
||||
const t = useTranslations("networks");
|
||||
const maxVisible = 6;
|
||||
const visible = resources.slice(0, maxVisible);
|
||||
const remaining = resources.length - maxVisible;
|
||||
@@ -534,7 +528,7 @@ function AffectedResourceList({ resources }: { resources: NetworkResource[] }) {
|
||||
))}
|
||||
{remaining > 0 && (
|
||||
<div className="border-t border-nb-gray-900 px-3 py-2 text-nb-gray-200">
|
||||
{t("remainingMore", { count: remaining })}
|
||||
+ {remaining} more
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { DomainListBadge } from "@components/ui/DomainListBadge";
|
||||
import { IconDirectionSign } from "@tabler/icons-react";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { ExitNodeHelpTooltip } from "@/modules/exit-node/ExitNodeHelpTooltip";
|
||||
|
||||
@@ -12,7 +9,6 @@ type Props = {
|
||||
domains?: string[];
|
||||
};
|
||||
export default function NetworkRangeCell({ network, domains }: Props) {
|
||||
const t = useTranslations("networks");
|
||||
const isExitNode = network === "0.0.0.0/0";
|
||||
const hasDomains = domains ? domains.length > 0 : false;
|
||||
|
||||
@@ -22,7 +18,7 @@ export default function NetworkRangeCell({ network, domains }: Props) {
|
||||
<ExitNodeHelpTooltip>
|
||||
<div className={"flex gap-2 items-center dark:text-nb-gray-300 group"}>
|
||||
<IconDirectionSign size={16} className={"text-yellow-400"} />
|
||||
{t("exitNode")}{" "}
|
||||
Exit Node{" "}
|
||||
<InfoIcon
|
||||
size={14}
|
||||
className={
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import Button from "@components/Button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PlusCircle, ShieldIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -11,26 +8,23 @@ type Props = {
|
||||
};
|
||||
|
||||
export const PolicyCell = ({ count }: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
return count > 0 ? (
|
||||
<div className={"flex gap-3"}>
|
||||
<Badge variant={"gray"} useHover={true}>
|
||||
<ShieldIcon size={14} className={"text-green-500"} />
|
||||
<div>
|
||||
<span className={"font-medium"}>{count}</span> {t("accessPolicies")}
|
||||
<span className={"font-medium"}>{count}</span> Access Policie(s)
|
||||
</div>
|
||||
</Badge>
|
||||
<Button size={"xs"} variant={"secondary"} className={"min-w-[130px]"}>
|
||||
<PlusCircle size={12} />
|
||||
{tCommon("create")}
|
||||
Add Policy
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button size={"xs"} variant={"secondary"} className={"min-w-[130px]"}>
|
||||
<PlusCircle size={12} />
|
||||
{tCommon("create")}
|
||||
Add Policy
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -9,12 +6,11 @@ type Props = {
|
||||
size?: number;
|
||||
};
|
||||
export const NetworkRoutesDeprecationInfo = ({ size = 14 }: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
return (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-[230px]"}>
|
||||
{t("routesDeprecationInfo")}
|
||||
Network Routes will be deprecated and replaced with Networks.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import { Label } from "@components/Label";
|
||||
@@ -51,7 +49,6 @@ export default function NetworkResourceAccessControl({
|
||||
hasResourceGroups = false,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const { network, confirmMultiResourceAction } = useNetworksContext();
|
||||
const { openEditPolicyModal, deletePolicy } = usePolicies();
|
||||
const [policyModalOpen, setPolicyModalOpen] = useState(false);
|
||||
@@ -143,13 +140,13 @@ export default function NetworkResourceAccessControl({
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-2 px-4 text-left text-[11px] uppercase tracking-wider text-nb-gray-400 font-medium">
|
||||
{t("resourceNameLabel")}
|
||||
Name
|
||||
</th>
|
||||
<th className="py-2 pl-5 pr-2 text-left text-[11px] uppercase tracking-wider text-nb-gray-400 font-medium">
|
||||
{t("resourceGroupsLabel")}
|
||||
Source Groups
|
||||
</th>
|
||||
<th className="py-2 px-4 text-left text-[11px] uppercase tracking-wider text-nb-gray-400 font-medium">
|
||||
{tCommon("configurePolicies")}
|
||||
Protocol & Ports
|
||||
</th>
|
||||
<th className="py-2 pr-4 pl-2" />
|
||||
</tr>
|
||||
@@ -236,7 +233,7 @@ export default function NetworkResourceAccessControl({
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Edit2 size={14} className="shrink-0" />
|
||||
{t("editPolicy")}
|
||||
Edit Policy
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -245,7 +242,7 @@ export default function NetworkResourceAccessControl({
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Trash2 size={14} className="shrink-0" />
|
||||
{t("deletePolicy")}
|
||||
Delete Policy
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -260,16 +257,16 @@ export default function NetworkResourceAccessControl({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="dotted"
|
||||
className={"w-full mt-1"}
|
||||
size="sm"
|
||||
onClick={openAddPolicy}
|
||||
data-testid="add-policy"
|
||||
>
|
||||
<PlusIcon size={14} />
|
||||
{tCommon("addPolicy")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="dotted"
|
||||
className={"w-full mt-1"}
|
||||
size="sm"
|
||||
onClick={openAddPolicy}
|
||||
data-testid="add-policy"
|
||||
>
|
||||
<PlusIcon size={14} />
|
||||
{tCommon("addPolicy")}
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
open={policyModalOpen}
|
||||
@@ -305,6 +302,5 @@ export default function NetworkResourceAccessControl({
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,16 +153,16 @@ export function ResourceModalContent({
|
||||
const nameError = useMemo(() => {
|
||||
if (name === "") return "";
|
||||
if (resourceExists(name, resource?.id))
|
||||
return t("nameAlreadyExists");
|
||||
return "A resource with this name already exists. Please use another name.";
|
||||
return "";
|
||||
}, [name, resourceExists, resource?.id, t]);
|
||||
}, [name, resourceExists, resource?.id]);
|
||||
|
||||
const confirmMissingPolicies = async () => {
|
||||
if (allResourcePolicies.length > 0) return true;
|
||||
return confirm({
|
||||
title: t("noPoliciesConfirmTitle"),
|
||||
title: "No Access Control Policies Configured",
|
||||
description:
|
||||
t("noPoliciesConfirmDesc"),
|
||||
"Without access control policies, this resource will not be accessible by any peers. You can also create policies later. Are you sure you want to continue?",
|
||||
type: "warning",
|
||||
confirmText: resource ? t("saveChanges") : t("addResource"),
|
||||
cancelText: tCommon("cancel"),
|
||||
@@ -185,9 +185,9 @@ export function ResourceModalContent({
|
||||
});
|
||||
|
||||
notify({
|
||||
title: t("resourceCreated"),
|
||||
description: t("resourceCreatedDesc", { name }),
|
||||
loadingMessage: t("resourceCreating"),
|
||||
title: "Resource Created",
|
||||
description: `The resource "${name}" has been created successfully.`,
|
||||
loadingMessage: "Creating resource...",
|
||||
promise,
|
||||
});
|
||||
|
||||
@@ -208,9 +208,9 @@ export function ResourceModalContent({
|
||||
onUpdated?.(r);
|
||||
});
|
||||
notify({
|
||||
title: t("resourceUpdated"),
|
||||
description: t("resourceUpdatedDesc", { name }),
|
||||
loadingMessage: t("resourceUpdating"),
|
||||
title: "Resource Updated",
|
||||
description: `Resource "${name}" has been updated successfully.`,
|
||||
loadingMessage: "Updating resource...",
|
||||
promise,
|
||||
});
|
||||
};
|
||||
@@ -231,7 +231,7 @@ export function ResourceModalContent({
|
||||
description={
|
||||
resource
|
||||
? `${resource.name}`
|
||||
: t("resourceAddNewDesc", { networkName: network?.name })
|
||||
: `Add new resource to "${network?.name}"`
|
||||
}
|
||||
color={"yellow"}
|
||||
/>
|
||||
@@ -394,17 +394,14 @@ data-testid="resource-name-input"
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
{t.rich("resourceGroupsLearnMore", {
|
||||
link: (chunks) => (
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/networks#resources"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
),
|
||||
})}
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/networks#resources"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Resources
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -9,7 +7,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/DropdownMenu";
|
||||
import { notify } from "@components/Notification";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import {
|
||||
MoreVertical,
|
||||
@@ -29,7 +26,6 @@ type Props = {
|
||||
resource: NetworkResource;
|
||||
};
|
||||
export const ResourceActionCell = ({ resource }: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const { permission } = usePermissions();
|
||||
const { deleteResource, network, openResourceModal } = useNetworksContext();
|
||||
const { mutate } = useSWRConfig();
|
||||
@@ -42,11 +38,11 @@ export const ResourceActionCell = ({ resource }: Props) => {
|
||||
const toggleEnabled = async () => {
|
||||
const nextEnabled = !resource.enabled;
|
||||
notify({
|
||||
title: t("updateResource"),
|
||||
description: nextEnabled
|
||||
? t("resourceNowEnabled", { name: resource?.name })
|
||||
: t("resourceNowDisabled", { name: resource?.name }),
|
||||
loadingMessage: t("updatingResource"),
|
||||
title: `Update Resource`,
|
||||
description: `'${resource?.name}' is now ${
|
||||
nextEnabled ? "enabled" : "disabled"
|
||||
}`,
|
||||
loadingMessage: "Updating resource...",
|
||||
duration: 1200,
|
||||
promise: update({
|
||||
...resource,
|
||||
@@ -77,7 +73,7 @@ export const ResourceActionCell = ({ resource }: Props) => {
|
||||
disabled={
|
||||
!permission.networks.update && !permission.networks.delete
|
||||
}
|
||||
aria-label={t("resourceEdit")}
|
||||
aria-label={"Resource actions"}
|
||||
>
|
||||
<MoreVertical size={16} className={"shrink-0"} />
|
||||
</Button>
|
||||
@@ -92,7 +88,7 @@ export const ResourceActionCell = ({ resource }: Props) => {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<SquarePenIcon size={14} className={"shrink-0"} />
|
||||
{t("resourceEdit")}
|
||||
Edit
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -104,7 +100,7 @@ export const ResourceActionCell = ({ resource }: Props) => {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<PowerIcon size={14} className={"shrink-0"} />
|
||||
{resource.enabled ? t("resourceDisable") : t("resourceEnable")}
|
||||
{resource.enabled ? "Disable" : "Enable"}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -118,7 +114,7 @@ export const ResourceActionCell = ({ resource }: Props) => {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
{t("resourceDelete")}
|
||||
Delete
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import CopyToClipboardText from "@components/CopyToClipboardText";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React from "react";
|
||||
import { NetworkResource } from "@/interfaces/Network";
|
||||
|
||||
@@ -9,10 +6,9 @@ type Props = {
|
||||
resource: NetworkResource;
|
||||
};
|
||||
export default function ResourceAddressCell({ resource }: Readonly<Props>) {
|
||||
const t = useTranslations("networks");
|
||||
return (
|
||||
<CopyToClipboardText
|
||||
message={t("addressCopied", { address: resource.address })}
|
||||
message={`${resource.address} has been copied to your clipboard`}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { notify } from "@components/Notification";
|
||||
import { ToggleSwitch } from "@components/ToggleSwitch";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
@@ -20,7 +17,6 @@ export const ResourceEnabledCell = ({
|
||||
resource,
|
||||
mutateAllResourcesOnUpdate,
|
||||
}: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
@@ -32,11 +28,11 @@ export const ResourceEnabledCell = ({
|
||||
|
||||
const toggle = async (enabled: boolean) => {
|
||||
notify({
|
||||
title: t("updateResource"),
|
||||
description: enabled
|
||||
? t("resourceNowEnabled", { name: resource?.name })
|
||||
: t("resourceNowDisabled", { name: resource?.name }),
|
||||
loadingMessage: t("updatingResource"),
|
||||
title: `Update Resource`,
|
||||
description: `'${resource?.name}' is now ${
|
||||
enabled ? "enabled" : "disabled"
|
||||
}`,
|
||||
loadingMessage: "Updating resource...",
|
||||
duration: 1200,
|
||||
promise: update({
|
||||
...resource,
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import Badge from "@components/Badge";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
@@ -13,6 +9,7 @@ import {
|
||||
import { NetworkResource } from "@/interfaces/Network";
|
||||
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import Badge from "@components/Badge";
|
||||
import { CirclePlusIcon } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
@@ -20,7 +17,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const ResourceExposeServiceCell = ({ resource }: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const { permission } = usePermissions();
|
||||
const { openModal, reverseProxies } = useReverseProxies();
|
||||
const { network } = useNetworksContext();
|
||||
@@ -77,7 +73,7 @@ export const ResourceExposeServiceCell = ({ resource }: Props) => {
|
||||
disabled={!permission.services?.create}
|
||||
>
|
||||
<CirclePlusIcon size={12} />
|
||||
{t("expose")}
|
||||
Expose
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import MultipleGroups, {
|
||||
TransparentEditIconButton,
|
||||
} from "@components/ui/MultipleGroups";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { IconCirclePlus } from "@tabler/icons-react";
|
||||
import * as React from "react";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
@@ -16,7 +13,6 @@ type Props = {
|
||||
resource?: NetworkResource;
|
||||
};
|
||||
export const ResourceGroupCell = ({ resource }: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { network, openResourceGroupModal } = useNetworksContext();
|
||||
@@ -49,7 +45,7 @@ export const ResourceGroupCell = ({ resource }: Props) => {
|
||||
disabled={!permission.networks.update}
|
||||
>
|
||||
<IconCirclePlus size={14} />
|
||||
{t("addResourceBtn")}
|
||||
Add
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
Modal,
|
||||
@@ -58,8 +56,7 @@ const ResourceGroupModalContent = ({
|
||||
network,
|
||||
onUpdated,
|
||||
}: ModalProps) => {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const t = useTranslations("common");
|
||||
const update = useApiCall<NetworkResource>(
|
||||
`/networks/${network?.id}/resources/${resource?.id}`,
|
||||
).put;
|
||||
@@ -72,9 +69,9 @@ const ResourceGroupModalContent = ({
|
||||
const updateResource = async () => {
|
||||
const savedGroups = await saveGroups();
|
||||
notify({
|
||||
title: t("updateResource"),
|
||||
description: t("groupUpdated", { name: resource?.name || "" }),
|
||||
loadingMessage: t("updatingGroups"),
|
||||
title: "Update Resource",
|
||||
description: `'${resource?.name}' groups updated`,
|
||||
loadingMessage: "Updating resource groups...",
|
||||
promise: update({
|
||||
...resource,
|
||||
groups: savedGroups.map((g) => g.id),
|
||||
@@ -87,8 +84,10 @@ const ResourceGroupModalContent = ({
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-2xl"}>
|
||||
<ModalHeader
|
||||
title={t("resourceGroupsModalTitle")}
|
||||
description={t("resourceGroupsModalDesc")}
|
||||
title={"Resource Groups"}
|
||||
description={
|
||||
"Add this resource to a group (e.g., Databases, Web Servers) and reference the group in access policies to simplify management."
|
||||
}
|
||||
icon={<FolderGit2 size={18} />}
|
||||
/>
|
||||
|
||||
@@ -100,7 +99,7 @@ const ResourceGroupModalContent = ({
|
||||
onChange={setGroups}
|
||||
values={groups}
|
||||
showPeerCounter={false}
|
||||
placeholder={t("resourceGroupsPlaceholder")}
|
||||
placeholder={"Add or select resource group(s)..."}
|
||||
policies={policies}
|
||||
/>
|
||||
</div>
|
||||
@@ -113,7 +112,7 @@ const ResourceGroupModalContent = ({
|
||||
</ModalClose>
|
||||
|
||||
<Button variant={"primary"} onClick={updateResource}>
|
||||
{t("saveGroups")}
|
||||
Save Groups
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import HelpText from "@components/HelpText";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { validator } from "@utils/helpers";
|
||||
import cidr from "ip-cidr";
|
||||
import { GlobeIcon, NetworkIcon, WorkflowIcon } from "lucide-react";
|
||||
@@ -23,18 +20,13 @@ type Props = {
|
||||
export const ResourceSingleAddressInput = ({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
label = "Address",
|
||||
className = "",
|
||||
onError,
|
||||
description,
|
||||
placeholder,
|
||||
description = "Enter a single IP address, CIDR block or domain name",
|
||||
placeholder = "Address (IP, CIDR or Domain)",
|
||||
autoFocus,
|
||||
}: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const resolvedLabel = label || t("addressLabel");
|
||||
const resolvedDescription = description || t("addressDescription");
|
||||
const resolvedPlaceholder = placeholder || t("addressPlaceholder");
|
||||
|
||||
const hasChars = useMemo(() => {
|
||||
return !!value.match(/[a-z*]/i);
|
||||
}, [value]);
|
||||
@@ -59,18 +51,18 @@ export const ResourceSingleAddressInput = ({
|
||||
!value.includes(".") ||
|
||||
value.endsWith(".")
|
||||
) {
|
||||
return t("domainError");
|
||||
return "Please enter a valid domain, e.g. service.internal, example.com or *.example.com";
|
||||
}
|
||||
return ""; // Valid domain
|
||||
}
|
||||
|
||||
// Case 2: If it's not a valid domain, check if it's a valid CIDR
|
||||
if (!cidr.isValidAddress(value)) {
|
||||
return t("ipCidrError");
|
||||
return "Please enter a valid IP or CIDR, e.g., 10.0.0.21, 192.168.1.0/24, 2001:db8::1 or 2001:db8::/64";
|
||||
}
|
||||
|
||||
return ""; // Valid CIDR
|
||||
}, [value, hasChars, isCIDRBlock, t]);
|
||||
}, [value, hasChars, isCIDRBlock]);
|
||||
|
||||
useEffect(() => {
|
||||
onError?.(error);
|
||||
@@ -78,14 +70,14 @@ export const ResourceSingleAddressInput = ({
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Label>{resolvedLabel}</Label>
|
||||
<HelpText>{resolvedDescription}</HelpText>
|
||||
<Label>{label}</Label>
|
||||
<HelpText>{description}</HelpText>
|
||||
<Input
|
||||
autoFocus={autoFocus}
|
||||
data-testid="resource-address-input"
|
||||
customPrefix={PrefixIcon}
|
||||
error={error}
|
||||
placeholder={resolvedPlaceholder}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { NetworkIcon, WorkflowIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -9,16 +6,15 @@ type Props = {
|
||||
single: boolean;
|
||||
};
|
||||
export default function ResourceTypeCell({ single }: Props) {
|
||||
const t = useTranslations("networks");
|
||||
return (
|
||||
<div className={"inline-flex"}>
|
||||
{single ? (
|
||||
<Badge variant={"gray"} className={"min-w-[130px]"}>
|
||||
<WorkflowIcon size={14} /> {t("singleIP")}
|
||||
<WorkflowIcon size={14} /> Single IP
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant={"gray"} className={"min-w-[130px]"}>
|
||||
<NetworkIcon size={14} /> {t("ipRange")}
|
||||
<NetworkIcon size={14} /> IP Range
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import SkeletonTable, {
|
||||
SkeletonTableHeader,
|
||||
} from "@components/skeletons/SkeletonTable";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { Suspense } from "react";
|
||||
import { NetworkResource } from "@/interfaces/Network";
|
||||
@@ -21,23 +18,19 @@ export const ResourcesTabContent = ({
|
||||
data,
|
||||
isLoading,
|
||||
}: ResourcesSectionProps) => {
|
||||
const t = useTranslations("networks");
|
||||
return (
|
||||
<div className={"px-8"}>
|
||||
<div className={"flex justify-between items-center mb-5"}>
|
||||
<div>
|
||||
<Paragraph>
|
||||
{t.rich("resourcesTabDescription", {
|
||||
link: (chunks) => (
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/networks#resources"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
),
|
||||
})}
|
||||
Add resources to this network to control what peers can access.{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/networks#resources"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { DataTable } from "@components/table/DataTable";
|
||||
@@ -23,7 +21,6 @@ import NoResults from "@components/ui/NoResults";
|
||||
import { IconCirclePlus } from "@tabler/icons-react";
|
||||
import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import { removeAllSpaces } from "@utils/helpers";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ArrowUpRightIcon, Layers3Icon } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import * as React from "react";
|
||||
@@ -50,14 +47,97 @@ type Props = {
|
||||
isGroupPage?: boolean;
|
||||
};
|
||||
|
||||
const NetworkResourceColumns: ColumnDef<NetworkResource>[] = [
|
||||
{
|
||||
id: "id",
|
||||
accessorKey: "id",
|
||||
filterFn: "exactMatch",
|
||||
},
|
||||
{
|
||||
id: "name",
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Resource</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return <ResourceNameCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
accessorKey: "description",
|
||||
accessorFn: (resource) =>
|
||||
removeAllSpaces(resource?.description || "").toLowerCase(),
|
||||
},
|
||||
{
|
||||
id: "address",
|
||||
accessorKey: "address",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Address</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return <ResourceAddressCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
accessorKey: "enabled",
|
||||
},
|
||||
{
|
||||
id: "groups",
|
||||
accessorFn: (resource) => {
|
||||
let groups = (resource?.groups ?? []) as Group[];
|
||||
return groups.map((group) => group.name).join(", ");
|
||||
},
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Groups</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return <ResourceGroupCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "group_names",
|
||||
accessorFn: (resource) => {
|
||||
const groups = (resource?.groups ?? []) as Group[];
|
||||
return groups.map((g) => g.name).filter((n): n is string => !!n);
|
||||
},
|
||||
filterFn: "arrIncludesSome",
|
||||
},
|
||||
{
|
||||
id: "policies",
|
||||
accessorKey: "id",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Policies</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return <ResourcePolicyCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "expose_service",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
return <ResourceExposeServiceCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
return <ResourceActionCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default function ResourcesTable({
|
||||
resources,
|
||||
isLoading,
|
||||
headingTarget,
|
||||
isGroupPage,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
const params = useSearchParams();
|
||||
const resourceId = params.get("resource") ?? undefined;
|
||||
@@ -101,120 +181,39 @@ export default function ResourcesTable({
|
||||
|
||||
const columns = useMemo<ColumnDef<NetworkResource>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "id",
|
||||
accessorKey: "id",
|
||||
filterFn: "exactMatch",
|
||||
},
|
||||
{
|
||||
id: "name",
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("resourceColumn")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return <ResourceNameCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
accessorKey: "description",
|
||||
accessorFn: (resource) =>
|
||||
removeAllSpaces(resource?.description || "").toLowerCase(),
|
||||
},
|
||||
{
|
||||
id: "address",
|
||||
accessorKey: "address",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("address")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return <ResourceAddressCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
accessorKey: "enabled",
|
||||
},
|
||||
{
|
||||
id: "groups",
|
||||
accessorFn: (resource) => {
|
||||
let groups = (resource?.groups ?? []) as Group[];
|
||||
return groups.map((group) => group.name).join(", ");
|
||||
},
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{tCommon("groups")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return <ResourceGroupCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "group_names",
|
||||
accessorFn: (resource) => {
|
||||
const groups = (resource?.groups ?? []) as Group[];
|
||||
return groups.map((g) => g.name).filter((n): n is string => !!n);
|
||||
},
|
||||
filterFn: "arrIncludesSome",
|
||||
},
|
||||
{
|
||||
id: "policies",
|
||||
accessorKey: "id",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("policiesColumn")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return <ResourcePolicyCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "expose_service",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
return <ResourceExposeServiceCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
return <ResourceActionCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
...NetworkResourceColumns,
|
||||
{
|
||||
id: "exposed",
|
||||
accessorFn: (resource) =>
|
||||
resource?.id ? exposedResourceIds.has(resource.id) : false,
|
||||
},
|
||||
],
|
||||
[t, tCommon, exposedResourceIds],
|
||||
[exposedResourceIds],
|
||||
);
|
||||
|
||||
const statusOptions = useMemo<RadioOption<boolean | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: tCommon("all"), dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: tCommon("active"), dotClass: "bg-green-500" },
|
||||
{ value: false, label: tCommon("inactive"), dotClass: "bg-nb-gray-700" },
|
||||
{ value: undefined, label: "All", dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: "Active", dotClass: "bg-green-500" },
|
||||
{ value: false, label: "Inactive", dotClass: "bg-nb-gray-700" },
|
||||
],
|
||||
[tCommon],
|
||||
[],
|
||||
);
|
||||
|
||||
const exposedOptions = useMemo<RadioOption<boolean | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: tCommon("all") },
|
||||
{ value: true, label: t("exposed") },
|
||||
{ value: false, label: t("notExposed") },
|
||||
{ value: undefined, label: "All" },
|
||||
{ value: true, label: "Exposed" },
|
||||
{ value: false, label: "Not Exposed" },
|
||||
],
|
||||
[tCommon, t],
|
||||
[],
|
||||
);
|
||||
|
||||
const filterDefs = useMemo<TableFilterDef[]>(
|
||||
() => [
|
||||
{
|
||||
id: "enabled",
|
||||
label: tCommon("status"),
|
||||
label: "Status",
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as boolean | undefined}
|
||||
@@ -228,7 +227,7 @@ export default function ResourcesTable({
|
||||
},
|
||||
{
|
||||
id: "group_names",
|
||||
label: tCommon("groups"),
|
||||
label: "Groups",
|
||||
renderPicker: (p) => (
|
||||
<GroupsPicker
|
||||
value={p.value as string[] | undefined}
|
||||
@@ -241,7 +240,7 @@ export default function ResourcesTable({
|
||||
},
|
||||
{
|
||||
id: "exposed",
|
||||
label: tCommon("settings"),
|
||||
label: "Service",
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as boolean | undefined}
|
||||
@@ -254,7 +253,7 @@ export default function ResourcesTable({
|
||||
formatRadioChip(v as boolean | undefined, exposedOptions),
|
||||
},
|
||||
],
|
||||
[statusOptions, exposedOptions, tableGroups, tCommon],
|
||||
[statusOptions, exposedOptions, tableGroups],
|
||||
);
|
||||
|
||||
const removeResourceParam = React.useCallback(() => {
|
||||
@@ -275,7 +274,7 @@ export default function ResourcesTable({
|
||||
showSearchAndFilters={true}
|
||||
inset={false}
|
||||
tableClassName={"mt-0"}
|
||||
text={t("resources")}
|
||||
text={"Resources"}
|
||||
columns={columns}
|
||||
keepStateInLocalStorage={false}
|
||||
initialPageSize={25}
|
||||
@@ -289,20 +288,20 @@ export default function ResourcesTable({
|
||||
<TableFilterChips table={table} filters={filterDefs} />
|
||||
)}
|
||||
data={resources}
|
||||
searchPlaceholder={t("searchResources")}
|
||||
searchPlaceholder={"Search by name, address or group..."}
|
||||
isLoading={isLoading}
|
||||
getStartedCard={
|
||||
<NoResults
|
||||
className={"py-4"}
|
||||
title={
|
||||
isGroupPage
|
||||
? t("noAssignedResources")
|
||||
: t("noNetworkResources")
|
||||
? "This group has no assigned resources"
|
||||
: "This network has no resources"
|
||||
}
|
||||
description={
|
||||
isGroupPage
|
||||
? t("noAssignedResourcesDesc")
|
||||
: t("noNetworkResourcesDesc")
|
||||
? "Assign this group to your resources inside your networks to see them listed here."
|
||||
: "Add resources to this network to control what peers can access. Resources can be anything from a single IP, a subnet, or a domain."
|
||||
}
|
||||
icon={<Layers3Icon size={20} className={"text-nb-gray-400"} />}
|
||||
>
|
||||
@@ -313,7 +312,7 @@ export default function ResourcesTable({
|
||||
className={"mt-4"}
|
||||
onClick={() => router.push("/networks")}
|
||||
>
|
||||
{t("goToNetworks")}
|
||||
Go to Networks
|
||||
<ArrowUpRightIcon size={16} />
|
||||
</Button>
|
||||
</>
|
||||
@@ -340,7 +339,7 @@ export default function ResourcesTable({
|
||||
data-testid={"add-resource"}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
{t("addResourceBtn")}
|
||||
Add
|
||||
</Button>
|
||||
)
|
||||
: undefined
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import SkeletonTable, {
|
||||
SkeletonTableHeader,
|
||||
} from "@components/skeletons/SkeletonTable";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { Suspense, useMemo } from "react";
|
||||
import { NetworkRouter } from "@/interfaces/Network";
|
||||
@@ -23,7 +20,6 @@ export const NetworkRoutingPeersTabContent = ({
|
||||
routers?: NetworkRouter[];
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const t = useTranslations("networks");
|
||||
const { groups } = useGroups();
|
||||
const { users } = useUsers();
|
||||
const { data: peers } = useFetchApi<Peer[]>(`/peers`);
|
||||
@@ -48,17 +44,15 @@ export const NetworkRoutingPeersTabContent = ({
|
||||
<div className={"flex justify-between items-center mb-5"}>
|
||||
<div>
|
||||
<Paragraph>
|
||||
{t.rich("routingPeersTabDescription", {
|
||||
link: (chunks) => (
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/manage/networks#routing-peers"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{chunks}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
),
|
||||
})}
|
||||
Add routing peers to this network to access resources inside this
|
||||
network.{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/manage/networks#routing-peers"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import { DataTable } from "@components/table/DataTable";
|
||||
@@ -18,7 +16,6 @@ import {
|
||||
import NoResults from "@components/ui/NoResults";
|
||||
import { IconCirclePlus } from "@tabler/icons-react";
|
||||
import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import { useTranslations } from "next-intl";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
@@ -36,13 +33,59 @@ type Props = {
|
||||
headingTarget?: HTMLHeadingElement | null;
|
||||
};
|
||||
|
||||
const NetworkRouterColumns: ColumnDef<NetworkRouter>[] = [
|
||||
{
|
||||
id: "name",
|
||||
accessorKey: "id",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Peer</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => <NetworkRoutingPeerName router={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
accessorKey: "enabled",
|
||||
},
|
||||
{
|
||||
id: "metric",
|
||||
accessorKey: "metric",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Metric</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<RouteMetricCell metric={row.original.metric} useHoverStyle={false} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "masquerade",
|
||||
accessorKey: "masquerade",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Masquerade</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <RoutingPeersMasqueradeCell router={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
return <RoutingPeersActionCell router={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "search",
|
||||
accessorKey: "search",
|
||||
header: "",
|
||||
filterFn: "fuzzy",
|
||||
},
|
||||
];
|
||||
|
||||
export default function NetworkRoutingPeersTable({
|
||||
routers,
|
||||
isLoading,
|
||||
headingTarget,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
const { openAddRoutingPeerModal, network } = useNetworksContext();
|
||||
|
||||
@@ -55,18 +98,18 @@ export default function NetworkRoutingPeersTable({
|
||||
|
||||
const statusOptions = useMemo<RadioOption<boolean | undefined>[]>(
|
||||
() => [
|
||||
{ value: undefined, label: tCommon("all"), dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: tCommon("active"), dotClass: "bg-green-500" },
|
||||
{ value: false, label: tCommon("inactive"), dotClass: "bg-nb-gray-700" },
|
||||
{ value: undefined, label: "All", dotClass: "bg-nb-gray-500" },
|
||||
{ value: true, label: "Active", dotClass: "bg-green-500" },
|
||||
{ value: false, label: "Inactive", dotClass: "bg-nb-gray-700" },
|
||||
],
|
||||
[tCommon],
|
||||
[],
|
||||
);
|
||||
|
||||
const filterDefs = useMemo<TableFilterDef[]>(
|
||||
() => [
|
||||
{
|
||||
id: "enabled",
|
||||
label: tCommon("status"),
|
||||
label: "Status",
|
||||
renderPicker: (p) => (
|
||||
<RadioPicker
|
||||
value={p.value as boolean | undefined}
|
||||
@@ -79,58 +122,7 @@ export default function NetworkRoutingPeersTable({
|
||||
formatRadioChip(v as boolean | undefined, statusOptions),
|
||||
},
|
||||
],
|
||||
[statusOptions, tCommon],
|
||||
);
|
||||
|
||||
const columns = useMemo<ColumnDef<NetworkRouter>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "name",
|
||||
accessorKey: "id",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("peer")}</DataTableHeader>;
|
||||
},
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => <NetworkRoutingPeerName router={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
accessorKey: "enabled",
|
||||
},
|
||||
{
|
||||
id: "metric",
|
||||
accessorKey: "metric",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("metric")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<RouteMetricCell metric={row.original.metric} useHoverStyle={false} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "masquerade",
|
||||
accessorKey: "masquerade",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>{t("masquerade")}</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <RoutingPeersMasqueradeCell router={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
return <RoutingPeersActionCell router={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "search",
|
||||
accessorKey: "search",
|
||||
header: "",
|
||||
filterFn: "fuzzy",
|
||||
},
|
||||
],
|
||||
[t],
|
||||
[statusOptions],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -144,8 +136,8 @@ export default function NetworkRoutingPeersTable({
|
||||
showSearchAndFilters={true}
|
||||
inset={false}
|
||||
tableClassName={"mt-0"}
|
||||
text={t("routingPeers")}
|
||||
columns={columns}
|
||||
text={"Routing Peers"}
|
||||
columns={NetworkRouterColumns}
|
||||
keepStateInLocalStorage={false}
|
||||
initialPageSize={25}
|
||||
showResetFilterButton={false}
|
||||
@@ -153,13 +145,15 @@ export default function NetworkRoutingPeersTable({
|
||||
<TableFilterChips table={table} filters={filterDefs} />
|
||||
)}
|
||||
data={routers}
|
||||
searchPlaceholder={t("searchRoutingPeers")}
|
||||
searchPlaceholder={"Search by peer name, group name..."}
|
||||
isLoading={isLoading}
|
||||
getStartedCard={
|
||||
<NoResults
|
||||
className={"py-4"}
|
||||
title={t("noRoutingPeers")}
|
||||
description={t("noRoutingPeersDesc")}
|
||||
title={"This network has no routing peers"}
|
||||
description={
|
||||
"Add routing peers to this network to access resources inside this network."
|
||||
}
|
||||
icon={<PeerIcon size={18} className={"fill-nb-gray-400"} />}
|
||||
/>
|
||||
}
|
||||
@@ -174,7 +168,7 @@ export default function NetworkRoutingPeersTable({
|
||||
disabled={!permission.networks.update}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
{t("addRoutingPeerBtn")}
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Callout } from "@components/Callout";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
@@ -28,7 +25,6 @@ export const RoutingPeerMasqueradeSwitch = ({
|
||||
routingPeerGroupId,
|
||||
"data-testid": dataTestId,
|
||||
}: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
return (
|
||||
<RoutingPeerMasqueradeTooltip show={disabled}>
|
||||
<div className={"flex flex-col gap-4"}>
|
||||
@@ -40,10 +36,12 @@ export const RoutingPeerMasqueradeSwitch = ({
|
||||
label={
|
||||
<>
|
||||
<VenetianMask size={15} />
|
||||
{t("masquerade")}
|
||||
Masquerade
|
||||
</>
|
||||
}
|
||||
helpText={t("masqueradeHelp")}
|
||||
helpText={
|
||||
"Allow access to your private networks without configuring routes on your local routers or other devices."
|
||||
}
|
||||
/>
|
||||
{routingPeerGroupId && !value && (
|
||||
<RoutingPeerGroupNonLinuxWarning
|
||||
@@ -64,12 +62,11 @@ export const RoutingPeerMasqueradeTooltip = ({
|
||||
show = false,
|
||||
children,
|
||||
}: RoutingPeerMasqueradeTooltipProps) => {
|
||||
const t = useTranslations("networks");
|
||||
return (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs"}>
|
||||
{t("masqueradeTooltip")}
|
||||
Masquerade needs to be enabled for non-Linux routing peers.
|
||||
</div>
|
||||
}
|
||||
delayDuration={250}
|
||||
@@ -87,7 +84,6 @@ const RoutingPeerGroupNonLinuxWarning = ({
|
||||
}: {
|
||||
routingPeerGroupId: string;
|
||||
}) => {
|
||||
const t = useTranslations("networks");
|
||||
const { groups } = useGroups();
|
||||
const { data: peers } = useFetchApi<Peer[]>("/peers", true);
|
||||
const group = groups?.find((g) => g.id === routingPeerGroupId);
|
||||
@@ -116,12 +112,10 @@ const RoutingPeerGroupNonLinuxWarning = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t.rich("masqueradeNonLinuxWarning", {
|
||||
important: (chunks) => (
|
||||
<span className={"text-netbird font-normal"}>{chunks}</span>
|
||||
),
|
||||
groupName: group?.name || "",
|
||||
})}
|
||||
Group <span className={"text-netbird font-normal"}>{group?.name}</span>{" "}
|
||||
contains at least one non-Linux peer.
|
||||
<br /> Disabled Masquerade will have no effect on non-Linux routing
|
||||
peers.
|
||||
</Callout>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -9,7 +7,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/DropdownMenu";
|
||||
import { notify } from "@components/Notification";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import {
|
||||
MoreVertical,
|
||||
@@ -28,7 +25,6 @@ type Props = {
|
||||
router: NetworkRouter;
|
||||
};
|
||||
export const RoutingPeersActionCell = ({ router }: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const { permission } = usePermissions();
|
||||
const { deleteRouter, network, openAddRoutingPeerModal } =
|
||||
useNetworksContext();
|
||||
@@ -42,9 +38,9 @@ export const RoutingPeersActionCell = ({ router }: Props) => {
|
||||
const toggleEnabled = async () => {
|
||||
const nextEnabled = !router.enabled;
|
||||
notify({
|
||||
title: t("networkRoutingPeer"),
|
||||
description: nextEnabled ? t("routingPeerEnabled") : t("routingPeerDisabled"),
|
||||
loadingMessage: t("updatingRoutingPeer"),
|
||||
title: "Network Routing Peer",
|
||||
description: `Routing peer is now ${nextEnabled ? "enabled" : "disabled"}`,
|
||||
loadingMessage: "Updating routing peer...",
|
||||
duration: 1200,
|
||||
promise: update({
|
||||
...router,
|
||||
@@ -72,7 +68,7 @@ export const RoutingPeersActionCell = ({ router }: Props) => {
|
||||
disabled={
|
||||
!permission.networks.update && !permission.networks.delete
|
||||
}
|
||||
aria-label={t("routerEdit")}
|
||||
aria-label={"Routing peer actions"}
|
||||
>
|
||||
<MoreVertical size={16} className={"shrink-0"} />
|
||||
</Button>
|
||||
@@ -87,7 +83,7 @@ export const RoutingPeersActionCell = ({ router }: Props) => {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<SquarePenIcon size={14} className={"shrink-0"} />
|
||||
{t("routerEdit")}
|
||||
Edit
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -99,7 +95,7 @@ export const RoutingPeersActionCell = ({ router }: Props) => {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<PowerIcon size={14} className={"shrink-0"} />
|
||||
{router.enabled ? t("routerDisable") : t("routerEnable")}
|
||||
{router.enabled ? "Disable" : "Enable"}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -113,7 +109,7 @@ export const RoutingPeersActionCell = ({ router }: Props) => {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
{t("remove")}
|
||||
Remove
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { notify } from "@components/Notification";
|
||||
import { ToggleSwitch } from "@components/ToggleSwitch";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
@@ -15,7 +12,6 @@ type Props = {
|
||||
router: NetworkRouter;
|
||||
};
|
||||
export const RoutingPeersEnabledCell = ({ router }: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const { permission } = usePermissions();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { network } = useNetworksContext();
|
||||
@@ -26,9 +22,9 @@ export const RoutingPeersEnabledCell = ({ router }: Props) => {
|
||||
|
||||
const toggle = async (enabled: boolean) => {
|
||||
notify({
|
||||
title: t("networkRoutingPeer"),
|
||||
description: enabled ? t("routingPeerEnabled") : t("routingPeerDisabled"),
|
||||
loadingMessage: t("updatingRoutingPeer"),
|
||||
title: "Network Routing Peer",
|
||||
description: `Routing peer is now ${enabled ? "enabled" : "disabled"}`,
|
||||
loadingMessage: "Updating routing peer...",
|
||||
promise: update({
|
||||
...router,
|
||||
enabled,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { notify } from "@components/Notification";
|
||||
import { ToggleSwitch } from "@components/ToggleSwitch";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import * as React from "react";
|
||||
@@ -19,7 +16,6 @@ type Props = {
|
||||
router: NetworkRouter;
|
||||
};
|
||||
export const RoutingPeersMasqueradeCell = ({ router }: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const { permission } = usePermissions();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { network } = useNetworksContext();
|
||||
@@ -44,9 +40,9 @@ export const RoutingPeersMasqueradeCell = ({ router }: Props) => {
|
||||
|
||||
const toggle = async (enabled: boolean) => {
|
||||
notify({
|
||||
title: t("networkRoutingPeer"),
|
||||
description: enabled ? t("masqueradeEnabled") : t("masqueradeDisabled"),
|
||||
loadingMessage: t("updatingMasquerade"),
|
||||
title: "Network Routing Peer",
|
||||
description: `Masquerade is now ${enabled ? "enabled" : "disabled"}`,
|
||||
loadingMessage: "Updating masquerade...",
|
||||
promise: update({
|
||||
...router,
|
||||
masquerade: enabled,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -8,7 +6,6 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/DropdownMenu";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { EyeIcon, MoreVertical, PencilLineIcon, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
@@ -20,8 +17,6 @@ type Props = {
|
||||
network: Network;
|
||||
};
|
||||
export default function NetworkActionCell({ network }: Readonly<Props>) {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
const { deleteNetwork, openEditNetworkModal } = useNetworksContext();
|
||||
const router = useRouter();
|
||||
@@ -47,7 +42,7 @@ export default function NetworkActionCell({ network }: Readonly<Props>) {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<EyeIcon size={14} className={"shrink-0"} />
|
||||
{t("viewDetails")}
|
||||
View Details
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
@@ -57,7 +52,7 @@ export default function NetworkActionCell({ network }: Readonly<Props>) {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<PencilLineIcon size={14} className={"shrink-0"} />
|
||||
{t("renameNetwork")}
|
||||
Rename
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -70,7 +65,7 @@ export default function NetworkActionCell({ network }: Readonly<Props>) {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
{tCommon("delete")}
|
||||
Delete
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import Button from "@components/Button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { LayersIcon, PlusCircle } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as React from "react";
|
||||
@@ -15,7 +12,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NetworkResourceCell = ({ network }: Props) => {
|
||||
const t = useTranslations("networks");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { openResourceModal } = useNetworksContext();
|
||||
@@ -46,7 +42,7 @@ export const NetworkResourceCell = ({ network }: Props) => {
|
||||
data-testid={"add-resource"}
|
||||
>
|
||||
<PlusCircle size={12} />
|
||||
{t("addResourceBtn")}
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -59,7 +55,7 @@ export const NetworkResourceCell = ({ network }: Props) => {
|
||||
data-testid={"add-resource"}
|
||||
>
|
||||
<PlusCircle size={12} />
|
||||
{t("addResourceBtn")}
|
||||
Add
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Badge from "@components/Badge";
|
||||
import Button from "@components/Button";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { HelpCircle, PlusCircle } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -17,10 +14,29 @@ type Props = {
|
||||
network: Network;
|
||||
};
|
||||
export default function NetworkRoutingPeerCell({ network }: Props) {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
const router = useRouter();
|
||||
const disabledText = useMemo(
|
||||
() => (
|
||||
<>
|
||||
High availability is currently{" "}
|
||||
<span className={"text-yellow-400 font-medium"}>inactive</span> for this
|
||||
network.
|
||||
</>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const enabledText = useMemo(
|
||||
() => (
|
||||
<>
|
||||
High availability is{" "}
|
||||
<span className={"text-green-500 font-medium"}>active</span> for this
|
||||
network.
|
||||
</>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const { openAddRoutingPeerModal } = useNetworksContext();
|
||||
|
||||
@@ -31,22 +47,26 @@ export default function NetworkRoutingPeerCell({ network }: Props) {
|
||||
network?.routing_peers_count && network.routing_peers_count > 0
|
||||
);
|
||||
|
||||
const statusLabel = isHighlyAvailable ? tCommon("active") : tCommon("inactive");
|
||||
const tooltipText = isHighlyAvailable
|
||||
? t("highAvailabilityActiveText", { status: statusLabel })
|
||||
: t("highAvailabilityInactiveText", { status: statusLabel });
|
||||
const helpText = isHighlyAvailable
|
||||
? t("highAvailabilityHelpActive")
|
||||
: t("highAvailabilityHelpInactive");
|
||||
|
||||
return (
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<FullTooltip
|
||||
interactive={false}
|
||||
content={
|
||||
<div className={"max-w-xs text-xs"}>
|
||||
<div>{tooltipText}</div>
|
||||
<div className={"inline-flex mt-2"}>{helpText}</div>
|
||||
<>
|
||||
{isHighlyAvailable ? enabledText : disabledText}
|
||||
{isHighlyAvailable ? (
|
||||
<div className={"inline-flex mt-2"}>
|
||||
You can add more routing peers to increase the availability of
|
||||
this network.
|
||||
</div>
|
||||
) : (
|
||||
<div className={"inline-flex mt-2"}>
|
||||
Go ahead and add more routing peers or groups with routing
|
||||
peers to enable high availability for this network.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -68,7 +88,8 @@ export default function NetworkRoutingPeerCell({ network }: Props) {
|
||||
isHighlyAvailable ? "bg-green-500" : "bg-yellow-400",
|
||||
)}
|
||||
></div>
|
||||
{t("peerCount", { count: network?.routing_peers_count ?? 0 })}
|
||||
{network?.routing_peers_count && network.routing_peers_count}{" "}
|
||||
Peer(s)
|
||||
</>
|
||||
|
||||
<HelpCircle size={12} />
|
||||
@@ -81,10 +102,10 @@ export default function NetworkRoutingPeerCell({ network }: Props) {
|
||||
className={"!px-3"}
|
||||
onClick={() => openAddRoutingPeerModal(network)}
|
||||
disabled={!permission.networks.update}
|
||||
aria-label={t("addRoutingPeer")}
|
||||
aria-label={"Add routing peer"}
|
||||
>
|
||||
<PlusCircle size={12} />
|
||||
{t("addRoutingPeerBtn")}
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,13 +8,12 @@ import {
|
||||
import { CalendarClockIcon, CalendarIcon, Check } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import * as React from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Avatar from "@/assets/avatars/jack.jpeg";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import { useExperiment } from "@/cloud/cloud-hooks/useExperiment";
|
||||
import { useAnalytics } from "@/contexts/AnalyticsProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { useAccount } from "@/modules/account/useAccount";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -29,58 +28,83 @@ export const OnboardingDemoCall = ({ open, onOpenChange }: Props) => {
|
||||
const { trackEventV2 } = useAnalytics();
|
||||
const account = useAccount();
|
||||
const { loggedInUser } = useLoggedInUser();
|
||||
const t = useTranslations("onboarding");
|
||||
|
||||
const [variant, variantKey] = useExperiment("onboarding-call", {
|
||||
v1: {
|
||||
title: t("demoCall.v1.title"),
|
||||
desc: t("demoCall.v1.desc"),
|
||||
title: "Book a Technical Overview (Not a Sales Call)",
|
||||
desc: "You’ll meet with a solutions engineer who will walk through how NetBird works, answer your implementation questions - no slides, no hard sell.",
|
||||
features: [
|
||||
t("demoCall.v1.feature1"),
|
||||
t("demoCall.v1.feature2"),
|
||||
t("demoCall.v1.feature3"),
|
||||
"Live walkthrough of setup and architecture",
|
||||
"Implementation of use case, for your stack",
|
||||
"Best practices and general overview",
|
||||
],
|
||||
cta: t("demoCall.v1.cta"),
|
||||
cancel: t("demoCall.v1.cancel"),
|
||||
cta: "Book Now",
|
||||
cancel: "No Thanks",
|
||||
},
|
||||
v2: {
|
||||
title: t("demoCall.v2.title"),
|
||||
desc: t.rich("demoCall.v2.desc", { br: () => <br /> }),
|
||||
title: "Talk to our Solutions Engineer",
|
||||
desc: (
|
||||
<>
|
||||
Get a 30-min technical overview. We’ll go over your specific use-case
|
||||
and answer any technical questions you might have. <br /> We’re
|
||||
offering this as a technical onboard support for you. <br /> This is
|
||||
NOT a sales call.
|
||||
</>
|
||||
),
|
||||
features: [],
|
||||
cta: t("demoCall.v2.cta"),
|
||||
cancel: t("demoCall.v2.cancel"),
|
||||
cta: "Book Now",
|
||||
cancel: "No Thanks",
|
||||
},
|
||||
v3: {
|
||||
title: t("demoCall.v3.title"),
|
||||
desc: t.rich("demoCall.v3.desc", { br: () => <br /> }),
|
||||
title: "Book a Technical Overview (Not a Sales Call)",
|
||||
desc: (
|
||||
<>
|
||||
Get a 30-min technical overview. We’ll go over your specific use-case
|
||||
and answer any technical questions you might have. <br /> We’re
|
||||
offering this as a technical onboard support for you. <br /> This is
|
||||
NOT a sales call.
|
||||
</>
|
||||
),
|
||||
features: [],
|
||||
cta: t("demoCall.v3.cta"),
|
||||
cancel: t("demoCall.v3.cancel"),
|
||||
cta: "Book Now",
|
||||
cancel: "No Thanks",
|
||||
},
|
||||
v4: {
|
||||
title: t("demoCall.v4.title"),
|
||||
desc: t("demoCall.v4.desc"),
|
||||
title: "Book a Technical Overview",
|
||||
desc: "You’ll meet with a solutions engineer who will walk through how NetBird works, answer your implementation questions - no slides, no hard sell.",
|
||||
features: [
|
||||
t("demoCall.v4.feature1"),
|
||||
t("demoCall.v4.feature2"),
|
||||
t("demoCall.v4.feature3"),
|
||||
"Live walkthrough of setup and architecture",
|
||||
"Implementation of use case, for your stack",
|
||||
"Best practices and general overview",
|
||||
],
|
||||
cta: t("demoCall.v4.cta"),
|
||||
cancel: t("demoCall.v4.cancel"),
|
||||
cta: "Book Now",
|
||||
cancel: "No Thanks",
|
||||
},
|
||||
v5: {
|
||||
title: t("demoCall.v5.title"),
|
||||
desc: t.rich("demoCall.v5.desc", { br: () => <br /> }),
|
||||
title: "Talk to our Solutions Engineer",
|
||||
desc: (
|
||||
<>
|
||||
Get a 30-min technical overview. We’ll go over your specific use-case
|
||||
and answer any technical questions you might have. <br /> We’re
|
||||
offering this as a technical onboard support for you.
|
||||
</>
|
||||
),
|
||||
features: [],
|
||||
cta: t("demoCall.v5.cta"),
|
||||
cancel: t("demoCall.v5.cancel"),
|
||||
cta: "Book Now",
|
||||
cancel: "No Thanks",
|
||||
},
|
||||
v6: {
|
||||
title: t("demoCall.v6.title"),
|
||||
desc: t.rich("demoCall.v6.desc", { br: () => <br /> }),
|
||||
title: "Book a Technical Overview",
|
||||
desc: (
|
||||
<>
|
||||
Get a 30-min technical overview. We’ll go over your specific use-case
|
||||
and answer any technical questions you might have. <br /> We’re
|
||||
offering this as a technical onboard support for you.
|
||||
</>
|
||||
),
|
||||
features: [],
|
||||
cta: t("demoCall.v6.cta"),
|
||||
cancel: t("demoCall.v6.cancel"),
|
||||
cta: "Book Now",
|
||||
cancel: "No Thanks",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -227,7 +251,8 @@ export const OnboardingDemoCall = ({ open, onOpenChange }: Props) => {
|
||||
>
|
||||
<CalendarClockIcon size={12} />
|
||||
<div>
|
||||
{t("demoCall.duration", { duration: 30 })}
|
||||
The call usually takes around
|
||||
<span className={"font-medium"}> 30 minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ArrowRightIcon, PlayIcon } from "lucide-react";
|
||||
import Image, { StaticImageData } from "next/image";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import ACLImage from "@/assets/onboarding/acl.png";
|
||||
import ActivityImage from "@/assets/onboarding/activity.png";
|
||||
import PostureCheckImage from "@/assets/onboarding/posture.png";
|
||||
@@ -14,71 +13,78 @@ type Props = {
|
||||
};
|
||||
|
||||
export const OnboardingEnd = ({ onFinish }: Props) => {
|
||||
const { oidcUser: user } = useOidcUser();
|
||||
const name = user?.given_name || user?.name || user?.preferred_username;
|
||||
const t = useTranslations("onboarding");
|
||||
const { oidcUser: user } = useOidcUser();
|
||||
const name = user?.given_name || user?.name || user?.preferred_username;
|
||||
|
||||
const title = name ? t("congratulationsName", { name }) : t("congratulations");
|
||||
const title = name ? `Congratulations, ${name}!` : "Congratulations!";
|
||||
|
||||
return (
|
||||
<div className={"relative flex flex-col h-full justify-between"}>
|
||||
<div>
|
||||
<h1 className={"text-xl text-center max-w-sm mx-auto"}>
|
||||
{title} <br />
|
||||
{t("completedOnboarding")}
|
||||
</h1>
|
||||
<div
|
||||
className={
|
||||
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
|
||||
}
|
||||
>
|
||||
{t("whatsNext")}
|
||||
</div>
|
||||
return (
|
||||
<div className={"relative flex flex-col h-full justify-between"}>
|
||||
<div>
|
||||
<h1 className={"text-xl text-center max-w-sm mx-auto"}>
|
||||
{title} <br />
|
||||
You’ve completed the onboarding.
|
||||
</h1>
|
||||
<div
|
||||
className={
|
||||
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
|
||||
}
|
||||
>
|
||||
What’s next? Check out these guides to get the most out of NetBird. To
|
||||
learn more, explore the dashboard, visit our documentation, or browse
|
||||
our YouTube channel.
|
||||
</div>
|
||||
|
||||
<div className={"mt-8 flex flex-col gap-8"}>
|
||||
<VideoGuide
|
||||
title={t("videoAccessControlTitle")}
|
||||
src={ACLImage}
|
||||
description={t("videoAccessControlDescription")}
|
||||
href={"https://www.youtube.com/watch?v=WtZD_q-g_Jc"}
|
||||
/>
|
||||
<VideoGuide
|
||||
title={t("videoIdPTitle")}
|
||||
src={PostureCheckImage}
|
||||
description={t("videoIdPDescription")}
|
||||
href={"https://www.youtube.com/watch?v=RxYWTpf7cgY"}
|
||||
/>
|
||||
<VideoGuide
|
||||
title={t("videoHowNetBirdWorksTitle")}
|
||||
description={t("videoHowNetBirdWorksDescription")}
|
||||
src={ActivityImage}
|
||||
href={"https://www.youtube.com/watch?v=CFa7SY4Up9k&t=261s"}
|
||||
/>
|
||||
</div>
|
||||
<div className={"mt-8 flex flex-col gap-8"}>
|
||||
<VideoGuide
|
||||
title={"Access Control in Under 5 Minutes"}
|
||||
src={ACLImage}
|
||||
description={
|
||||
"Learn how to manage access for your network resources effectively. Whether you want to restrict access to specific machines or allow certain users to connect."
|
||||
}
|
||||
href={"https://www.youtube.com/watch?v=WtZD_q-g_Jc"}
|
||||
/>
|
||||
<VideoGuide
|
||||
title={"Provision Users & Groups From Your IdP"}
|
||||
src={PostureCheckImage}
|
||||
description={
|
||||
"Learn how to provision users and groups from your identity provider, such as Okta, Azure AD, or Google Workspace, to manage access control in NetBird and automate onboarding and offboarding processes."
|
||||
}
|
||||
href={"https://www.youtube.com/watch?v=RxYWTpf7cgY"}
|
||||
/>
|
||||
<VideoGuide
|
||||
title={"How NetBird Works"}
|
||||
description={
|
||||
"Learn more about how NetBird works, its architecture, and how it can help you build secure networks."
|
||||
}
|
||||
src={ActivityImage}
|
||||
href={"https://www.youtube.com/watch?v=CFa7SY4Up9k&t=261s"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={"mt-10 flex items-center justify-center"}>
|
||||
<Button variant={"secondaryLighter"} onClick={onFinish}>
|
||||
{t("goToDashboard")}
|
||||
<ArrowRightIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className={"mt-10 flex items-center justify-center"}>
|
||||
<Button variant={"secondaryLighter"} onClick={onFinish}>
|
||||
Go to Dashboard
|
||||
<ArrowRightIcon size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type VideoGuideProps = {
|
||||
src?: string | StaticImageData;
|
||||
title: string;
|
||||
description: string;
|
||||
href?: string;
|
||||
src?: string | StaticImageData;
|
||||
title?: string;
|
||||
description?: string;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
const VideoGuide = ({
|
||||
src = ACLImage,
|
||||
title,
|
||||
description,
|
||||
href = "#",
|
||||
src = ACLImage,
|
||||
title = "Access Control in Under 5 Minutes",
|
||||
description = "Learn how to manage access for your network resources effectively. Whether you want to restrict access to specific machines or allow certain users to connect.",
|
||||
href = "#",
|
||||
}: VideoGuideProps) => {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -66,7 +66,9 @@ export const OnboardingIntent = ({ onSelect, useCases, isBusiness }: Props) => {
|
||||
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
|
||||
}
|
||||
>
|
||||
{t("description")}
|
||||
NetBird provides the flexibility of both a peer-to-peer overlay
|
||||
network and a remote network access solution. Choose what fits your
|
||||
needs, you can always combine both.
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -75,22 +77,22 @@ export const OnboardingIntent = ({ onSelect, useCases, isBusiness }: Props) => {
|
||||
)}
|
||||
>
|
||||
<IntentCard
|
||||
title={t("p2pTitle")}
|
||||
title={"Peer-to-Peer Network"}
|
||||
description={
|
||||
isBusiness
|
||||
? t("p2pDescription_business")
|
||||
: t("p2pDescription_personal")
|
||||
? "Install NetBird on two or more devices to create secure, direct WireGuard connections, like laptop to server or server to database. Add at least two machines to get started."
|
||||
: "Install NetBird on two or more devices in your homelab, such as your laptop, NAS, or Raspberry Pi, to create secure, direct WireGuard connections."
|
||||
}
|
||||
recommended={isP2PRecommended}
|
||||
icon={<PeerIcon size={18} className={"fill-netbird"} />}
|
||||
onClick={() => onSelect(Intent.P2P)}
|
||||
/>
|
||||
<IntentCard
|
||||
title={t("remoteAccessTitle")}
|
||||
title={"Remote Network Access"}
|
||||
description={
|
||||
isBusiness
|
||||
? t("remoteAccessDescription_business")
|
||||
: t("remoteAccessDescription_personal")
|
||||
? "Enable employee remote access to VMs, Kubernetes clusters, and cloud or on-prem resources without installing NetBird on every machine."
|
||||
: "Securely access your homelab remotely from anywhere without installing NetBird on every device."
|
||||
}
|
||||
recommended={isNetworksRecommended}
|
||||
icon={<NetworkRoutesIcon size={18} className={"fill-netbird"} />}
|
||||
@@ -117,7 +119,6 @@ const IntentCard = ({
|
||||
onClick,
|
||||
recommended,
|
||||
}: IntentCardProps) => {
|
||||
const t = useTranslations("onboarding");
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
@@ -142,26 +143,27 @@ const IntentCard = ({
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{recommended && (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
{t("recommendedTooltip", { title })}
|
||||
</div>
|
||||
}
|
||||
{recommended && (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
Based on your previous choices, we recommend starting with{" "}
|
||||
{title}. You can always combine both options later.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"relative",
|
||||
"inline-flex text-[0.7rem] font-light bg-netbird/10 border border-netbird-400/30 text-netbird-400 rounded-full px-2 py-1 pb-0.5 leading-none",
|
||||
"hover:bg-netbird/20 cursor-help transition-all self-center",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"relative",
|
||||
"inline-flex text-[0.7rem] font-light bg-netbird/10 border border-netbird-400/30 text-netbird-400 rounded-full px-2 py-1 pb-0.5 leading-none",
|
||||
"hover:bg-netbird/20 cursor-help transition-all self-center",
|
||||
)}
|
||||
>
|
||||
{t("recommended")}
|
||||
<HelpCircle size={10} className={"ml-1"} />
|
||||
</span>
|
||||
</FullTooltip>
|
||||
)}
|
||||
Recommended
|
||||
<HelpCircle size={10} className={"ml-1"} />
|
||||
</span>
|
||||
</FullTooltip>
|
||||
)}
|
||||
</h2>
|
||||
<p className={"!text-nb-gray-300 text-[.85rem]"}>{description}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -9,7 +7,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/DropdownMenu";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { notify } from "@components/Notification";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
@@ -24,19 +21,19 @@ import {
|
||||
TimerResetIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useMemo } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useBypass, useBypassedPeers } from "@/cloud/edr/useBypass";
|
||||
import { usePeer } from "@/contexts/PeerProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { ExitNodeDropdownButton } from "@/modules/exit-node/ExitNodeDropdownButton";
|
||||
import { useIntegrations } from "@/modules/integrations/edr/useIntegrations";
|
||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
|
||||
export default function PeerActionCell() {
|
||||
const { peer, deletePeer, update, toggleSSH, setSSHInstructionsModal } =
|
||||
@@ -45,8 +42,6 @@ export default function PeerActionCell() {
|
||||
const { mutate } = useSWRConfig();
|
||||
const { permission } = usePermissions();
|
||||
const { confirm } = useDialog();
|
||||
const t = useTranslations("peers");
|
||||
const tCommon = useTranslations("common");
|
||||
|
||||
// Approval / EDR-bypass state. We pull this directly so the action
|
||||
// menu can offer Approve / Bypass / Revoke without the inline badges
|
||||
@@ -60,16 +55,16 @@ export default function PeerActionCell() {
|
||||
|
||||
const approvePeer = async () => {
|
||||
const choice = await confirm({
|
||||
title: t("confirmApprove", { name: peer.name }),
|
||||
description: t("confirmApproveDescription"),
|
||||
confirmText: t("approve"),
|
||||
cancelText: tCommon("cancel"),
|
||||
title: `Approve peer '${peer.name}'?`,
|
||||
description: "Are you sure you want to approve this peer?",
|
||||
confirmText: "Approve",
|
||||
cancelText: "Cancel",
|
||||
type: "default",
|
||||
});
|
||||
if (!choice) return;
|
||||
notify({
|
||||
title: t("approveSuccess", { name: peer.name }),
|
||||
description: t("approveSuccessDescription"),
|
||||
title: `Peer ${peer.name} approved`,
|
||||
description: `This peer was approved and can now connect to other peers.`,
|
||||
promise: update({
|
||||
name: peer.name,
|
||||
ssh: peer.ssh_enabled,
|
||||
@@ -79,41 +74,45 @@ export default function PeerActionCell() {
|
||||
mutate("/peers");
|
||||
mutate("/groups");
|
||||
}),
|
||||
loadingMessage: t("approveLoading"),
|
||||
loadingMessage: "Approving peer...",
|
||||
});
|
||||
};
|
||||
|
||||
const handleBypassCompliance = async () => {
|
||||
const choice = await confirm({
|
||||
title: t("bypassComplianceConfirmTitle", { name: peer.name }),
|
||||
description: t("bypassComplianceConfirmDescription"),
|
||||
confirmText: t("bypassCompliance"),
|
||||
cancelText: tCommon("cancel"),
|
||||
title: `Bypass compliance for '${peer.name}'?`,
|
||||
description:
|
||||
"This will override the compliance check and allow this peer to connect. " +
|
||||
"The bypass will be automatically removed if the device becomes compliant.",
|
||||
confirmText: "Bypass Compliance",
|
||||
cancelText: "Cancel",
|
||||
type: "warning",
|
||||
});
|
||||
if (!choice || !peer.id) return;
|
||||
notify({
|
||||
title: t("bypassComplianceSuccess", { name: peer.name }),
|
||||
description: t("bypassComplianceSuccessDescription"),
|
||||
title: `Compliance bypassed for ${peer.name}`,
|
||||
description: `This peer can now connect to other peers.`,
|
||||
promise: bypassCompliance(peer.id),
|
||||
loadingMessage: t("bypassComplianceLoading"),
|
||||
loadingMessage: "Bypassing compliance...",
|
||||
});
|
||||
};
|
||||
|
||||
const handleRevokeBypass = async () => {
|
||||
const choice = await confirm({
|
||||
title: t("revokeBypassConfirmTitle", { name: peer.name }),
|
||||
description: t("revokeBypassConfirmDescription"),
|
||||
confirmText: t("revoke"),
|
||||
cancelText: tCommon("cancel"),
|
||||
title: `Revoke compliance bypass for '${peer.name}'?`,
|
||||
description:
|
||||
"This peer will be subject to normal compliance validation. " +
|
||||
"If still non-compliant, it will lose network access.",
|
||||
confirmText: "Revoke",
|
||||
cancelText: "Cancel",
|
||||
type: "warning",
|
||||
});
|
||||
if (!choice || !peer.id) return;
|
||||
notify({
|
||||
title: t("revokeBypassSuccess"),
|
||||
description: t("revokeBypassSuccessDescription", { name: peer.name }),
|
||||
title: `Compliance bypass revoked`,
|
||||
description: `Peer ${peer.name} is now subject to normal compliance validation.`,
|
||||
promise: revokeBypass(peer.id),
|
||||
loadingMessage: t("revokeBypassLoading"),
|
||||
loadingMessage: "Revoking compliance bypass...",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -144,16 +143,11 @@ export default function PeerActionCell() {
|
||||
const showRemoteAccessItems = !isMobile && !!peer.connected;
|
||||
|
||||
const toggleLoginExpiration = async () => {
|
||||
const state = peer.login_expiration_enabled
|
||||
? tCommon("disabled")
|
||||
: tCommon("enabled");
|
||||
const text = peer.login_expiration_enabled ? "disabled" : "enabled";
|
||||
const disableLoginExpiration = peer.login_expiration_enabled;
|
||||
notify({
|
||||
title: t("loginExpirationUpdated", { state }),
|
||||
description: t("loginExpirationUpdateDescription", {
|
||||
name: peer.name,
|
||||
state,
|
||||
}),
|
||||
title: `Session expiration is ${text}`,
|
||||
description: `Session expiration for peer ${peer.name} was successfully ${text}.`,
|
||||
promise: update({
|
||||
loginExpiration: !peer.login_expiration_enabled,
|
||||
inactivityExpiration: disableLoginExpiration
|
||||
@@ -163,28 +157,31 @@ export default function PeerActionCell() {
|
||||
mutate("/peers");
|
||||
mutate("/groups");
|
||||
}),
|
||||
loadingMessage: t("loginExpirationUpdating"),
|
||||
loadingMessage: "Updating session expiration...",
|
||||
});
|
||||
};
|
||||
|
||||
const disableDashboardSSH = async () => {
|
||||
const choice = await confirm({
|
||||
title: t("disableSSHConfirmation"),
|
||||
title: `Disable SSH Access?`,
|
||||
description: (
|
||||
<div>
|
||||
{t("disableSSHDescription")}{" "}
|
||||
Starting from NetBird v0.61.0, once SSH access is disabled, you cannot
|
||||
re-enable it again from the dashboard. You'll need to create an
|
||||
explicit access control policy and update your NetBird client to
|
||||
restore SSH functionality.{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/manage/peers/ssh"}
|
||||
target={"_blank"}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{tCommon("learnMore")}
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
),
|
||||
confirmText: tCommon("disable"),
|
||||
cancelText: tCommon("cancel"),
|
||||
confirmText: "Disable",
|
||||
cancelText: "Cancel",
|
||||
type: "warning",
|
||||
maxWidthClass: "max-w-xl",
|
||||
});
|
||||
@@ -213,7 +210,7 @@ export default function PeerActionCell() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<MonitorIcon size={14} className={"shrink-0"} />
|
||||
{t("viewDetails")}
|
||||
View Details
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -224,7 +221,7 @@ export default function PeerActionCell() {
|
||||
<DropdownMenuItem onClick={approvePeer}>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<CheckCircle2 size={14} className={"shrink-0"} />
|
||||
{t("approve")}
|
||||
Approve
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
@@ -233,16 +230,16 @@ export default function PeerActionCell() {
|
||||
className={"w-full block"}
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
{t("bypassTooltip", {
|
||||
integrationName: activeIntegrationName,
|
||||
})}
|
||||
Bypass {activeIntegrationName} compliance check and
|
||||
allow this peer to connect. The bypass is automatically
|
||||
removed when the device becomes compliant.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem onClick={handleBypassCompliance}>
|
||||
<div className={"flex gap-3 items-center w-full"}>
|
||||
<ShieldCheck size={14} className={"shrink-0"} />
|
||||
{t("bypassCompliance")}
|
||||
Bypass Compliance
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</FullTooltip>
|
||||
@@ -251,7 +248,7 @@ export default function PeerActionCell() {
|
||||
<DropdownMenuItem onClick={handleRevokeBypass}>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<ShieldOff size={14} className={"shrink-0"} />
|
||||
{t("revokeBypass")}
|
||||
Revoke Bypass
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
@@ -273,7 +270,9 @@ export default function PeerActionCell() {
|
||||
className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}
|
||||
>
|
||||
<IconInfoCircle size={14} />
|
||||
<span>{t("expirationDisabledTooltip")}</span>
|
||||
<span>
|
||||
Expiration is disabled for all peers added with an setup-key.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
className={"w-full block"}
|
||||
@@ -285,9 +284,8 @@ export default function PeerActionCell() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center w-full"}>
|
||||
<TimerResetIcon size={14} className={"shrink-0"} />
|
||||
{peer.login_expiration_enabled
|
||||
? t("disableLoginExpiration")
|
||||
: t("enableLoginExpiration")}
|
||||
{peer.login_expiration_enabled ? "Disable" : "Enable"} Session
|
||||
Expiration
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</FullTooltip>
|
||||
@@ -304,7 +302,7 @@ export default function PeerActionCell() {
|
||||
<div className={"flex gap-3 items-center w-full"}>
|
||||
<TerminalSquare size={14} className={"shrink-0"} />
|
||||
<div className={"flex justify-between items-center w-full"}>
|
||||
{peer.ssh_enabled ? t("disableSSH") : t("enableSSH")}
|
||||
{peer.ssh_enabled ? "Disable" : "Enable"} SSH Access
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
@@ -321,7 +319,7 @@ export default function PeerActionCell() {
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
{tCommon("delete")}
|
||||
Delete
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user