mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dc89631aa | ||
|
|
3fa0aa8f00 | ||
|
|
3a132ef553 | ||
|
|
40b2797f82 | ||
|
|
dedef3fa56 | ||
|
|
121778c4a6 | ||
|
|
d4102c5d04 | ||
|
|
e78c35bdbe | ||
|
|
6ebee98695 | ||
|
|
f4b28d5f40 | ||
|
|
b4b6d9295b | ||
|
|
4898742ee9 |
No files matched your search
Generated
+36
-80
@@ -8,7 +8,7 @@
|
||||
"name": "netbird-dashboard",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@axa-fr/react-oidc": "^5.14.0",
|
||||
"@axa-fr/react-oidc": "^7.22.18",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
@@ -48,7 +48,7 @@
|
||||
"framer-motion": "^10.16.4",
|
||||
"ip-cidr": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.287.0",
|
||||
"lucide-react": "^0.383.0",
|
||||
"next": "13.5.5",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
@@ -93,16 +93,31 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@axa-fr/react-oidc": {
|
||||
"version": "5.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@axa-fr/react-oidc/-/react-oidc-5.14.2.tgz",
|
||||
"integrity": "sha512-N+ssJlVtVHnsvlusMxY3zLPKCB+lGzeHIxWXUb0WY3uA7Z+jxx7A2m9W1kHbhYzHuihgA3rWIcdKsvtdkeKXwg==",
|
||||
"node_modules/@axa-fr/oidc-client": {
|
||||
"version": "7.22.18",
|
||||
"resolved": "https://registry.npmjs.org/@axa-fr/oidc-client/-/oidc-client-7.22.18.tgz",
|
||||
"integrity": "sha512-emXC5EfdopQyxHFNvhso05+x8x/2qJkk87MwbnGVE96dn5Hu9WTpn+mmj5ybVhKG7hizMmOtIQTgcDptnDGKHQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@openid/appauth": "1.3.1"
|
||||
"@axa-fr/oidc-client-service-worker": "7.22.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@axa-fr/oidc-client-service-worker": {
|
||||
"version": "7.22.18",
|
||||
"resolved": "https://registry.npmjs.org/@axa-fr/oidc-client-service-worker/-/oidc-client-service-worker-7.22.18.tgz",
|
||||
"integrity": "sha512-DMtERkFMSB3P2oyNqA/VWg0gtg3UccRo2YzZ2kmEvFQEUURhTgCMpp03Y0v9wWt6sPmt54hCGphoKfqLOjJj7w=="
|
||||
},
|
||||
"node_modules/@axa-fr/react-oidc": {
|
||||
"version": "7.22.18",
|
||||
"resolved": "https://registry.npmjs.org/@axa-fr/react-oidc/-/react-oidc-7.22.18.tgz",
|
||||
"integrity": "sha512-bMLdQV8kpd4OYvl1Yz8KSeZuKVdAL2fS0I4V6Up271oVLpi4sPN+xuHr2j9J9ZOFdqp4bE1eC3IxVo/Q6Cihzg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@axa-fr/oidc-client": "7.22.18",
|
||||
"@axa-fr/oidc-client-service-worker": "7.22.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "x",
|
||||
"react-dom": "x"
|
||||
"react": "^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
@@ -542,32 +557,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@openid/appauth": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@openid/appauth/-/appauth-1.3.1.tgz",
|
||||
"integrity": "sha512-e54kpi219wES2ijPzeHe1kMnT8VKH8YeTd1GAn9BzVBmutz3tBgcG1y8a4pziNr4vNjFnuD4W446Ua7ELnNDiA==",
|
||||
"dependencies": {
|
||||
"@types/base64-js": "^1.3.0",
|
||||
"@types/jquery": "^3.5.5",
|
||||
"base64-js": "^1.5.1",
|
||||
"follow-redirects": "^1.13.3",
|
||||
"form-data": "^4.0.0",
|
||||
"opener": "^1.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@openid/appauth/node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
@@ -1658,24 +1647,11 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/base64-js": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.3.2.tgz",
|
||||
"integrity": "sha512-Q2Xn2/vQHRGLRXhQ5+BSLwhHkR3JVflxVKywH0Q6fVoAiUE8fFYL2pE5/l2ZiOiBDfA8qUqRnSxln4G/NFz1Sg=="
|
||||
},
|
||||
"node_modules/@types/crypto-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
||||
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz",
|
||||
"integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==",
|
||||
"dependencies": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@@ -1731,7 +1707,8 @@
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz",
|
||||
"integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg=="
|
||||
"integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
@@ -2187,7 +2164,8 @@
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
"version": "1.0.0",
|
||||
@@ -2285,6 +2263,7 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2927,6 +2906,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -3170,6 +3150,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -4057,25 +4038,6 @@
|
||||
"tailwindcss": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
|
||||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
@@ -5351,9 +5313,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.287.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.287.0.tgz",
|
||||
"integrity": "sha512-auxP2bTGiMoELzX+6ItTeNzLmhGd/O+PHBsrXV2YwPXYCxarIFJhiMOSzFT9a1GWeYPSZtnWdLr79IVXr/5JqQ==",
|
||||
"version": "0.383.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.383.0.tgz",
|
||||
"integrity": "sha512-13xlG0CQCJtzjSQYwwJ3WRqMHtRj3EXmLlorrARt7y+IHnxUCp3XyFNL1DfaGySWxHObDvnu1u1dV+0VMKHUSg==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
@@ -5396,6 +5358,7 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -5404,6 +5367,7 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -5712,14 +5676,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||
"bin": {
|
||||
"opener": "bin/opener-bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@
|
||||
"cypress:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@axa-fr/react-oidc": "^5.14.0",
|
||||
"@axa-fr/react-oidc": "^7.22.18",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
@@ -53,7 +53,7 @@
|
||||
"framer-motion": "^10.16.4",
|
||||
"ip-cidr": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.287.0",
|
||||
"lucide-react": "^0.383.0",
|
||||
"next": "13.5.5",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
|
||||
@@ -23,6 +23,7 @@ import Separator from "@components/Separator";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import useRedirect from "@hooks/useRedirect";
|
||||
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import dayjs from "dayjs";
|
||||
@@ -66,8 +67,11 @@ import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
|
||||
export default function PeerPage() {
|
||||
const queryParameter = useSearchParams();
|
||||
const peerId = queryParameter.get("id");
|
||||
const { data: peer } = useFetchApi<Peer>("/peers/" + peerId);
|
||||
return peer ? (
|
||||
const { data: peer, isLoading } = useFetchApi<Peer>("/peers/" + peerId, true);
|
||||
|
||||
useRedirect("/peers", false, !peerId);
|
||||
|
||||
return peer && !isLoading ? (
|
||||
<PeerProvider peer={peer}>
|
||||
<PeerOverview />
|
||||
</PeerProvider>
|
||||
|
||||
@@ -10,6 +10,7 @@ import Paragraph from "@components/Paragraph";
|
||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import Separator from "@components/Separator";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import useRedirect from "@hooks/useRedirect";
|
||||
import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
|
||||
import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import { generateColorFromString } from "@utils/helpers";
|
||||
@@ -42,6 +43,8 @@ export default function UserPage() {
|
||||
return users?.find((u) => u.id === userId);
|
||||
}, [users, userId]);
|
||||
|
||||
useRedirect("/team/users", false, !userId);
|
||||
|
||||
return !isLoading && user ? (
|
||||
<UserOverview user={user} />
|
||||
) : (
|
||||
|
||||
@@ -35,6 +35,6 @@ export default function NotFound() {
|
||||
}
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
useRedirect(url == "/" ? "/peers" : url + (queryParams && `?${queryParams}`));
|
||||
useRedirect("/peers" + (queryParams && `?${queryParams}`));
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { OidcProvider } from "@axa-fr/react-oidc";
|
||||
import {
|
||||
AuthorityConfiguration,
|
||||
OidcConfiguration,
|
||||
} from "@axa-fr/react-oidc/dist/vanilla/oidc";
|
||||
OidcProvider,
|
||||
} from "@axa-fr/react-oidc";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
@@ -30,7 +30,7 @@ const auth0AuthorityConfig: AuthorityConfiguration = {
|
||||
revocation_endpoint: new URL("oauth/revoke", config.authority).href,
|
||||
end_session_endpoint: new URL("v2/logout", config.authority).href,
|
||||
userinfo_endpoint: new URL("userinfo", config.authority).href,
|
||||
//issuer: new URL("", config.authority).href,
|
||||
issuer: new URL("", config.authority).href,
|
||||
};
|
||||
|
||||
const onEvent = (configurationName: any, eventName: any, data: any) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CommandItem } from "@components/Command";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
|
||||
import { IconArrowBack } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
@@ -62,8 +63,13 @@ export function NetworkRouteSelector({
|
||||
const isSearching = search.length > 0;
|
||||
const found =
|
||||
dropdownOptions.filter((item) => {
|
||||
const hasDomains = item?.domains ? item.domains.length > 0 : false;
|
||||
const domains =
|
||||
hasDomains && item?.domains ? item?.domains.join(" ") : "";
|
||||
return (
|
||||
item.network_id.includes(search) || item.network.includes(search)
|
||||
item.network_id.includes(search) ||
|
||||
item.network?.includes(search) ||
|
||||
domains.includes(search)
|
||||
);
|
||||
}).length > 0;
|
||||
return isSearching && !found;
|
||||
@@ -117,6 +123,7 @@ export function NetworkRouteSelector({
|
||||
>
|
||||
{value.network}
|
||||
</div>
|
||||
<DomainList domains={value?.domains} />
|
||||
</div>
|
||||
) : (
|
||||
<span>Select an existing network...</span>
|
||||
@@ -208,7 +215,11 @@ export function NetworkRouteSelector({
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.network + option.network_id}
|
||||
value={option.network + option.network_id}
|
||||
value={
|
||||
option.network +
|
||||
option.network_id +
|
||||
option?.domains?.join(", ")
|
||||
}
|
||||
onSelect={() => {
|
||||
togglePeer(option);
|
||||
setOpen(false);
|
||||
@@ -226,6 +237,7 @@ export function NetworkRouteSelector({
|
||||
>
|
||||
{option.network}
|
||||
</div>
|
||||
<DomainList domains={option?.domains} />
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
@@ -238,3 +250,19 @@ export function NetworkRouteSelector({
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function DomainList({ domains }: { domains?: string[] }) {
|
||||
const firstDomain = domains ? domains[0] : "";
|
||||
return (
|
||||
domains &&
|
||||
domains.length > 0 && (
|
||||
<FullTooltip
|
||||
content={<div className={"text-xs max-w-sm"}>{domains.join(", ")}</div>}
|
||||
>
|
||||
<div className={"text-xs text-nb-gray-300"}>
|
||||
{firstDomain} {domains.length > 1 && "+" + (domains.length - 1)}
|
||||
</div>
|
||||
</FullTooltip>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ interface Props extends IconVariant {
|
||||
className?: string;
|
||||
margin?: string;
|
||||
truncate?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export default function ModalHeader({
|
||||
icon,
|
||||
@@ -19,6 +20,7 @@ export default function ModalHeader({
|
||||
className = "pb-6 px-8",
|
||||
margin = "mt-0",
|
||||
truncate = false,
|
||||
children,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className={cn(className, "min-w-0")}>
|
||||
@@ -26,11 +28,15 @@ export default function ModalHeader({
|
||||
{icon && <SquareIcon color={color} icon={icon} />}
|
||||
<div className={"min-w-0"}>
|
||||
<h2 className={"text-lg my-0 leading-[1.5]"}>{title}</h2>
|
||||
<Paragraph
|
||||
className={cn("text-sm", margin, truncate && "!block truncate")}
|
||||
>
|
||||
{description}
|
||||
</Paragraph>
|
||||
{children ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<Paragraph
|
||||
className={cn("text-sm", margin, truncate && "!block truncate")}
|
||||
>
|
||||
{description}
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import Badge from "@components/Badge";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
domains: string[];
|
||||
};
|
||||
export const DomainListBadge = ({ domains }: Props) => {
|
||||
const firstDomain = domains.length > 0 ? domains[0] : undefined;
|
||||
|
||||
return (
|
||||
<DomainsTooltip domains={domains}>
|
||||
<div className={"inline-flex items-center gap-2"}>
|
||||
{firstDomain && (
|
||||
<Badge variant={"gray"}>
|
||||
<GlobeIcon size={10} />
|
||||
{firstDomain}
|
||||
</Badge>
|
||||
)}
|
||||
{domains && domains.length > 1 && (
|
||||
<Badge variant={"gray"}>+ {domains.length - 1}</Badge>
|
||||
)}
|
||||
</div>
|
||||
</DomainsTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const DomainsTooltip = ({
|
||||
domains,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
domains: string[];
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<FullTooltip
|
||||
interactive={false}
|
||||
className={className}
|
||||
content={
|
||||
<div className={"flex flex-col gap-2 items-start"}>
|
||||
{domains.map((domain) => {
|
||||
return (
|
||||
domain && (
|
||||
<div
|
||||
key={domain}
|
||||
className={"flex gap-2 items-center justify-between w-full"}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-center text-nb-gray-300 text-xs"
|
||||
}
|
||||
>
|
||||
<GlobeIcon size={11} />
|
||||
{domain}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
disabled={domains.length <= 1}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import Button from "@components/Button";
|
||||
import { Input } from "@components/Input";
|
||||
import { validator } from "@utils/helpers";
|
||||
import { uniqueId } from "lodash";
|
||||
import { GlobeIcon, MinusCircleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Domain } from "@/interfaces/Domain";
|
||||
|
||||
type Props = {
|
||||
value: Domain;
|
||||
onChange: (d: Domain) => void;
|
||||
onRemove: () => void;
|
||||
onError?: (error: boolean) => void;
|
||||
error?: string;
|
||||
};
|
||||
enum ActionType {
|
||||
ADD = "ADD",
|
||||
REMOVE = "REMOVE",
|
||||
UPDATE = "UPDATE",
|
||||
}
|
||||
|
||||
export const domainReducer = (state: Domain[], action: any): Domain[] => {
|
||||
switch (action.type) {
|
||||
case ActionType.ADD:
|
||||
return [...state, { name: "", id: uniqueId("domain") }];
|
||||
case ActionType.REMOVE:
|
||||
return state.filter((_, i) => i !== action.index);
|
||||
case ActionType.UPDATE:
|
||||
return state.map((n, i) => (i === action.index ? action.d : n));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default function InputDomain({
|
||||
value,
|
||||
onChange,
|
||||
onRemove,
|
||||
onError,
|
||||
}: Readonly<Props>) {
|
||||
const [name, setName] = useState(value?.name || "");
|
||||
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value);
|
||||
onChange({ ...value, name: e.target.value });
|
||||
};
|
||||
|
||||
const domainError = useMemo(() => {
|
||||
if (name == "") {
|
||||
return "";
|
||||
}
|
||||
const valid = validator.isValidDomain(name);
|
||||
if (!valid) {
|
||||
return "Please enter a valid domain, e.g. example.com or intra.example.com";
|
||||
}
|
||||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
const hasError = domainError !== "" && domainError !== undefined;
|
||||
onError?.(hasError);
|
||||
return () => onError?.(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [domainError]);
|
||||
|
||||
return (
|
||||
<div className={"flex gap-2 w-full"}>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={<GlobeIcon size={15} />}
|
||||
placeholder={"e.g., example.com"}
|
||||
maxWidthClass={"w-full"}
|
||||
value={name}
|
||||
error={domainError}
|
||||
onChange={handleNameChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={"h-[42px]"}
|
||||
variant={"default-outline"}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<MinusCircleIcon size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
approval_required == undefined ? undefined : approval_required,
|
||||
},
|
||||
`/${peer.id}`,
|
||||
);
|
||||
) as Promise<Peer>;
|
||||
};
|
||||
|
||||
const openSSHDialog = async (): Promise<boolean> => {
|
||||
|
||||
@@ -34,6 +34,8 @@ export default function RoutesProvider({ children }: Props) {
|
||||
onSuccess?: (route: Route) => void,
|
||||
message?: string,
|
||||
) => {
|
||||
const hasDomains = route.domains ? route.domains.length > 0 : false;
|
||||
|
||||
notify({
|
||||
title: "Network " + route.network_id + "-" + route.network,
|
||||
description: message
|
||||
@@ -48,7 +50,9 @@ export default function RoutesProvider({ children }: Props) {
|
||||
peer: toUpdate.peer ?? (route.peer || undefined),
|
||||
peer_groups:
|
||||
toUpdate.peer_groups ?? (route.peer_groups || undefined),
|
||||
network: route.network,
|
||||
network: !hasDomains ? route.network : undefined,
|
||||
domains: hasDomains ? route.domains : undefined,
|
||||
keep_route: route.keep_route,
|
||||
metric: toUpdate.metric ?? route.metric ?? 9999,
|
||||
masquerade: toUpdate.masquerade ?? route.masquerade ?? true,
|
||||
groups: toUpdate.groups ?? route.groups ?? [],
|
||||
@@ -80,7 +84,9 @@ export default function RoutesProvider({ children }: Props) {
|
||||
enabled: route.enabled,
|
||||
peer: route.peer || undefined,
|
||||
peer_groups: route.peer_groups || undefined,
|
||||
network: route.network,
|
||||
network: route?.network || undefined,
|
||||
domains: route?.domains || undefined,
|
||||
keep_route: route?.keep_route || false,
|
||||
metric: route.metric || 9999,
|
||||
masquerade: route.masquerade,
|
||||
groups: route.groups || [],
|
||||
|
||||
@@ -19,6 +19,8 @@ export const getOperatingSystem = (os: string) => {
|
||||
if (os.toLowerCase().includes("android"))
|
||||
return OperatingSystem.ANDROID as const;
|
||||
if (os.toLowerCase().includes("ios")) return OperatingSystem.IOS as const;
|
||||
if (os.toLowerCase().includes("ipad")) return OperatingSystem.IOS as const;
|
||||
if (os.toLowerCase().includes("iphone")) return OperatingSystem.IOS as const;
|
||||
if (os.toLowerCase().includes("windows"))
|
||||
return OperatingSystem.WINDOWS as const;
|
||||
return OperatingSystem.LINUX as const;
|
||||
|
||||
+35
-15
@@ -1,8 +1,9 @@
|
||||
import loadConfig from "@utils/config";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
export const useRedirect = (
|
||||
url: string,
|
||||
replace: boolean = false,
|
||||
@@ -10,24 +11,43 @@ export const useRedirect = (
|
||||
) => {
|
||||
const router = useRouter();
|
||||
const currentPath = usePathname();
|
||||
const callBackUrls = [config.redirectURI, config.silentRedirectURI];
|
||||
const callBackUrls = useRef([config.redirectURI, config.silentRedirectURI]);
|
||||
const isRedirecting = useRef(false);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enable) return;
|
||||
if (callBackUrls.includes(url)) return; // Don't redirect to the callback urls to avoid infinite loop
|
||||
if (url === currentPath) return; // Don't redirect to the current page
|
||||
// If redirect is disabled or the url is already in the callback urls or the url is the current path then do not redirect
|
||||
if (!enable || callBackUrls.current.includes(url) || url === currentPath)
|
||||
return;
|
||||
|
||||
const redirect = replace ? router.replace : router.push; // Replace the current history or add a new one
|
||||
const performRedirect = () => {
|
||||
if (!isRedirecting.current) {
|
||||
isRedirecting.current = true;
|
||||
router.refresh();
|
||||
if (replace) {
|
||||
router.replace(url);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
isRedirecting.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
router.refresh();
|
||||
redirect(url);
|
||||
performRedirect();
|
||||
|
||||
// Timer in case the user has his browser tab open but not focused
|
||||
const interval = setInterval(() => {
|
||||
router.refresh();
|
||||
redirect(url);
|
||||
}, 1000);
|
||||
// Try to redirect after 1.25 seconds if for whatever reason the redirect did not happen (network change, browser tab open but not focused etc.)
|
||||
intervalRef.current = setInterval(() => {
|
||||
if (!isRedirecting.current) {
|
||||
performRedirect();
|
||||
}
|
||||
}, 1250);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [replace, router, url, enable]);
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [replace, router, url, enable, currentPath]);
|
||||
};
|
||||
|
||||
export default useRedirect;
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface Domain {
|
||||
id?: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -17,11 +17,6 @@ export interface Nameserver {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
id?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const NameserverPresets: Record<string, NameserverGroup> = {
|
||||
Default: {
|
||||
name: "",
|
||||
|
||||
+10
-2
@@ -3,26 +3,34 @@ export interface Route {
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
peer?: string;
|
||||
network: string;
|
||||
network?: string;
|
||||
domains?: string[];
|
||||
network_id: string;
|
||||
network_type?: string;
|
||||
metric?: number;
|
||||
masquerade: boolean;
|
||||
groups: string[];
|
||||
keep_route?: boolean;
|
||||
// Frontend only
|
||||
peer_groups?: string[];
|
||||
routesGroups?: string[];
|
||||
groupedRoutes?: GroupedRoute[];
|
||||
group_names?: string[];
|
||||
domain_search?: string;
|
||||
}
|
||||
|
||||
export interface GroupedRoute {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
network: string;
|
||||
network?: string;
|
||||
domains?: string[];
|
||||
keep_route?: boolean;
|
||||
network_id: string;
|
||||
high_availability_count: number;
|
||||
is_using_route_groups: boolean;
|
||||
routes?: Route[];
|
||||
group_names?: string[];
|
||||
description?: string;
|
||||
description_search?: string;
|
||||
domain_search?: string;
|
||||
}
|
||||
@@ -3,7 +3,11 @@ import { Label } from "@components/Label";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { isLocalDev, isProduction } from "@utils/netbird";
|
||||
import { isEmpty } from "lodash";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import React, { useMemo } from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { useCountries } from "@/contexts/CountryProvider";
|
||||
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
||||
|
||||
type Props = {
|
||||
@@ -54,7 +58,8 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "setupkey.peer.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
|
||||
with the NetBird IP <Value>{m.ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -113,29 +118,38 @@ export default function ActivityDescription({ event }: Props) {
|
||||
* Route
|
||||
*/
|
||||
|
||||
if (event.activity_code == "route.delete")
|
||||
if (event.activity_code == "route.delete") {
|
||||
let hasDomains = m?.domains && m?.domains.length > 0;
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Route <Value>{m.name}</Value> with the <Value>{m.network_range}</Value>{" "}
|
||||
range was deleted
|
||||
Route <Value>{m.name}</Value> with the {hasDomains ? "domain(s)" : ""}{" "}
|
||||
<Value>{hasDomains ? m?.domains : m.network_range}</Value>{" "}
|
||||
{hasDomains ? "" : "range"} was deleted
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.activity_code == "route.update")
|
||||
if (event.activity_code == "route.update") {
|
||||
let hasDomains = m?.domains && m?.domains.length > 0;
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Route <Value>{m.name}</Value> with the <Value>{m.network_range}</Value>{" "}
|
||||
range was updated
|
||||
Route <Value>{m.name}</Value> with the {hasDomains ? "domain(s)" : ""}{" "}
|
||||
<Value>{hasDomains ? m?.domains : m.network_range}</Value>{" "}
|
||||
{hasDomains ? "" : "range"} was updated
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.activity_code == "route.add")
|
||||
if (event.activity_code == "route.add") {
|
||||
let hasDomains = m?.domains && m?.domains.length > 0;
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Route <Value>{m.name}</Value> with the <Value>{m.network_range}</Value>{" "}
|
||||
range was created
|
||||
Route <Value>{m.name}</Value> with the {hasDomains ? "domain(s)" : ""}{" "}
|
||||
<Value>{hasDomains ? m?.domains : m.network_range}</Value>{" "}
|
||||
{hasDomains ? "" : "range"} was created
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* User
|
||||
@@ -144,21 +158,24 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "user.peer.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was deleted
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
|
||||
NetBird IP <Value>{m.ip}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.peer.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was added
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
|
||||
with the NetBird IP <Value>{m.ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "user.peer.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> with ip <Value>{m.ip}</Value> was updated
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> with
|
||||
NetBird IP <Value>{m.ip}</Value> was updated
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -252,15 +269,15 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "peer.group.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{m.group}</Value> was removed from the peer with the ip{" "}
|
||||
<Value>{m.peer_ip}</Value>
|
||||
Group <Value>{m.group}</Value> was removed from the peer with the
|
||||
NetBird IP <Value>{m.peer_ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.group.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Group <Value>{m.group}</Value> was added to the peer with the ip{" "}
|
||||
Group <Value>{m.group}</Value> was added to the peer with the NetBird IP{" "}
|
||||
<Value>{m.peer_ip}</Value>
|
||||
</div>
|
||||
);
|
||||
@@ -303,7 +320,7 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "peer.rename")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer with the ip <Value>{m.ip}</Value> was renamed to{" "}
|
||||
Peer with the NetBird IP <Value>{m.ip}</Value> was renamed to{" "}
|
||||
<Value>{m.name}</Value>
|
||||
</div>
|
||||
);
|
||||
@@ -311,7 +328,7 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "peer.approve")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer with the ip <Value>{m.ip}</Value> was approved
|
||||
Peer with the NetBird IP <Value>{m.ip}</Value> was approved
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -559,7 +576,7 @@ function Value({
|
||||
return children ? (
|
||||
<span
|
||||
className={cn(
|
||||
"text-nb-gray-200 inline font-medium bg-nb-gray-900 py-[3px] text-[11px] px-[5px] border border-nb-gray-800 rounded-[4px]",
|
||||
"text-nb-gray-200 inline-flex gap-1 items-center max-h-[22px] font-medium bg-nb-gray-900 py-[3px] text-[11px] px-[5px] border border-nb-gray-800 rounded-[4px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -567,3 +584,40 @@ function Value({
|
||||
</span>
|
||||
) : null;
|
||||
}
|
||||
|
||||
function PeerConnectionInfo({ meta }: { meta: any }) {
|
||||
const hasMeta =
|
||||
!isEmpty(meta?.location_country_code) ||
|
||||
!isEmpty(meta?.location_connection_ip);
|
||||
const { countries } = useCountries();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
if (!countries) return "Unknown";
|
||||
const country = countries.find(
|
||||
(c) => c.country_code === meta?.location_country_code,
|
||||
);
|
||||
if (!country) return "Unknown";
|
||||
if (!meta?.location_city_name) return country.country_name;
|
||||
return `${country.country_name}, ${meta?.location_city_name}`;
|
||||
}, [countries, meta]);
|
||||
|
||||
return hasMeta ? (
|
||||
<>
|
||||
{" "}
|
||||
from{" "}
|
||||
{meta?.location_connection_ip && (
|
||||
<Value>{meta?.location_connection_ip}</Value>
|
||||
)}{" "}
|
||||
{meta?.location_country_code && (
|
||||
<Value>
|
||||
{isEmpty(meta?.location_country_code) ? (
|
||||
<GlobeIcon size={9} className={"text-nb-gray-300"} />
|
||||
) : (
|
||||
<RoundedFlag country={meta?.location_country_code} size={9} />
|
||||
)}
|
||||
{countryText}
|
||||
</Value>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
@@ -17,8 +17,9 @@ import Paragraph from "@components/Paragraph";
|
||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { Textarea } from "@components/Textarea";
|
||||
import InputDomain, { domainReducer } from "@components/ui/InputDomain";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { cn, validator } from "@utils/helpers";
|
||||
import { cn } from "@utils/helpers";
|
||||
import cidr from "ip-cidr";
|
||||
import { uniqueId } from "lodash";
|
||||
import {
|
||||
@@ -35,7 +36,7 @@ import {
|
||||
import React, { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import DNSIcon from "@/assets/icons/DNSIcon";
|
||||
import { Domain, Nameserver, NameserverGroup } from "@/interfaces/Nameserver";
|
||||
import { Nameserver, NameserverGroup } from "@/interfaces/Nameserver";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
|
||||
type Props = {
|
||||
@@ -97,19 +98,6 @@ enum ActionType {
|
||||
UPDATE = "UPDATE",
|
||||
}
|
||||
|
||||
export const domainReducer = (state: Domain[], action: any) => {
|
||||
switch (action.type) {
|
||||
case ActionType.ADD:
|
||||
return [...state, { name: "", id: uniqueId("ns") }];
|
||||
case ActionType.REMOVE:
|
||||
return state.filter((_, i) => i !== action.index);
|
||||
case ActionType.UPDATE:
|
||||
return state.map((n, i) => (i === action.index ? action.d : n));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export function NameserverModalContent({
|
||||
onSuccess,
|
||||
preset,
|
||||
@@ -199,7 +187,7 @@ export function NameserverModalContent({
|
||||
// Domains
|
||||
const [domains, setDomains] = useReducer(domainReducer, [], () => {
|
||||
if (preset?.domains?.length) {
|
||||
return preset.domains.map((d) => ({ name: d, id: uniqueId("ns") }));
|
||||
return preset.domains.map((d) => ({ name: d, id: uniqueId("domain") }));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
@@ -370,7 +358,7 @@ export function NameserverModalContent({
|
||||
<div className={"flex flex-col gap-2 w-full"}>
|
||||
{domains.map((domain, i) => {
|
||||
return (
|
||||
<DomainInput
|
||||
<InputDomain
|
||||
key={domain.id}
|
||||
value={domain}
|
||||
onChange={(d) =>
|
||||
@@ -619,63 +607,3 @@ function NameserverInput({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DomainInput({
|
||||
value,
|
||||
onChange,
|
||||
onRemove,
|
||||
onError,
|
||||
}: {
|
||||
value: Domain;
|
||||
onChange: (d: Domain) => void;
|
||||
onRemove: () => void;
|
||||
onError?: (error: boolean) => void;
|
||||
error?: string;
|
||||
}) {
|
||||
const [name, setName] = useState(value.name);
|
||||
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value);
|
||||
onChange({ ...value, name: e.target.value });
|
||||
};
|
||||
|
||||
const domainError = useMemo(() => {
|
||||
if (name == "") {
|
||||
return "";
|
||||
}
|
||||
const valid = validator.isValidDomain(name);
|
||||
if (!valid) {
|
||||
onError && onError(true);
|
||||
return "Please enter a valid domain, e.g. example.com or intra.example.com";
|
||||
}
|
||||
onError && onError(false);
|
||||
}, [name, onError]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => onError && onError(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={"flex gap-2 w-full"}>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={<GlobeIcon size={15} />}
|
||||
placeholder={"e.g., example.com"}
|
||||
maxWidthClass={"w-full"}
|
||||
value={name}
|
||||
error={domainError}
|
||||
onChange={handleNameChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={"h-[42px]"}
|
||||
variant={"default-outline"}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<MinusCircleIcon size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export const AddExitNodeButton = ({ peer, firstTime = false }: Props) => {
|
||||
) : (
|
||||
<>
|
||||
<IconDirectionSign size={16} className={"text-yellow-400"} />
|
||||
Setup Exit Node
|
||||
Set Up Exit Node
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
Loaded 20 of 35 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user