mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e2e38764e |
No files matched your search
@@ -26,6 +26,7 @@ The dashboard makes it possible to:
|
||||
- NextJS
|
||||
- ReactJS
|
||||
- Tailwind CSS
|
||||
- [React Flow](https://reactflow.dev/) for the Control Center
|
||||
- Auth0
|
||||
- Nginx
|
||||
- Docker
|
||||
|
||||
@@ -7,6 +7,10 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
default_type application/wasm;
|
||||
}
|
||||
location = /ironrdp-pkg/ironrdp_web_bg.wasm {
|
||||
root /usr/share/nginx/html;
|
||||
default_type application/wasm;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri.html $uri/ =404;
|
||||
|
||||
Generated
+815
-9
File diff suppressed because it is too large.
Load diff
+6
-1
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@axa-fr/react-oidc": "^7.22.18",
|
||||
"@dagrejs/dagre": "^1.1.5",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
@@ -35,6 +36,7 @@
|
||||
"@tanstack/match-sorter-utils": "^8.8.4",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/react": "^18",
|
||||
@@ -42,6 +44,7 @@
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@xyflow/react": "^12.8.4",
|
||||
"autoprefixer": "^10",
|
||||
"chart.js": "^4.4.8",
|
||||
"chroma-js": "^3.1.2",
|
||||
@@ -49,8 +52,10 @@
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"elkjs": "^0.10.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
@@ -60,7 +65,7 @@
|
||||
"ip-cidr": "^3.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.481.0",
|
||||
"lucide-react": "^0.539.0",
|
||||
"next": "^14.2.28",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Control Center - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
File diff suppressed because it is too large.
Load diff
+7
-1
@@ -167,4 +167,10 @@ p {
|
||||
|
||||
.xterm-viewport {
|
||||
@apply m-0 p-0 box-border;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Control Center */
|
||||
.react-flow__node-groupNode .selected{
|
||||
@apply border-netbird;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function ControlCenterIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path d="M5 3a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5Zm0 12a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2H5Zm12 0a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-2Zm0-12a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-2Z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 6.5a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1ZM10 18a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm-4-4a1 1 0 0 1-1-1v-2a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1Zm12 0a1 1 0 0 1-1-1v-2a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import { SmallBadge } from "@components/ui/SmallBadge";
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import ActivityIcon from "@/assets/icons/ActivityIcon";
|
||||
import ControlCenterIcon from "@/assets/icons/ControlCenterIcon";
|
||||
import DNSIcon from "@/assets/icons/DNSIcon";
|
||||
import DocsIcon from "@/assets/icons/DocsIcon";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
@@ -67,6 +70,23 @@ export default function Navigation({
|
||||
>
|
||||
<div>
|
||||
<SidebarItemGroup>
|
||||
<SidebarItem
|
||||
icon={<ControlCenterIcon size={16} />}
|
||||
label={
|
||||
<div className={"flex items-center gap-2"}>
|
||||
Control Center
|
||||
<SmallBadge
|
||||
text={"Beta"}
|
||||
variant={"sky"}
|
||||
className={"text-[8px] leading-none py-[3px] px-[5px]"}
|
||||
textClassName={"top-0"}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
href={"/control-center"}
|
||||
visible={permission.policies.read}
|
||||
/>
|
||||
|
||||
<SidebarItem
|
||||
icon={<PeerIcon />}
|
||||
label="Peers"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { SegmentedTabs } from "@components/SegmentedTabs";
|
||||
import { FolderGit2, MonitorSmartphoneIcon, NetworkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
export enum FlowView {
|
||||
NETWORKS = "networks",
|
||||
GROUPS = "groups",
|
||||
PEERS = "peers",
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value?: FlowView;
|
||||
onChange?: (value: FlowView) => void;
|
||||
};
|
||||
|
||||
export const FlowSelector = ({ value, onChange }: Props) => {
|
||||
return (
|
||||
<SegmentedTabs value={value} onChange={(v) => onChange?.(v as FlowView)}>
|
||||
<SegmentedTabs.List
|
||||
className={
|
||||
"border-b rounded-b-lg text-sm font-medium bg-nb-gray-930 p-1"
|
||||
}
|
||||
>
|
||||
<SegmentedTabs.Trigger
|
||||
value={FlowView.PEERS}
|
||||
className={"text-xs px-3 py-1"}
|
||||
>
|
||||
<MonitorSmartphoneIcon size={12} />
|
||||
Peers
|
||||
</SegmentedTabs.Trigger>
|
||||
<SegmentedTabs.Trigger
|
||||
value={FlowView.GROUPS}
|
||||
className={"text-xs px-3 py-1"}
|
||||
>
|
||||
<FolderGit2 size={12} />
|
||||
Groups
|
||||
</SegmentedTabs.Trigger>
|
||||
<SegmentedTabs.Trigger
|
||||
value={FlowView.NETWORKS}
|
||||
className={"text-xs px-3 py-[0.45rem]"}
|
||||
>
|
||||
<NetworkIcon size={12} />
|
||||
Networks
|
||||
</SegmentedTabs.Trigger>
|
||||
</SegmentedTabs.List>
|
||||
</SegmentedTabs>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import Button from "@components/Button";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import CircleIcon from "@/assets/icons/CircleIcon";
|
||||
import { Network, NetworkRouter } from "@/interfaces/Network";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
|
||||
type Props = {
|
||||
network: Network;
|
||||
};
|
||||
|
||||
export const NetworkRoutingPeerCount = ({ network }: Props) => {
|
||||
const { data: routers, isLoading: isRoutersLoading } =
|
||||
useFetchApi<NetworkRouter[]>("/networks/routers");
|
||||
const { data: peers, isLoading: isPeersLoading } =
|
||||
useFetchApi<Peer[]>("/peers");
|
||||
|
||||
const routingPeerStatusColor = useMemo(() => {
|
||||
if (!network) return "bg-nb-gray-500";
|
||||
const routerCount = network.routers?.length || 0;
|
||||
if (routerCount === 0) return "bg-nb-gray-500";
|
||||
if (routerCount === 1) return "bg-yellow-400";
|
||||
if (routerCount > 1) return "bg-green-400";
|
||||
return "bg-nb-gray-500";
|
||||
}, [network]);
|
||||
|
||||
const networkRouters = useMemo(() => {
|
||||
if (!network || !peers) return [];
|
||||
const routerIds = network?.routers?.map((r) => r) || [];
|
||||
return routers?.filter((r) => routerIds.includes(r.id)) || [];
|
||||
}, [network, peers, routers]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xs"}
|
||||
className={"!bg-nb-gray-930 !text-nb-gray-300 cursor-default"}
|
||||
>
|
||||
<CircleIcon
|
||||
size={8}
|
||||
className={cn("shrink-0 block", routingPeerStatusColor)}
|
||||
/>
|
||||
{network.routers?.length || 0} Routing Peer(s)
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Edge, useInternalNode } from "@xyflow/react";
|
||||
import React from "react";
|
||||
import { getEdgeParams } from "@/modules/control-center/utils/edge-helper";
|
||||
|
||||
type AnimatedLineProps = Edge<
|
||||
{
|
||||
label?: string;
|
||||
color?: string;
|
||||
},
|
||||
"animated-line"
|
||||
>;
|
||||
|
||||
function AnimatedLine({ id, source, target, data }: AnimatedLineProps) {
|
||||
const sourceNode = useInternalNode(source);
|
||||
const targetNode = useInternalNode(target);
|
||||
if (!sourceNode || !targetNode) return null;
|
||||
|
||||
const { sx, sy, tx, ty } = getEdgeParams(sourceNode, targetNode);
|
||||
|
||||
const labelX = (sx + tx) / 2;
|
||||
const labelY = (sy + ty) / 2;
|
||||
|
||||
let angle = Math.atan2(ty - sy, tx - sx) * (180 / Math.PI);
|
||||
if (angle < -90 || angle > 90) {
|
||||
angle += 180;
|
||||
}
|
||||
|
||||
const label = data?.label || "";
|
||||
const hasLabel = label?.length > 0;
|
||||
const fontSize = 12;
|
||||
const paddingX = hasLabel ? 2 : 0;
|
||||
const paddingY = hasLabel ? 2 : 0;
|
||||
|
||||
const gapWidth = hasLabel ? 4 : 0;
|
||||
const labelTextWidth = label.length * 7;
|
||||
|
||||
const labelWidth = gapWidth + labelTextWidth + paddingX * 2;
|
||||
const labelHeight = fontSize + paddingY * 2;
|
||||
|
||||
const dx = tx - sx;
|
||||
const dy = ty - sy;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
const gap = labelWidth / 2;
|
||||
const nx = dx / length;
|
||||
const ny = dy / length;
|
||||
|
||||
const preLabelX = labelX - nx * gap;
|
||||
const preLabelY = labelY - ny * gap;
|
||||
|
||||
const postLabelX = labelX + nx * gap;
|
||||
const postLabelY = labelY + ny * gap;
|
||||
|
||||
const color = data?.color || "#0e9f6e";
|
||||
|
||||
return (
|
||||
<>
|
||||
<line
|
||||
x1={sx}
|
||||
y1={sy}
|
||||
x2={preLabelX}
|
||||
y2={preLabelY}
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5, 5"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="20"
|
||||
to="0"
|
||||
dur="0.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</line>
|
||||
<line
|
||||
x1={postLabelX}
|
||||
y1={postLabelY}
|
||||
x2={tx}
|
||||
y2={ty}
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5, 5"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="20"
|
||||
to="0"
|
||||
dur="0.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</line>
|
||||
{label && hasLabel && (
|
||||
<foreignObject
|
||||
x={labelX - labelWidth / 2}
|
||||
y={labelY - labelHeight / 2}
|
||||
width={labelWidth}
|
||||
height={labelHeight}
|
||||
style={{ overflow: "visible" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: labelWidth,
|
||||
height: labelHeight,
|
||||
fontSize,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: `${paddingY}px ${paddingX}px`,
|
||||
transform: `rotate(${angle}deg)`,
|
||||
transformOrigin: "center center",
|
||||
boxSizing: "border-box",
|
||||
background: "none",
|
||||
}}
|
||||
className={
|
||||
"flex items-center justify-center gap-1 select-none pointer-events-none z-10 text-green-50"
|
||||
}
|
||||
>
|
||||
<div className={"whitespace-nowrap"}>{label}</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnimatedLine;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { BaseEdge, type EdgeProps, getSmoothStepPath } from "@xyflow/react";
|
||||
import React from "react";
|
||||
|
||||
export function BidirectionalEdges({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
}: EdgeProps) {
|
||||
const [forwardPath] = getSmoothStepPath({
|
||||
sourceX: sourceX - 5,
|
||||
sourceY: sourceY - 5,
|
||||
sourcePosition,
|
||||
targetX: targetX + 15,
|
||||
targetY: targetY - 5,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
const [backwardPath] = getSmoothStepPath({
|
||||
sourceX: targetX + 5,
|
||||
sourceY: targetY + 5,
|
||||
sourcePosition: targetPosition,
|
||||
targetX: sourceX - 15,
|
||||
targetY: sourceY + 5,
|
||||
targetPosition: sourcePosition,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
id={`${id}-forward`}
|
||||
path={forwardPath}
|
||||
style={{
|
||||
strokeWidth: 2,
|
||||
stroke: "#0e9f6e",
|
||||
strokeDasharray: "5, 5",
|
||||
}}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="20"
|
||||
to="0"
|
||||
dur="0.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</BaseEdge>
|
||||
|
||||
<BaseEdge
|
||||
id={`${id}-backward`}
|
||||
path={backwardPath}
|
||||
style={{
|
||||
strokeWidth: 2,
|
||||
stroke: "#0e9f6e",
|
||||
strokeDasharray: "5, 5",
|
||||
}}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="20"
|
||||
to="0"
|
||||
dur="0.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</BaseEdge>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
BaseEdge,
|
||||
type EdgeProps,
|
||||
getSimpleBezierPath,
|
||||
getSmoothStepPath,
|
||||
getStraightPath,
|
||||
} from "@xyflow/react";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
data: {
|
||||
enabled: boolean;
|
||||
type: "smoothstep" | "straight" | "bezier";
|
||||
};
|
||||
} & EdgeProps;
|
||||
|
||||
export function DirectionIn({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
data,
|
||||
}: Props) {
|
||||
const { enabled, type = "straight" } = data;
|
||||
|
||||
const getPath = () => {
|
||||
switch (type) {
|
||||
case "straight":
|
||||
return getStraightPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
case "bezier":
|
||||
return getSimpleBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
case "smoothstep":
|
||||
return getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
default:
|
||||
return getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [edgePath] = getPath();
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
style={{
|
||||
opacity: enabled ? 1 : 0.6,
|
||||
strokeWidth: 2,
|
||||
stroke: enabled ? "#0e9f6e" : "#787878",
|
||||
strokeDasharray: "5, 5",
|
||||
}}
|
||||
>
|
||||
{enabled && (
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="20"
|
||||
to="0"
|
||||
dur="0.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
)}
|
||||
</BaseEdge>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeProps,
|
||||
getBezierPath,
|
||||
useInternalNode,
|
||||
} from "@xyflow/react";
|
||||
import React from "react";
|
||||
import { getEdgeParams } from "@/modules/control-center/utils/edge-helper";
|
||||
|
||||
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
|
||||
const sourceNode = useInternalNode(source);
|
||||
const targetNode = useInternalNode(target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
|
||||
sourceNode,
|
||||
targetNode,
|
||||
);
|
||||
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX: sx,
|
||||
sourceY: sy,
|
||||
sourcePosition: sourcePos,
|
||||
targetPosition: targetPos,
|
||||
targetX: tx,
|
||||
targetY: ty,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
style={{
|
||||
strokeWidth: 2,
|
||||
stroke: "#0e9f6e",
|
||||
strokeDasharray: "5, 5",
|
||||
}}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="20"
|
||||
to="0"
|
||||
dur="0.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</BaseEdge>
|
||||
);
|
||||
}
|
||||
|
||||
export default FloatingEdge;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { BaseEdge, type EdgeProps, getSimpleBezierPath } from "@xyflow/react";
|
||||
import React from "react";
|
||||
import { useSourceGroupEnabled } from "@/modules/control-center/utils/helpers";
|
||||
|
||||
type Props = {
|
||||
data: {
|
||||
enabled: boolean;
|
||||
};
|
||||
} & EdgeProps;
|
||||
|
||||
export function SimpleConnection({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
data,
|
||||
source,
|
||||
}: Props) {
|
||||
const [edgePath] = getSimpleBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
const enabled = useSourceGroupEnabled(source);
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
id={id}
|
||||
path={edgePath}
|
||||
style={{
|
||||
strokeWidth: 1.5,
|
||||
stroke: "#595959",
|
||||
strokeDasharray: "0, 0",
|
||||
opacity: enabled ? 1 : 0.6,
|
||||
}}
|
||||
></BaseEdge>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import TruncatedText from "@components/ui/TruncatedText";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { GlobeIcon, NetworkIcon, WorkflowIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { NetworkResource } from "@/interfaces/Network";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
import { OSLogo } from "@/modules/peers/PeerOSCell";
|
||||
|
||||
type DeviceCardProps = {
|
||||
device?: Peer;
|
||||
resource?: NetworkResource;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const DeviceCard = ({
|
||||
device,
|
||||
resource,
|
||||
className,
|
||||
}: DeviceCardProps) => {
|
||||
if (!device && !resource) return;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex shrink-0 items-center gap-2.5 text-nb-gray-300 text-left py-1 pl-3 pr-4 rounded-md group/machine my-0 w-[200px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-md h-9 w-9 shrink-0 bg-nb-gray-850 transition-all",
|
||||
"group-hover:bg-nb-gray-800 relative",
|
||||
)}
|
||||
>
|
||||
{device && <PeerOSIcon os={device.os} />}
|
||||
{resource?.type && <ResourceIcon type={resource.type} />}
|
||||
|
||||
{device?.country_code && (
|
||||
<div className={"absolute -bottom-[4px] -right-[4px]"}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-full border-[3px] shrink-0",
|
||||
"border-nb-gray-940",
|
||||
)}
|
||||
>
|
||||
<RoundedFlag country={device?.country_code} size={10} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex flex-col gap-0 justify-center mt-2 leading-tight"}>
|
||||
<span
|
||||
className={
|
||||
"mb-1.5 font-normal text-[0.85rem] text-nb-gray-100 flex items-center gap-2"
|
||||
}
|
||||
>
|
||||
<TruncatedText
|
||||
text={device?.name || resource?.name || "Unknown"}
|
||||
maxWidth={"150px"}
|
||||
hideTooltip={true}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
"text-sm font-normal text-nb-gray-400 -top-[0.3rem] relative"
|
||||
}
|
||||
>
|
||||
{device?.ip || resource?.address}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PeerOSIcon = ({ os }: { os: string }) => {
|
||||
const osType = getOperatingSystem(os);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center grayscale brightness-[100%] contrast-[40%]",
|
||||
"w-4 h-4 shrink-0",
|
||||
osType === OperatingSystem.WINDOWS && "p-[2.5px]",
|
||||
osType === OperatingSystem.APPLE && "p-[2.7px]",
|
||||
osType === OperatingSystem.FREEBSD && "p-[1.5px]",
|
||||
)}
|
||||
>
|
||||
<OSLogo os={os} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ResourceIcon = ({
|
||||
type,
|
||||
size = 15,
|
||||
}: {
|
||||
type: "domain" | "host" | "subnet";
|
||||
size?: number;
|
||||
}) => {
|
||||
switch (type) {
|
||||
case "domain":
|
||||
return <GlobeIcon size={size} />;
|
||||
case "subnet":
|
||||
return <NetworkIcon size={size} />;
|
||||
case "host":
|
||||
return <WorkflowIcon size={size} />;
|
||||
default:
|
||||
return <WorkflowIcon size={size} />;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { GroupBadgeIcon } from "@components/ui/GroupBadgeIcon";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Handle, type Node, Position } from "@xyflow/react";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
|
||||
type GroupNodeProps = Node<
|
||||
{
|
||||
group: Group;
|
||||
enabled: boolean;
|
||||
hoverable?: boolean;
|
||||
onClick?: (g: Group) => void;
|
||||
},
|
||||
"groupNode"
|
||||
>;
|
||||
|
||||
export const GroupNode = ({ data, id }: GroupNodeProps) => {
|
||||
const { enabled = true, group, hoverable = true, onClick } = data;
|
||||
|
||||
const countLabel = useMemo(() => {
|
||||
const peerCount = group?.peers_count || 0;
|
||||
const resourceCount = group?.resources_count || 0;
|
||||
if (resourceCount === 0) {
|
||||
return `${peerCount} Peer(s)`;
|
||||
}
|
||||
if (peerCount === 0) {
|
||||
return `${resourceCount} Resource(s)`;
|
||||
}
|
||||
return `${peerCount} Peer(s), ${resourceCount} Resource(s)`;
|
||||
}, [group?.peers_count, group?.resources_count]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"cc-group-node bg-nb-gray-940 border border-nb-gray-800 rounded-lg overflow-hidden transition-all",
|
||||
!enabled && "opacity-60",
|
||||
hoverable && "hover:bg-nb-gray-930 cursor-pointer",
|
||||
)}
|
||||
onClick={() => onClick?.(group)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"flex w-full items-center justify-between text-nb-gray-300 gap-2 text-sm pl-3 pr-5 py-3 font-normal"
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center gap-3 font-normal text-sm"}>
|
||||
<div
|
||||
className={
|
||||
"h-9 w-9 bg-nb-gray-850 rounded-md flex items-center justify-center shrink-0"
|
||||
}
|
||||
>
|
||||
<GroupBadgeIcon id={group?.id} issued={group?.issued} size={14} />
|
||||
</div>
|
||||
<div>
|
||||
<div className={" text-nb-gray-200 font-normal whitespace-nowrap"}>
|
||||
{group.name}
|
||||
</div>
|
||||
<div className={"text-nb-gray-400 whitespace-nowrap text-xs"}>
|
||||
{countLabel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={"sr"}
|
||||
className={"opacity-0"}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={"tl"}
|
||||
className={"opacity-0"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Handle, type Node, Position } from "@xyflow/react";
|
||||
import { NetworkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import CircleIcon from "@/assets/icons/CircleIcon";
|
||||
import { DeviceCard } from "@/modules/control-center/nodes/DeviceCard";
|
||||
import { Network, NetworkResource } from "@/interfaces/Network";
|
||||
|
||||
type NetworkNodeType = {
|
||||
network: Network;
|
||||
};
|
||||
|
||||
type NetworkNodeProps = Node<NetworkNodeType, "networkNode">;
|
||||
|
||||
export const NetworkNode = ({ data }: NetworkNodeProps) => {
|
||||
const { data: networkResources, isLoading: isLoadingResources } = useFetchApi<
|
||||
NetworkResource[]
|
||||
>("/networks/resources");
|
||||
|
||||
const n = data.network as Network;
|
||||
const resourceIds = n?.resources || [];
|
||||
const routingPeers = n?.routers || [];
|
||||
const resources =
|
||||
networkResources?.filter((r) => resourceIds.includes(r?.id || "")) || [];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-nb-gray-940 border border-nb-gray-800 rounded-2xl overflow-hidden group hover:bg-nb-gray-935 transition-all cursor-pointer",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between text-nb-gray-300 gap-2 text-sm pl-6 pr-6 py-3.5 font-normal bg-nb-gray-935 border-b border-nb-gray-800 group-hover:bg-nb-gray-930 transition-all",
|
||||
resources?.length === 0 && "border-b-0",
|
||||
)}
|
||||
>
|
||||
<div className={"flex items-center gap-3 font-normal text-sm"}>
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
" text-nb-gray-100 font-medium whitespace-nowrap flex items-center gap-2"
|
||||
}
|
||||
>
|
||||
<NetworkIcon size={12} />
|
||||
{n?.name}
|
||||
</div>
|
||||
<div className={"text-nb-gray-400 whitespace-nowrap mt-0.5"}>
|
||||
{resources?.length || 0} Resources
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex items-center gap-2 text-xs"}>
|
||||
<CircleIcon
|
||||
size={8}
|
||||
className={cn(
|
||||
"shrink-0 block",
|
||||
routingPeers?.length === 0 && "bg-nb-gray-500",
|
||||
routingPeers?.length === 1 && "bg-yellow-400",
|
||||
routingPeers?.length > 1 && "bg-green-400",
|
||||
)}
|
||||
/>
|
||||
{routingPeers?.length || 0} Routing Peer(s)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{resources && resources.length > 0 && (
|
||||
<div className={"p-2 flex flex-col gap-4 relative"}>
|
||||
<div className={"grid grid-cols-2 relative z-0"}>
|
||||
{resources?.slice(0, 6).map((r) => {
|
||||
return <DeviceCard resource={r} key={r.id} />;
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute w-full h-full bg-gradient-to-b from-transparent via-nb-gray-940/20 to-nb-gray-940 z-10 left-0 top-0 pointer-events-none",
|
||||
resources?.length > 6 ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={"sr"}
|
||||
style={{
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={"tl"}
|
||||
style={{
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Handle, type Node, Position } from "@xyflow/react";
|
||||
import * as React from "react";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
import { DeviceCard } from "@/modules/control-center/nodes/DeviceCard";
|
||||
import { useAnySourceGroupEnabled } from "@/modules/control-center/utils/helpers";
|
||||
|
||||
type PeerNodeProps = Node<
|
||||
{
|
||||
peer: Peer;
|
||||
enabled?: boolean;
|
||||
},
|
||||
"peerNode"
|
||||
>;
|
||||
|
||||
export const PeerNode = ({ data, id }: PeerNodeProps) => {
|
||||
const { peer, enabled } = data;
|
||||
const isEnabled = useAnySourceGroupEnabled(id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"border-0 border-nb-gray-800 rounded-lg overflow-hidden transition-all"
|
||||
}
|
||||
>
|
||||
<DeviceCard
|
||||
device={peer}
|
||||
className={cn("p-0", !isEnabled && "opacity-60")}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={"sr"}
|
||||
className={"opacity-0"}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={"tl"}
|
||||
className={"opacity-0"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loaded 20 of 33 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user