mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41dfccca76 |
No files matched your search
@@ -23,13 +23,13 @@ import Separator from "@components/Separator";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import LoginExpiredBadge from "@components/ui/LoginExpiredBadge";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import useRedirect from "@hooks/useRedirect";
|
||||
import { IconCloudLock, IconInfoCircle } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import dayjs from "dayjs";
|
||||
import { isEmpty, trim } from "lodash";
|
||||
import {
|
||||
Barcode,
|
||||
Cpu,
|
||||
FlagIcon,
|
||||
Globe,
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
NetworkIcon,
|
||||
PencilIcon,
|
||||
TerminalSquare,
|
||||
TimerResetIcon,
|
||||
} from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { toASCII } from "punycode";
|
||||
@@ -56,11 +55,11 @@ import PeerProvider, { usePeer } from "@/contexts/PeerProvider";
|
||||
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { useHasChanges } from "@/hooks/useHasChanges";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
|
||||
import { PeerExpirationToggle } from "@/modules/peer/PeerExpirationToggle";
|
||||
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
|
||||
|
||||
export default function PeerPage() {
|
||||
@@ -70,16 +69,9 @@ export default function PeerPage() {
|
||||
|
||||
useRedirect("/peers", false, !peerId);
|
||||
|
||||
const peerKey = useMemo(() => {
|
||||
let id = peer?.id ?? "";
|
||||
let ssh = peer?.ssh_enabled ? "1" : "0";
|
||||
let expiration = peer?.login_expiration_enabled ? "1" : "0";
|
||||
return `${id}-${ssh}-${expiration}`;
|
||||
}, [peer]);
|
||||
|
||||
return peer && !isLoading ? (
|
||||
<PeerProvider peer={peer} key={peerId}>
|
||||
<PeerOverview key={peerKey} />
|
||||
<PeerOverview />
|
||||
</PeerProvider>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
@@ -96,15 +88,20 @@ function PeerOverview() {
|
||||
const [loginExpiration, setLoginExpiration] = useState(
|
||||
peer.login_expiration_enabled,
|
||||
);
|
||||
const [inactivityExpiration, setInactivityExpiration] = useState(
|
||||
peer.inactivity_expiration_enabled,
|
||||
);
|
||||
const [selectedGroups, setSelectedGroups, { getAllGroupCalls }] =
|
||||
useGroupHelper({
|
||||
initial: peerGroups,
|
||||
peer,
|
||||
});
|
||||
|
||||
/**
|
||||
* Check the operating system of the peer, if it is linux, then show the routes table, otherwise hide it.
|
||||
*/
|
||||
const isLinux = useMemo(() => {
|
||||
const operatingSystem = getOperatingSystem(peer.os);
|
||||
return operatingSystem == OperatingSystem.LINUX;
|
||||
}, [peer.os]);
|
||||
|
||||
/**
|
||||
* Detect if there are changes in the peer information, if there are changes, then enable the save button.
|
||||
*/
|
||||
@@ -113,16 +110,10 @@ function PeerOverview() {
|
||||
ssh,
|
||||
selectedGroups,
|
||||
loginExpiration,
|
||||
inactivityExpiration,
|
||||
]);
|
||||
|
||||
const updatePeer = async () => {
|
||||
const updateRequest = update({
|
||||
name,
|
||||
ssh,
|
||||
loginExpiration,
|
||||
inactivityExpiration,
|
||||
});
|
||||
const updateRequest = update(name, ssh, loginExpiration);
|
||||
const groupCalls = getAllGroupCalls();
|
||||
const batchCall = groupCalls
|
||||
? [...groupCalls, updateRequest]
|
||||
@@ -133,19 +124,13 @@ function PeerOverview() {
|
||||
promise: Promise.all(batchCall).then(() => {
|
||||
mutate("/peers/" + peer.id);
|
||||
mutate("/groups");
|
||||
updateHasChangedRef([
|
||||
name,
|
||||
ssh,
|
||||
selectedGroups,
|
||||
loginExpiration,
|
||||
inactivityExpiration,
|
||||
]);
|
||||
updateHasChangedRef([name, ssh, selectedGroups, loginExpiration]);
|
||||
}),
|
||||
loadingMessage: "Saving the peer...",
|
||||
});
|
||||
};
|
||||
|
||||
const { isUser, isOwnerOrAdmin } = useLoggedInUser();
|
||||
const { isUser } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
@@ -227,43 +212,53 @@ function PeerOverview() {
|
||||
<div className={"flex gap-10 w-full mt-5 max-w-6xl"}>
|
||||
<PeerInformationCard peer={peer} />
|
||||
|
||||
<div className={"flex flex-col gap-6 w-1/2 transition-all"}>
|
||||
<div>
|
||||
<PeerExpirationToggle
|
||||
peer={peer}
|
||||
value={loginExpiration}
|
||||
icon={<TimerResetIcon size={16} />}
|
||||
onChange={(state) => {
|
||||
setLoginExpiration(state);
|
||||
!state && setInactivityExpiration(false);
|
||||
}}
|
||||
/>
|
||||
{isOwnerOrAdmin && !!peer?.user_id && (
|
||||
<div className={"flex flex-col gap-6 w-1/2"}>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div
|
||||
className={cn(
|
||||
"border border-nb-gray-900 border-t-0 rounded-b-md bg-nb-gray-940 px-[1.28rem] pt-3 pb-5 flex flex-col gap-4 mx-[0.25rem]",
|
||||
!loginExpiration
|
||||
? "opacity-50 pointer-events-none"
|
||||
: "bg-nb-gray-930/80",
|
||||
)}
|
||||
className={
|
||||
"flex gap-2 items-center !text-nb-gray-300 text-xs"
|
||||
}
|
||||
>
|
||||
<PeerExpirationToggle
|
||||
peer={peer}
|
||||
variant={"blank"}
|
||||
value={inactivityExpiration}
|
||||
onChange={setInactivityExpiration}
|
||||
title={"Require login after disconnect"}
|
||||
description={
|
||||
"Enable to require authentication after users disconnect from management for 10 minutes."
|
||||
}
|
||||
className={
|
||||
!loginExpiration ? "opacity-40 pointer-events-none" : ""
|
||||
}
|
||||
/>
|
||||
{!peer.user_id ? (
|
||||
<>
|
||||
<>
|
||||
<IconInfoCircle size={14} />
|
||||
<span>
|
||||
Login expiration is disabled for all peers added
|
||||
with an setup-key.
|
||||
</span>
|
||||
</>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LockIcon size={14} />
|
||||
<span>
|
||||
{`You don't have the required permissions to update this
|
||||
setting.`}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
}
|
||||
className={"w-full block"}
|
||||
disabled={!!peer.user_id && !isUser}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
disabled={!peer.user_id || isUser}
|
||||
value={loginExpiration}
|
||||
onChange={setLoginExpiration}
|
||||
label={
|
||||
<>
|
||||
<IconCloudLock size={16} />
|
||||
Login Expiration
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Enable to require SSO login peers to re-authenticate when their login expires."
|
||||
}
|
||||
/>
|
||||
</FullTooltip>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div
|
||||
@@ -321,7 +316,7 @@ function PeerOverview() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isUser ? (
|
||||
{isLinux && !isUser ? (
|
||||
<>
|
||||
<Separator />
|
||||
<PeerNetworkRoutesSection peer={peer} />
|
||||
@@ -339,7 +334,7 @@ function PeerOverview() {
|
||||
);
|
||||
}
|
||||
|
||||
function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
function PeerInformationCard({ peer }: { peer: Peer }) {
|
||||
const { isLoading, getRegionByPeer } = useCountries();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
@@ -375,20 +370,14 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"DNS label"}
|
||||
copyText={"Domain name"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
className={
|
||||
peer?.extra_dns_labels && peer.extra_dns_labels.length > 0
|
||||
? "items-start"
|
||||
: ""
|
||||
}
|
||||
value={peer.dns_label}
|
||||
extraText={peer?.extra_dns_labels}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
@@ -440,19 +429,6 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
}
|
||||
value={peer.os}
|
||||
/>
|
||||
|
||||
{peer.serial_number && peer.serial_number !== "" && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Barcode size={16} />
|
||||
Serial Number
|
||||
</>
|
||||
}
|
||||
value={peer.serial_number}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
@@ -499,7 +475,7 @@ interface ModalProps {
|
||||
peer: Peer;
|
||||
initialName: string;
|
||||
}
|
||||
function EditNameModal({ onSuccess, peer, initialName }: Readonly<ModalProps>) {
|
||||
function EditNameModal({ onSuccess, peer, initialName }: ModalProps) {
|
||||
const [name, setName] = useState(initialName);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
|
||||
@@ -40,7 +40,6 @@ export default function UserPage() {
|
||||
const { data: users, isLoading } = useFetchApi<User[]>(
|
||||
`/users?service_user=${isServiceUser}`,
|
||||
);
|
||||
const { isOwnerOrAdmin } = useLoggedInUser();
|
||||
|
||||
const user = useMemo(() => {
|
||||
return users?.find((u) => u.id === userId);
|
||||
@@ -50,15 +49,11 @@ export default function UserPage() {
|
||||
|
||||
const userGroups = useGroupIdsToGroups(user?.auto_groups);
|
||||
|
||||
if (!isOwnerOrAdmin && user && !isLoading) {
|
||||
return <UserOverview user={user} initialGroups={[]} />;
|
||||
}
|
||||
|
||||
if (isOwnerOrAdmin && user && !isLoading && userGroups) {
|
||||
return <UserOverview user={user} initialGroups={userGroups} />;
|
||||
}
|
||||
|
||||
return <FullScreenLoading />;
|
||||
return !isLoading && user && userGroups !== undefined ? (
|
||||
<UserOverview user={user} initialGroups={userGroups} />
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
@@ -200,7 +195,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
|
||||
<UserInformationCard user={user} />
|
||||
<div className={"flex flex-col gap-8 w-1/2 "}>
|
||||
{!user.is_service_user && isOwnerOrAdmin && (
|
||||
{!user.is_service_user && (
|
||||
<div>
|
||||
<Label>Auto-assigned groups</Label>
|
||||
<HelpText>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function CircleIcon({
|
||||
size = 11,
|
||||
inactiveDot = "gray",
|
||||
className,
|
||||
}: Readonly<Props>) {
|
||||
}: Props) {
|
||||
return (
|
||||
<span
|
||||
style={{ width: size + "px", height: size + "px" }}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function EntraIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
width="231"
|
||||
height="231"
|
||||
viewBox="0 0 231 231"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path
|
||||
d="M48.7923 180.077C53.7717 183.183 62.0492 186.635 70.8015 186.635C78.771 186.635 86.1758 184.325 92.3102 180.385C92.3102 180.385 92.323 180.385 92.3358 180.373L115.5 165.896V218.167C111.83 218.167 108.134 217.166 104.925 215.164L48.7923 180.077Z"
|
||||
fill="#225086"
|
||||
/>
|
||||
<path
|
||||
d="M100.78 19.3398L4.53017 127.91C-2.90033 136.303 -0.962501 148.982 8.67533 155.001C8.67533 155.001 44.3007 177.267 48.7923 180.077C53.7717 183.183 62.0492 186.635 70.8015 186.635C78.771 186.635 86.1758 184.325 92.3102 180.385C92.3102 180.385 92.323 180.385 92.3358 180.373L115.5 165.896L59.4953 130.887L115.513 67.6958V12.8333C110.072 12.8333 104.63 15.0022 100.78 19.3398Z"
|
||||
fill="#66DDFF"
|
||||
/>
|
||||
<path
|
||||
d="M59.4953 130.887L60.1627 131.298L115.5 165.896H115.513V67.7087L115.5 67.6958L59.4953 130.887Z"
|
||||
fill="#CBF8FF"
|
||||
/>
|
||||
<path
|
||||
d="M222.325 155.001C231.963 148.982 233.9 136.303 226.47 127.91L163.317 56.672C158.222 54.2978 152.511 52.9375 146.467 52.9375C134.596 52.9375 123.983 58.058 116.925 66.1045L115.526 67.683L171.53 130.874L115.513 165.884V218.154C119.196 218.154 122.866 217.153 126.075 215.151L222.325 154.988V155.001Z"
|
||||
fill="#074793"
|
||||
/>
|
||||
<path
|
||||
d="M115.513 12.8333V67.6958L116.912 66.1173C123.97 58.0708 134.583 52.9503 146.454 52.9503C152.511 52.9503 158.209 54.3235 163.304 56.6848L130.207 19.3527C126.37 15.015 120.929 12.8462 115.5 12.8462L115.513 12.8333Z"
|
||||
fill="#0294E4"
|
||||
/>
|
||||
<path
|
||||
d="M171.518 130.887L115.513 67.7087V165.884L171.518 130.887Z"
|
||||
fill="#96BCC2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function GoogleIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
<path d="M1 1h22v22H1z" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function JWTIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
height="2500"
|
||||
viewBox=".4 .3 99.7 100"
|
||||
width="2500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<g fill="none">
|
||||
<path
|
||||
d="m57.8 27.2-.1-26.9h-15l.1 26.9 7.5 10.3zm-15 46.1v27h15v-27l-7.5-10.3z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="m57.8 73.3 15.8 21.8 12.1-8.8-15.8-21.8-12.1-3.9zm-15-46.1-15.9-21.8-12.1 8.8 15.8 21.8 12.2 3.9z"
|
||||
fill="#00f2e6"
|
||||
/>
|
||||
<path
|
||||
d="m30.6 36-25.6-8.3-4.6 14.2 25.6 8.4 12.1-4zm31.8 18.2 7.5 10.3 25.6 8.3 4.6-14.2-25.6-8.3z"
|
||||
fill="#00b9f1"
|
||||
/>
|
||||
<path
|
||||
d="m74.5 50.3 25.6-8.4-4.6-14.2-25.6 8.3-7.5 10.3zm-48.5 0-25.6 8.3 4.6 14.2 25.6-8.3 7.5-10.3z"
|
||||
fill="#d63aff"
|
||||
/>
|
||||
<path
|
||||
d="m30.6 64.5-15.8 21.8 12.1 8.8 15.9-21.8v-12.7zm39.3-28.5 15.8-21.8-12.1-8.8-15.8 21.8v12.7z"
|
||||
fill="#fb015b"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function OktaIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
width="215"
|
||||
height="215"
|
||||
viewBox="0 0 215 215"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path
|
||||
d="M118.287 1.49306L113.865 55.9597C111.773 55.7208 109.681 55.6014 107.53 55.6014C104.841 55.6014 102.211 55.7806 99.6412 56.1986L97.1312 29.8014C97.0714 28.9653 97.7288 28.2486 98.5655 28.2486H103.048L100.896 1.55278C100.836 0.716667 101.494 0 102.271 0H116.913C117.749 0 118.407 0.716667 118.287 1.55278V1.49306ZM81.3538 4.18056C81.1147 3.40417 80.278 2.92639 79.5011 3.225L65.7557 8.24167C64.9788 8.54028 64.6202 9.43611 64.9788 10.1528L76.1544 34.5194L71.9112 36.0722C71.1343 36.3708 70.7758 37.2667 71.1343 37.9833L82.549 61.9319C86.6727 59.6625 91.1549 57.9306 95.8761 56.8556L81.4135 4.18056H81.3538ZM47.6475 19.35L79.2621 63.9028C75.258 66.5306 71.6124 69.6361 68.5048 73.2194L49.56 54.5861C48.9623 53.9889 49.0221 53.0333 49.6197 52.4958L53.086 49.6292L34.2607 30.5778C33.663 29.9806 33.7228 29.025 34.3802 28.4875L45.5558 19.1111C46.2132 18.5736 47.1097 18.6931 47.5878 19.35H47.6475ZM21.1129 45.15C20.4555 44.6722 19.4993 44.9111 19.0809 45.6278L11.7899 58.2889C11.3715 59.0056 11.6703 59.9014 12.3875 60.2597L36.6512 71.7264L34.3802 75.6083C33.9619 76.325 34.2607 77.2806 35.0376 77.5792L59.1818 88.6278C60.9149 84.1486 63.2456 79.9681 66.1143 76.2056L21.1129 45.15ZM3.24377 79.55C3.3633 78.7139 4.19998 78.2361 4.97689 78.4153L57.867 92.2111C56.4924 96.6903 55.7155 101.408 55.596 106.306L29.1211 104.156C28.2844 104.096 27.6868 103.319 27.8661 102.483L28.643 98.0639L1.92899 95.5556C1.09231 95.4958 0.554444 94.7194 0.67397 93.8833L3.18401 79.4903L3.24377 79.55ZM1.2716 115.264C0.434919 115.324 -0.102947 116.1 0.0165789 116.936L2.58638 131.329C2.70591 132.165 3.54259 132.643 4.3195 132.464L30.2566 125.715L31.0335 130.135C31.153 130.971 31.9897 131.449 32.7666 131.269L58.3451 124.222C56.851 119.803 55.8948 115.085 55.6558 110.247L1.21184 115.264H1.2716ZM9.75792 153.128C9.33958 152.411 9.63839 151.515 10.3555 151.157L59.7196 127.746C61.5723 132.165 64.0823 136.286 67.0705 139.989L45.4363 155.397C44.7789 155.875 43.8227 155.696 43.4044 154.979L41.1334 151.037L19.0809 16Line truncated
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
+20
-60
@@ -7,7 +7,6 @@ import React from "react";
|
||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Card({ children, className, ...props }: Props) {
|
||||
return (
|
||||
<div
|
||||
@@ -33,7 +32,6 @@ type CardListItemProps = {
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
tooltip?: boolean;
|
||||
extraText?: string[];
|
||||
};
|
||||
|
||||
function CardListItem({
|
||||
@@ -43,8 +41,9 @@ function CardListItem({
|
||||
copy = false,
|
||||
copyText,
|
||||
tooltip = true,
|
||||
extraText = [],
|
||||
}: CardListItemProps) {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(value as string);
|
||||
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
@@ -53,68 +52,29 @@ function CardListItem({
|
||||
)}
|
||||
>
|
||||
<div className={"flex gap-2.5 items-center text-sm"}>{label}</div>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<CardTextItem
|
||||
label={label}
|
||||
value={value}
|
||||
copy={copy}
|
||||
copyText={copyText}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
{extraText?.map((extraLabel, index) => (
|
||||
<CardTextItem
|
||||
key={index}
|
||||
label={label}
|
||||
value={extraLabel}
|
||||
copy={copy}
|
||||
copyText={copyText}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
"text-right text-nb-gray-400 text-sm flex items-center gap-2",
|
||||
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
|
||||
)}
|
||||
onClick={() =>
|
||||
copy &&
|
||||
copyToClipBoard(
|
||||
`${copyText ? copyText : label} has been copied to clipboard.`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{tooltip ? (
|
||||
<TextWithTooltip text={value as string} maxChars={40} />
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
{copy && <Copy size={13} className={"shrink-0"} />}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
type CardTextItemProps = {
|
||||
label: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
tooltip?: boolean;
|
||||
};
|
||||
|
||||
const CardTextItem = ({
|
||||
label,
|
||||
value,
|
||||
copy = false,
|
||||
copyText,
|
||||
tooltip = true,
|
||||
}: CardTextItemProps) => {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(value as string);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-right text-nb-gray-400 text-sm flex items-center gap-2",
|
||||
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
|
||||
)}
|
||||
onClick={() =>
|
||||
copy &&
|
||||
copyToClipBoard(
|
||||
`${copyText ? copyText : label} has been copied to clipboard.`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{tooltip ? (
|
||||
<TextWithTooltip text={value as string} maxChars={40} />
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
{copy && <Copy size={13} className={"shrink-0"} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Card.List = CardList;
|
||||
Card.ListItem = CardListItem;
|
||||
|
||||
|
||||
@@ -6,18 +6,9 @@ import useCopyToClipboard from "@/hooks/useCopyToClipboard";
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
message?: string;
|
||||
iconAlignment?: "left" | "right";
|
||||
className?: string;
|
||||
alwaysShowIcon?: boolean;
|
||||
};
|
||||
|
||||
export default function CopyToClipboardText({
|
||||
children,
|
||||
message,
|
||||
iconAlignment = "right",
|
||||
className,
|
||||
alwaysShowIcon = false,
|
||||
}: Props) {
|
||||
export default function CopyToClipboardText({ children, message }: Props) {
|
||||
const [wrapper, copyToClipboard, copied] = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
@@ -25,7 +16,6 @@ export default function CopyToClipboardText({
|
||||
className={cn(
|
||||
"flex gap-2 items-center group cursor-pointer transition-all hover:underline underline-offset-4 decoration-dashed decoration-nb-gray-600",
|
||||
!copied && "hover:opacity-90",
|
||||
className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -38,21 +28,17 @@ export default function CopyToClipboardText({
|
||||
|
||||
{copied ? (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"text-nb-gray-100 group-hover:opacity-100 shrink-0",
|
||||
iconAlignment === "left" ? "order-first" : "order-last",
|
||||
!alwaysShowIcon && "opacity-0",
|
||||
)}
|
||||
size={11}
|
||||
className={
|
||||
"text-nb-gray-100 opacity-0 group-hover:opacity-100 shrink-0"
|
||||
}
|
||||
size={12}
|
||||
/>
|
||||
) : (
|
||||
<CopyIcon
|
||||
className={cn(
|
||||
"text-nb-gray-100 group-hover:opacity-100 shrink-0",
|
||||
iconAlignment === "left" ? "order-first" : "order-last",
|
||||
!alwaysShowIcon && "opacity-0",
|
||||
)}
|
||||
size={11}
|
||||
className={
|
||||
"text-nb-gray-100 opacity-0 group-hover:opacity-100 shrink-0"
|
||||
}
|
||||
size={12}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const DropdownInfoText = ({ children, className }: Props) => {
|
||||
export const DropdownInfoText = ({ children }: Props) => {
|
||||
return (
|
||||
<div className={cn("text-center pt-2 mb-6 text-nb-gray-400", className)}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={"text-center pt-2 mb-6 text-nb-gray-400"}>{children}</div>
|
||||
);
|
||||
};
|
||||
@@ -2,51 +2,16 @@ import HelpText from "@components/HelpText";
|
||||
import { Label } from "@components/Label";
|
||||
import { ToggleSwitch } from "@components/ToggleSwitch";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import React from "react";
|
||||
|
||||
export const fancyToggleSwitchVariants = cva([], {
|
||||
variants: {
|
||||
variant: {
|
||||
default: ["px-6 py-4 border rounded-md"],
|
||||
blank: null,
|
||||
},
|
||||
state: {
|
||||
true: null,
|
||||
false: null,
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: "default",
|
||||
state: true,
|
||||
className: ["border-nb-gray-800 bg-nb-gray-900/70"],
|
||||
},
|
||||
{
|
||||
variant: "default",
|
||||
state: false,
|
||||
className: [
|
||||
"border-nb-gray-910 bg-nb-gray-900/30 hover:bg-nb-gray-900/40",
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export type FancyToggleSwitchVariants = VariantProps<
|
||||
typeof fancyToggleSwitchVariants
|
||||
>;
|
||||
|
||||
interface Props extends FancyToggleSwitchVariants {
|
||||
type Props = {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
helpText?: React.ReactNode;
|
||||
label?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
dataCy?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
};
|
||||
export default function FancyToggleSwitch({
|
||||
value,
|
||||
onChange,
|
||||
@@ -54,49 +19,28 @@ export default function FancyToggleSwitch({
|
||||
label,
|
||||
children,
|
||||
disabled = false,
|
||||
dataCy,
|
||||
className,
|
||||
variant = "default",
|
||||
}: Readonly<Props>) {
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
onChange(!value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (disabled) return;
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
handleToggle();
|
||||
}
|
||||
};
|
||||
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
onClick={handleToggle}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
role={"switch"}
|
||||
aria-checked={value}
|
||||
onClick={() => {
|
||||
if (disabled) return;
|
||||
onChange(!value);
|
||||
}}
|
||||
className={cn(
|
||||
"cursor-pointer transition-all duration-300 relative z-[1]",
|
||||
"inline-block text-left w-full",
|
||||
"px-5 py-3.5 border rounded-md cursor-pointer transition-all duration-300 relative z-[1]",
|
||||
value
|
||||
? "border-nb-gray-800 bg-nb-gray-900/70"
|
||||
: "border-nb-gray-800 bg-nb-gray-900/30 hover:bg-nb-gray-900/40",
|
||||
disabled && "opacity-50 pointer-events-none",
|
||||
fancyToggleSwitchVariants({ variant, state: value }),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={"flex justify-between gap-10"}>
|
||||
<div className={"flex justify-between gap-10 "}>
|
||||
<div className={"max-w-sm"}>
|
||||
<Label>{label}</Label>
|
||||
<HelpText margin={false}>{helpText}</HelpText>
|
||||
</div>
|
||||
<div className={"mt-2 pr-1"}>
|
||||
<ToggleSwitch
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
dataCy={dataCy}
|
||||
/>
|
||||
<div className={"mt-2"}>
|
||||
<ToggleSwitch checked={value} onCheckedChange={onChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div>{children && value ? children : null}</div>
|
||||
|
||||
@@ -22,8 +22,6 @@ type Props = {
|
||||
keepOpen?: boolean;
|
||||
customOpen?: boolean;
|
||||
customOnOpenChange?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
delayDuration?: number;
|
||||
skipDelayDuration?: number;
|
||||
} & TooltipProps;
|
||||
export default function FullTooltip({
|
||||
children,
|
||||
@@ -39,8 +37,6 @@ export default function FullTooltip({
|
||||
keepOpen = false,
|
||||
customOpen,
|
||||
customOnOpenChange,
|
||||
delayDuration = 1,
|
||||
skipDelayDuration = 300,
|
||||
}: Props) {
|
||||
const [open, setOpen] = useState(!!keepOpen);
|
||||
|
||||
@@ -50,13 +46,9 @@ export default function FullTooltip({
|
||||
};
|
||||
|
||||
return !disabled ? (
|
||||
<TooltipProvider
|
||||
disableHoverableContent={!interactive}
|
||||
delayDuration={delayDuration}
|
||||
skipDelayDuration={skipDelayDuration}
|
||||
>
|
||||
<TooltipProvider disableHoverableContent={!interactive}>
|
||||
<Tooltip
|
||||
delayDuration={delayDuration}
|
||||
delayDuration={1}
|
||||
open={customOpen || open}
|
||||
onOpenChange={customOnOpenChange || handleOpen}
|
||||
>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,26 +1,31 @@
|
||||
import { DropdownInfoText } from "@components/DropdownInfoText";
|
||||
import { DropdownInput } from "@components/DropdownInput";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { VirtualScrollAreaList } from "@components/VirtualScrollAreaList";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { useSearch } from "@hooks/useSearch";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { isRoutingPeerSupported } from "@utils/version";
|
||||
import { sortBy, unionBy } from "lodash";
|
||||
import { ArrowUpCircleIcon, ChevronsUpDown, MapPin } from "lucide-react";
|
||||
import { ChevronsUpDown, MapPin } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { FcLinux } from "react-icons/fc";
|
||||
import { useElementSize } from "@/hooks/useElementSize";
|
||||
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { OSLogo } from "@/modules/peers/PeerOSCell";
|
||||
|
||||
const MapPinIcon = memo(() => <MapPin size={12} />);
|
||||
MapPinIcon.displayName = "MapPinIcon";
|
||||
|
||||
const LinuxIcon = memo(() => (
|
||||
<span className={"grayscale brightness-[100%] contrast-[40%]"}>
|
||||
<FcLinux className={"text-white text-lg min-w-[20px] brightness-150"} />
|
||||
</span>
|
||||
));
|
||||
LinuxIcon.displayName = "LinuxIcon";
|
||||
|
||||
interface MultiSelectProps {
|
||||
value?: Peer;
|
||||
onChange: React.Dispatch<React.SetStateAction<Peer | undefined>>;
|
||||
@@ -58,6 +63,11 @@ export function PeerSelector({
|
||||
// Sort
|
||||
let options = sortBy([...peers], "name") as Peer[];
|
||||
|
||||
// Filter out peers that are not linux
|
||||
options = options.filter((peer) => {
|
||||
return getOperatingSystem(peer.os) === OperatingSystem.LINUX;
|
||||
});
|
||||
|
||||
// Filter out excluded peers
|
||||
if (excludedPeers) {
|
||||
options = options.filter((peer) => {
|
||||
@@ -118,7 +128,8 @@ export function PeerSelector({
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center gap-2.5 text-sm"}>
|
||||
<TextWithTooltip text={value.name} maxChars={22} />
|
||||
<LinuxIcon />
|
||||
<TextWithTooltip text={value.name} maxChars={20} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -140,7 +151,7 @@ export function PeerSelector({
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
hideWhenDetached={false}
|
||||
className="w-full p-0 shadow-sm shadow-nb-gray-950"
|
||||
className="w-full p-0 shadow-sm shadow-nb-gray-950"
|
||||
style={{
|
||||
width: width,
|
||||
}}
|
||||
@@ -158,7 +169,9 @@ export function PeerSelector({
|
||||
{unfilteredItems.length == 0 && !search && (
|
||||
<div className={"max-w-xs mx-auto"}>
|
||||
<DropdownInfoText>
|
||||
{"No peers available to select."}
|
||||
{
|
||||
"Seems like you don't have any Linux peers to assign as a routing peer."
|
||||
}
|
||||
</DropdownInfoText>
|
||||
</div>
|
||||
)}
|
||||
@@ -172,35 +185,10 @@ export function PeerSelector({
|
||||
{filteredItems.length > 0 && (
|
||||
<VirtualScrollAreaList
|
||||
items={filteredItems}
|
||||
onSelect={(item) => {
|
||||
const isSupported = isRoutingPeerSupported(
|
||||
item.version,
|
||||
item.os,
|
||||
);
|
||||
if (!isSupported) return;
|
||||
togglePeer(item);
|
||||
}}
|
||||
onSelect={togglePeer}
|
||||
renderItem={(option) => {
|
||||
const os = getOperatingSystem(option.os);
|
||||
const isSupported = isRoutingPeerSupported(
|
||||
option.version,
|
||||
option.os,
|
||||
);
|
||||
return (
|
||||
<FullTooltip
|
||||
disabled={isSupported}
|
||||
interactive={false}
|
||||
delayDuration={200}
|
||||
skipDelayDuration={350}
|
||||
className={"w-full flex items-center justify-between"}
|
||||
content={
|
||||
<div className={"max-w-[240px] text-xs"}>
|
||||
Please update NetBird to at least{" "}
|
||||
<span className={"text-netbird"}>v0.36.6</span> or later
|
||||
to use this peer as a routing peer.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 text-sm",
|
||||
@@ -209,35 +197,8 @@ export function PeerSelector({
|
||||
: "text-nb-gray-300",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center grayscale brightness-[100%] contrast-[40%]",
|
||||
"w-4 h-4 shrink-0",
|
||||
os === OperatingSystem.WINDOWS && "p-[2.5px]",
|
||||
os === OperatingSystem.APPLE && "p-[2.7px]",
|
||||
os === OperatingSystem.FREEBSD && "p-[1.5px]",
|
||||
!isSupported && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<OSLogo os={option.os} />
|
||||
</div>
|
||||
|
||||
<div className={cn(!isSupported && "opacity-50")}>
|
||||
<TextWithTooltip
|
||||
text={option.name}
|
||||
maxChars={22}
|
||||
hideTooltip={!isSupported}
|
||||
/>
|
||||
</div>
|
||||
{!isSupported && (
|
||||
<div className={"relative"}>
|
||||
<span className="animate-ping absolute left-0 inline-flex h-[14px] w-[14px] rounded-full bg-netbird opacity-20"></span>
|
||||
<ArrowUpCircleIcon
|
||||
size={14}
|
||||
className={"text-netbird"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<LinuxIcon />
|
||||
<TextWithTooltip text={option.name} maxChars={20} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -246,13 +207,12 @@ export function PeerSelector({
|
||||
value && value.id == option.id
|
||||
? "text-white"
|
||||
: "text-nb-gray-300",
|
||||
!isSupported && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<MapPinIcon />
|
||||
{option.ip}
|
||||
</div>
|
||||
</FullTooltip>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import * as RadioPrimitive from "@radix-ui/react-radio-group";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
type RadioVariants = VariantProps<typeof variants>;
|
||||
|
||||
const variants = cva([], {
|
||||
variants: {
|
||||
variant: {
|
||||
default: [
|
||||
"dark:data-[state=unchecked]:bg-nb-gray-950 dark:border-nb-gray-900 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300",
|
||||
"dark:data-[state=checked]:bg-netbird",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Radio = forwardRef<
|
||||
React.ElementRef<typeof RadioPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof RadioPrimitive.Root> & RadioVariants
|
||||
>(
|
||||
(
|
||||
{ className, children, variant = "default", defaultValue, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<RadioPrimitive.Root
|
||||
ref={ref}
|
||||
defaultValue={defaultValue}
|
||||
name={props.name}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</RadioPrimitive.Root>
|
||||
),
|
||||
);
|
||||
Radio.displayName = RadioPrimitive.Root.displayName;
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
className?: string;
|
||||
} & RadioVariants;
|
||||
|
||||
const RadioItem = ({ value, className, variant = "default" }: Props) => {
|
||||
return (
|
||||
<RadioPrimitive.Item
|
||||
value={value}
|
||||
className={cn(
|
||||
variants({ variant }),
|
||||
"border-neutral-900",
|
||||
"peer h-5 w-5 shrink-0 rounded-full border",
|
||||
"ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50 relative",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<RadioPrimitive.Indicator asChild={true}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-2 w-2 bg-netbird absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center justify-center rounded-full",
|
||||
"data-[state=checked]:bg-white data-[state=checked]:text-neutral-50 ",
|
||||
)}
|
||||
></div>
|
||||
</RadioPrimitive.Indicator>
|
||||
</RadioPrimitive.Item>
|
||||
);
|
||||
};
|
||||
RadioItem.displayName = RadioPrimitive.Item.displayName;
|
||||
|
||||
export { Radio, RadioItem };
|
||||
@@ -1,3 +1,7 @@
|
||||
export default function Separator() {
|
||||
return <span className={"h-[1px] w-full bg-zinc-700/40 block"}></span>;
|
||||
return (
|
||||
<span
|
||||
className={"h-[1px] w-full dark:bg-nb-gray-900 bg-nb-gray-100 block"}
|
||||
></span>
|
||||
);
|
||||
}
|
||||
@@ -36,36 +36,29 @@ const switchVariants = cva("", {
|
||||
|
||||
const ToggleSwitch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
|
||||
SwitchVariants & { dataCy?: string }
|
||||
>(
|
||||
(
|
||||
{ className, size = "default", variant = "default", dataCy, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SwitchPrimitives.Root
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & SwitchVariants
|
||||
>(({ className, size = "default", variant = "default", ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950 ",
|
||||
className,
|
||||
switchVariants({ size, variant }),
|
||||
)}
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick?.(e);
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-neutral-300 dark:focus-visible:ring-offset-neutral-950 ",
|
||||
className,
|
||||
switchVariants({ size, variant }),
|
||||
switchVariants({ "thumb-size": size }),
|
||||
"pointer-events-none block rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 dark:bg-white",
|
||||
)}
|
||||
{...props}
|
||||
data-cy={dataCy}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick?.(e);
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
switchVariants({ "thumb-size": size }),
|
||||
"pointer-events-none block rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 dark:bg-white",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
),
|
||||
);
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
ToggleSwitch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { ToggleSwitch };
|
||||
@@ -10,9 +10,6 @@ const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
export const tooltipClasses =
|
||||
"z-[9999] overflow-hidden rounded-md border border-neutral-200 bg-white text-sm text-neutral-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-nb-gray-930 dark:bg-nb-gray-940 dark:text-neutral-50";
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
@@ -22,7 +19,10 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
asChild={true}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(tooltipClasses, className)}
|
||||
className={cn(
|
||||
"z-[9999] overflow-hidden rounded-md border border-neutral-200 bg-white text-sm text-neutral-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-nb-gray-930 dark:bg-nb-gray-940 dark:text-neutral-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div>{props.children}</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ function List({ children }: { children: React.ReactNode }) {
|
||||
<Tabs.List
|
||||
className={cn(
|
||||
"px-4 py-4 whitespace-nowrap overflow-y-hidden shrink-0 no-scrollbar",
|
||||
"lg:h-full items-start bg-nb-gray border-b-0 border-nb-gray-930",
|
||||
"lg:h-full items-start bg-nb-gray border-b border-nb-gray-930",
|
||||
"flex lg:flex-col lg:gap-1",
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -11,15 +11,13 @@ type Props<T extends { id?: string }> = {
|
||||
items: T[];
|
||||
onSelect: (item: T) => void;
|
||||
renderItem?: (item: T) => React.ReactNode;
|
||||
itemClassName?: string;
|
||||
};
|
||||
|
||||
export function VirtualScrollAreaList<T extends { id?: string }>({
|
||||
items,
|
||||
onSelect,
|
||||
renderItem,
|
||||
itemClassName,
|
||||
}: Readonly<Props<T>>) {
|
||||
}: Props<T>) {
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
const [selected, setSelected] = useState(0);
|
||||
|
||||
@@ -83,9 +81,8 @@ export function VirtualScrollAreaList<T extends { id?: string }>({
|
||||
<VirtualScrollListItemWrapper
|
||||
onMouseEnter={() => setSelected(index)}
|
||||
id={option.id}
|
||||
onClick={() => onClick(option)}
|
||||
onClick={() => onClick(option as T)}
|
||||
ariaSelected={selected === index}
|
||||
className={itemClassName}
|
||||
>
|
||||
{renderMemoizedItem ? renderMemoizedItem(option) : option.id}
|
||||
</VirtualScrollListItemWrapper>
|
||||
@@ -106,18 +103,10 @@ type ItemWrapperProps = {
|
||||
onMouseEnter?: () => void;
|
||||
onClick?: () => void;
|
||||
ariaSelected?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const VirtualScrollListItemWrapper = memo(
|
||||
({
|
||||
id,
|
||||
children,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
ariaSelected,
|
||||
className,
|
||||
}: ItemWrapperProps) => {
|
||||
({ id, children, onClick, onMouseEnter, ariaSelected }: ItemWrapperProps) => {
|
||||
return (
|
||||
<div
|
||||
key={id ?? undefined}
|
||||
@@ -129,7 +118,6 @@ export const VirtualScrollListItemWrapper = memo(
|
||||
className={cn(
|
||||
"text-xs flex justify-between py-2 px-3 cursor-pointer items-center rounded-md",
|
||||
"bg-transparent dark:aria-selected:bg-nb-gray-800/50",
|
||||
className,
|
||||
)}
|
||||
aria-selected={ariaSelected}
|
||||
role={"listitem"}
|
||||
|
||||
Loaded 20 of 78 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user