mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b786c3395a | ||
|
|
3c009ddc0d |
No files matched your search
@@ -1,7 +1,6 @@
|
||||
# Ignore zitadel environment
|
||||
.env
|
||||
Caddyfile
|
||||
agentgateway-stub.Caddyfile
|
||||
management.json
|
||||
turnserver.conf
|
||||
zitadel.env
|
||||
|
||||
@@ -19,7 +19,7 @@ check_docker_compose() {
|
||||
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
|
||||
|
||||
$DOCKER_COMPOSE_COMMAND down --volumes
|
||||
rm -f docker-compose.yml Caddyfile agentgateway-stub.Caddyfile zitadel.env .env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json proxy.env proxy-no-ports.env
|
||||
rm -f docker-compose.yml Caddyfile zitadel.env .env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json proxy.env proxy-no-ports.env
|
||||
rm -rf proxy-certs proxy-certs-no-ports
|
||||
rm -f ../../.test-config.json ../playwright.env.json
|
||||
rm -f ../fixtures/auth/owner.json ../fixtures/auth/user.json
|
||||
@@ -532,7 +532,7 @@ initEnvironment() {
|
||||
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
|
||||
echo "You can use the following commands:"
|
||||
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
|
||||
echo " rm -f docker-compose.yml Caddyfile agentgateway-stub.Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json proxy.env proxy-no-ports.env && rm -rf proxy-certs proxy-certs-no-ports"
|
||||
echo " rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json proxy.env proxy-no-ports.env && rm -rf proxy-certs proxy-certs-no-ports"
|
||||
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
|
||||
exit 1
|
||||
fi
|
||||
@@ -540,7 +540,6 @@ initEnvironment() {
|
||||
echo Rendering initial files...
|
||||
renderDockerCompose > docker-compose.yml
|
||||
renderCaddyfile > Caddyfile
|
||||
renderAgentGatewayStub > agentgateway-stub.Caddyfile
|
||||
renderZitadelEnv > zitadel.env
|
||||
echo "" > turnserver.conf
|
||||
echo "" > management.json
|
||||
@@ -613,30 +612,6 @@ initEnvironment() {
|
||||
echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env
|
||||
}
|
||||
|
||||
# Saving an Agent Network provider makes management call the upstream and
|
||||
# refuses the save unless it answers a model listing, so a provider spec needs
|
||||
# a reachable OpenAI-compatible endpoint. agentgateway is self-hosted and has
|
||||
# no public one; this stub stands in for it, inside the compose network where
|
||||
# management can reach it and without depending on a vendor being up.
|
||||
renderAgentGatewayStub() {
|
||||
cat <<'EOF'
|
||||
{
|
||||
admin off
|
||||
auto_https off
|
||||
}
|
||||
|
||||
:8088 {
|
||||
handle /v1/models {
|
||||
header Content-Type application/json
|
||||
respond `{"object":"list","data":[{"id":"agentgateway-e2e-model","object":"model","owned_by":"netbird-e2e"}]}` 200
|
||||
}
|
||||
handle {
|
||||
respond 404
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
renderCaddyfile() {
|
||||
cat <<EOF
|
||||
{
|
||||
@@ -843,15 +818,6 @@ services:
|
||||
volumes:
|
||||
- netbird_caddy_data:/data
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
# Stand-in upstream for the Agent Network provider specs: answers the model
|
||||
# listing management probes before it accepts a provider save. Reachable
|
||||
# only from inside the compose network, at http://agentgateway-stub:8088.
|
||||
agentgateway-stub:
|
||||
image: caddy
|
||||
restart: unless-stopped
|
||||
networks: [ netbird ]
|
||||
volumes:
|
||||
- ./agentgateway-stub.Caddyfile:/etc/caddy/Caddyfile
|
||||
# Management
|
||||
management:
|
||||
image: ghcr.io/netbirdio/management-cloud:${MANAGEMENT_IMAGE_TAG}
|
||||
|
||||
@@ -24,31 +24,6 @@ export async function openControlCenter(page: Page, tab?: FlowView) {
|
||||
await dismissBlockingOverlays(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the groups view already showing `groupName`.
|
||||
*
|
||||
* The view picks its own group on first render (getFirstGroup: the highest
|
||||
* peers_count that is a policy source), and that ranking runs over every group
|
||||
* in the account — including ones a spec on the other worker seeded moments
|
||||
* ago. Deleting the spec's own fixtures first is not enough to pin it, so
|
||||
* select the group by name and let the view rebuild around it.
|
||||
*/
|
||||
export async function openGroupView(page: Page, groupName: string) {
|
||||
await openControlCenter(page, "groups");
|
||||
const selector = canvasNode(page, "select-group-node");
|
||||
await expect(selector).toBeVisible();
|
||||
// Unforced on purpose: the opening fitView animates the canvas, and the
|
||||
// actionability check is what waits for the node to stop moving.
|
||||
await selector.click();
|
||||
await page.getByTestId("select-dropdown-search").fill(groupName);
|
||||
await page
|
||||
.locator('[role="option"]')
|
||||
.filter({ hasText: groupName })
|
||||
.first()
|
||||
.click({ force: true });
|
||||
await expect(selector).toContainText(groupName);
|
||||
}
|
||||
|
||||
export async function switchFlowView(page: Page, view: FlowView) {
|
||||
await dismissBlockingOverlays(page);
|
||||
await page.getByTestId(`cc-flow-${view}`).click({ force: true });
|
||||
|
||||
@@ -59,12 +59,7 @@ test.describe
|
||||
.getByTestId("agent-network-provider-option-agentgateway")
|
||||
.click({ force: true });
|
||||
|
||||
// Saving a provider makes management call the upstream, and it refuses
|
||||
// the save with a 422 unless the upstream answers a model listing — so a
|
||||
// made-up host cannot be used here. agentgateway is self-hosted and has
|
||||
// no public endpoint to point at, so create-test-env.sh runs a stub that
|
||||
// answers that probe inside the compose network.
|
||||
const upstreamURL = "http://agentgateway-stub:8088";
|
||||
const upstreamURL = "https://agentgateway.e2e.example";
|
||||
await page
|
||||
.getByTestId("agent-network-provider-upstream-url")
|
||||
.fill(upstreamURL);
|
||||
|
||||
@@ -364,17 +364,13 @@ test.describe.serial("Control Center Draft Matrix @control-center", () => {
|
||||
}) => {
|
||||
const group = await place(page, "group", 0.6, 0.5);
|
||||
await expectChangeCount(page, 1);
|
||||
// React Flow ignores delete keys while an input is focused, and the panel's
|
||||
// search takes focus on click, so blur it before pressing Backspace.
|
||||
await group.click();
|
||||
await expect(group).toHaveClass(/selected/);
|
||||
// React Flow ignores delete keys while an input is focused, and the panel
|
||||
// the click opens takes focus into its search — from an effect, about
|
||||
// 100ms later. Blurring before that lands leaves the input to claim focus
|
||||
// afterwards and swallow the Backspace, so wait for the focus, then drop
|
||||
// it.
|
||||
const panelSearch = page.locator("#cc-group-panel input").first();
|
||||
await expect(panelSearch).toBeFocused();
|
||||
await panelSearch.blur();
|
||||
await expect(panelSearch).not.toBeFocused();
|
||||
await page.evaluate(
|
||||
() => (document.activeElement as HTMLElement | null)?.blur(),
|
||||
);
|
||||
await page.keyboard.press("Backspace");
|
||||
await expect(canvasNode(page, "group-new-")).toHaveCount(0);
|
||||
await expectChangeCount(page, 0);
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
dismissBlockingOverlays,
|
||||
enterDraft,
|
||||
openControlCenter,
|
||||
openGroupView,
|
||||
resetDraftState,
|
||||
reviewButton,
|
||||
} from "../helpers/control-center";
|
||||
@@ -38,6 +37,7 @@ test.describe
|
||||
await deleteGroupsByPrefix(page, PREFIX);
|
||||
});
|
||||
|
||||
// Clean-slated so the draft group view's auto-select is deterministic.
|
||||
async function seedPolicyGroupView(page: Page) {
|
||||
await deletePoliciesBySubstring(page, PREFIX);
|
||||
await deleteGroupsByPrefix(page, PREFIX);
|
||||
@@ -49,9 +49,7 @@ test.describe
|
||||
src.id,
|
||||
dst.id,
|
||||
);
|
||||
// Selected by name: the view's own pick ranks every group in the account,
|
||||
// so a peer-bearing group from another spec on the other worker wins it.
|
||||
await openGroupView(page, src.name);
|
||||
await openControlCenter(page, "groups");
|
||||
await enterDraft(page);
|
||||
const policyNode = canvasNode(page, `policy-${policy.id}`);
|
||||
await expect(policyNode).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
clickContextMenuItem,
|
||||
dismissBlockingOverlays,
|
||||
openControlCenter,
|
||||
openGroupView,
|
||||
resetDraftState,
|
||||
switchFlowView,
|
||||
} from "../helpers/control-center";
|
||||
@@ -48,6 +47,8 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
});
|
||||
|
||||
async function seedPolicyAndOpenGroupView(page: Page, enabled = true) {
|
||||
// The group view auto-selects a group that HAS a policy, so stale policies
|
||||
// would make the rendered group non-deterministic.
|
||||
await deletePoliciesBySubstring(page, PREFIX);
|
||||
await deleteGroupsByPrefix(page, PREFIX);
|
||||
|
||||
@@ -63,10 +64,15 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
);
|
||||
|
||||
const policyNode = canvasNode(page, `policy-${policy.id}`);
|
||||
// Selecting src by name rather than trusting the view's own pick: the
|
||||
// auto-select ranks every group in the account, so a peer-bearing group
|
||||
// another spec seeded on the other worker outranks this one.
|
||||
await openGroupView(page, src.name);
|
||||
// A stale SWR read or a build/fit race can fail to place the node.
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
await openControlCenter(page, "groups");
|
||||
const shown = await policyNode
|
||||
.waitFor({ state: "visible", timeout: 10_000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (shown) break;
|
||||
}
|
||||
await expect(policyNode).toBeVisible({ timeout: 10_000 });
|
||||
return { src, dst, policy, policyNode };
|
||||
}
|
||||
@@ -506,6 +512,7 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
test("Should save group resource membership in live mode (correct payload)", async ({
|
||||
dashboardAsOwner: page,
|
||||
}) => {
|
||||
// Clean slate so the group view deterministically lands on our group.
|
||||
await deletePoliciesBySubstring(page, PREFIX);
|
||||
await deleteGroupsByPrefix(page, PREFIX);
|
||||
|
||||
@@ -522,7 +529,7 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
[],
|
||||
);
|
||||
|
||||
await openGroupView(page, src.name);
|
||||
await openControlCenter(page, "groups");
|
||||
const groupNode = canvasNode(page, `group-${dst.id}`);
|
||||
await expect(groupNode).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
@@ -603,7 +610,7 @@ test.describe.serial("Control Center Live Mode @control-center", () => {
|
||||
);
|
||||
await createPolicy(page, generateRandomName(PREFIX), src.id, d2.id);
|
||||
|
||||
await openGroupView(page, src.name);
|
||||
await openControlCenter(page, "groups");
|
||||
const policyNode = canvasNode(page, `policy-${p1.id}`);
|
||||
await expect(policyNode).toBeVisible({ timeout: 15_000 });
|
||||
await expect(
|
||||
|
||||
Generated
-7
@@ -10,7 +10,6 @@
|
||||
"dependencies": {
|
||||
"@axa-fr/react-oidc": "^7.26.3",
|
||||
"@dagrejs/dagre": "^1.1.5",
|
||||
"@novnc/novnc": "^1.7.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
@@ -1710,12 +1709,6 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@novnc/novnc": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@novnc/novnc/-/novnc-1.7.0.tgz",
|
||||
"integrity": "sha512-ucEJOx4T2avIRCleodk7YobZj5O2Ga2AeLfQ69A/yjG9HHba2+PDgwSkN3FttrmG+70ZGx21sElNFouK13RzyA==",
|
||||
"license": "MPL-2.0"
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.144.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz",
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"dependencies": {
|
||||
"@axa-fr/react-oidc": "^7.26.3",
|
||||
"@dagrejs/dagre": "^1.1.5",
|
||||
"@novnc/novnc": "^1.7.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
|
||||
@@ -70,10 +70,8 @@ import ReverseProxiesProvider, {
|
||||
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
|
||||
import { PeerEditIPModal } from "@/modules/peer/PeerEditIPModal";
|
||||
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
|
||||
import { isSSHSupportedOnOS } from "@/modules/remote-access/osSupport";
|
||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
import { VNCButton } from "@/modules/remote-access/vnc/VNCButton";
|
||||
import { PeerExpirationSettings } from "@/modules/peer/PeerExpirationSettings";
|
||||
|
||||
export default function PeerPage() {
|
||||
@@ -432,13 +430,6 @@ const PeerOverviewTabContent = () => {
|
||||
const { peer } = usePeer();
|
||||
const { permission } = usePermissions();
|
||||
const { selectedGroups, setSelectedGroups } = usePeerSettings();
|
||||
const isSSHSupported = isSSHSupportedOnOS(peer?.os);
|
||||
// Named in the order the buttons appear below, and only the ones that do.
|
||||
// RDP and VNC are unconditional: those servers need not be ours, so any
|
||||
// system may be running one.
|
||||
const remoteAccessMethods = [isSSHSupported && "SSH", "RDP", "VNC"]
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
|
||||
return (
|
||||
<div className={"px-8"}>
|
||||
@@ -467,17 +458,14 @@ const PeerOverviewTabContent = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSSHSupported && <PeerSSHToggle />}
|
||||
<PeerSSHToggle />
|
||||
|
||||
<div>
|
||||
<Label>Remote Access</Label>
|
||||
<HelpText>
|
||||
Connect directly to this peer via {remoteAccessMethods}.
|
||||
</HelpText>
|
||||
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
|
||||
<div className="flex gap-3">
|
||||
<SSHButton peer={peer} />
|
||||
<RDPButton peer={peer} />
|
||||
<VNCButton peer={peer} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { sendErrorNotification } from "@/modules/remote-access/errorNotification";
|
||||
import { notify } from "@components/Notification";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { IconCircleX } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
import { cn } from "@utils/helpers";
|
||||
|
||||
export default function RDPPage() {
|
||||
const { peerId, ipVersion } = useRDPQueryParams();
|
||||
const { peerId } = useRDPQueryParams();
|
||||
|
||||
const {
|
||||
data: peer,
|
||||
@@ -34,7 +35,7 @@ export default function RDPPage() {
|
||||
return (
|
||||
<div className={"w-screen h-screen overflow-hidden fixed inset-0"}>
|
||||
{peerId && peer && !isLoading ? (
|
||||
<RDPSession key={peer.id} peer={peer} ipVersion={ipVersion} />
|
||||
<RDPSession key={peer.id} peer={peer} />
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
)}
|
||||
@@ -44,10 +45,9 @@ export default function RDPPage() {
|
||||
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
ipVersion: string | null;
|
||||
};
|
||||
|
||||
function RDPSession({ peer, ipVersion }: Props) {
|
||||
function RDPSession({ peer }: Props) {
|
||||
const client = useNetBirdClient();
|
||||
const [isNetBirdConnecting, setIsNetBirdConnecting] = useState(false);
|
||||
const rdp = useRemoteDesktop(client);
|
||||
@@ -55,14 +55,20 @@ function RDPSession({ peer, ipVersion }: Props) {
|
||||
const [credentials, setCredentials] = useState<RDPCredentials | null>(null);
|
||||
const connected = useRef(false);
|
||||
|
||||
// Dial the IPv6 address when IPv6 was selected, IPv4 otherwise.
|
||||
const rdpHost =
|
||||
credentials?.ipVersion === "6" && peer.ipv6 ? peer.ipv6 : peer.ip;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${peer.name} - ${peer.ip} - RDP`;
|
||||
}, [peer.ip, peer.name, connected, rdp]);
|
||||
|
||||
const sendErrorNotification = (title: string, message: string) => {
|
||||
notify({
|
||||
title: title,
|
||||
description: message,
|
||||
icon: <IconCircleX size={24} />,
|
||||
backgroundColor: "bg-red-500",
|
||||
duration: 10000,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the RDP session state but keep the NetBird client connected,
|
||||
* so a retry does not pay the full engine reconnect.
|
||||
@@ -127,7 +133,7 @@ function RDPSession({ peer, ipVersion }: Props) {
|
||||
if (!credentials) return;
|
||||
try {
|
||||
const result = await rdp.connect({
|
||||
hostname: rdpHost,
|
||||
hostname: peer.ip,
|
||||
port: credentials.port,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
@@ -146,7 +152,7 @@ function RDPSession({ peer, ipVersion }: Props) {
|
||||
setCredentialsModal(true);
|
||||
await reset();
|
||||
}
|
||||
}, [credentials, rdpHost, rdp, reset]);
|
||||
}, [credentials, peer.ip, rdp, reset]);
|
||||
|
||||
/**
|
||||
* Establish RDP session when NetBird connection is ready
|
||||
@@ -216,7 +222,6 @@ function RDPSession({ peer, ipVersion }: Props) {
|
||||
peer={peer}
|
||||
onConnect={connect}
|
||||
loading={isLoading}
|
||||
initialIpVersion={ipVersion}
|
||||
/>
|
||||
|
||||
{/* Certificate Modal */}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
useNetBirdClient,
|
||||
} from "@/modules/remote-access/useNetBirdClient";
|
||||
import {
|
||||
usesStandardSSHPort,
|
||||
isNativeSSHSupported,
|
||||
isNetbirdSSHProtocolSupported,
|
||||
} from "@utils/version";
|
||||
|
||||
@@ -83,14 +83,8 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
const isClientDisconnected = client.status === NetBirdStatus.DISCONNECTED;
|
||||
const isClientConnecting = client.status === NetBirdStatus.CONNECTING;
|
||||
|
||||
// Dial the IPv6 address when ip_version=6 is requested and the peer has one,
|
||||
// IPv4 otherwise. peer.ip is always IPv4, so a request for "6" on a peer
|
||||
// without an IPv6 address has to fall back on the version too, or the dialer
|
||||
// is told to use a family the host doesn't belong to.
|
||||
const requestedIPv6 = ipVersion === "6" ? peer.ipv6 : undefined;
|
||||
const sshHost = requestedIPv6 ?? peer.ip;
|
||||
const sshIpVersion =
|
||||
ipVersion === "6" ? (requestedIPv6 ? "6" : "4") : ipVersion;
|
||||
// Use the FQDN when an IP version is specified so the dialer resolves to the correct address family.
|
||||
const sshHost = ipVersion ? peer.dns_label || peer.ip : peer.ip;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${username}@${sshHost} - ${peer.hostname}`;
|
||||
@@ -101,7 +95,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
if (isSSHConnected || isSSHConnecting) return;
|
||||
connected.current = false;
|
||||
try {
|
||||
const aclPort = usesStandardSSHPort(peer.version) ? "22022" : port;
|
||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
||||
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
||||
? "netbird-ssh"
|
||||
: "tcp";
|
||||
@@ -111,7 +105,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
hostname: sshHost,
|
||||
port: Number(port),
|
||||
username,
|
||||
ipVersion: sshIpVersion || undefined,
|
||||
ipVersion: ipVersion || undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Reconnection failed:", error);
|
||||
@@ -128,7 +122,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
connected.current = true;
|
||||
|
||||
try {
|
||||
const aclPort = usesStandardSSHPort(peer.version) ? "22022" : port;
|
||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
||||
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
||||
? "netbird-ssh"
|
||||
: "tcp";
|
||||
@@ -138,7 +132,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
hostname: sshHost,
|
||||
port: Number(port),
|
||||
username,
|
||||
ipVersion: sshIpVersion || undefined,
|
||||
ipVersion: ipVersion || undefined,
|
||||
});
|
||||
if (res === SSHStatus.CONNECTED) {
|
||||
sshConnectedOnce.current = true;
|
||||
@@ -174,7 +168,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
return (
|
||||
<DisconnectedMessage
|
||||
username={username}
|
||||
peerIp={sshHost}
|
||||
peerIp={peer.ip}
|
||||
onReconnect={handleReconnect}
|
||||
/>
|
||||
);
|
||||
@@ -184,7 +178,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
<>
|
||||
{session && <Terminal session={session} onClose={disconnect} />}
|
||||
{!isSSHConnected && (
|
||||
<LoadingMessage message={`Connecting to ${username}@${sshHost}...`} />
|
||||
<LoadingMessage message={`Connecting to ${username}@${peer.ip}...`} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -81,31 +81,6 @@ p {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* VNC viewport: noVNC writes the cursor inline on the canvas it creates
|
||||
* inside our container, not on the container itself. When there's no
|
||||
* remote sprite yet the inline value is `cursor: none`, and once a Cursor
|
||||
* pseudo-encoded sprite arrives it becomes `cursor: url(...)`. The :not()
|
||||
* rule below forces the browser default arrow as long as the inline
|
||||
* style isn't using a url(), so the user has a visible pointer in the
|
||||
* gap before any sprite is delivered and the remote sprite still wins
|
||||
* once it shows up. Skip this for vnc-cursor-remote so the second rule
|
||||
* below can actually hide the pointer when compositing is on.
|
||||
*/
|
||||
.vnc-cursor:not(.vnc-cursor-remote) canvas:not([style*="url("]) {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* In "show remote cursor" mode the server composites its cursor sprite
|
||||
* directly into the captured framebuffer, so the local pointer must be
|
||||
* hidden to avoid two stacked cursors. Matches both the empty/none and
|
||||
* url() inline cases.
|
||||
*/
|
||||
.vnc-cursor-remote canvas {
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
.timescape {
|
||||
@apply flex items-center gap-[1px] rounded-md py-2 px-3 select-none w-fit cursor-text bg-nb-gray-900;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 290 KiB |
@@ -4,7 +4,6 @@ import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const QUERY_PARAMS_KEY = "netbird-query-params";
|
||||
const PRESERVE_QUERY_PARAMS_PATHS = ["/peer/ssh", "/peer/rdp", "/peer/vnc"];
|
||||
|
||||
type StoredQueryParams = { path: string; params: string };
|
||||
|
||||
@@ -22,12 +21,6 @@ const VALID_PARAMS = [
|
||||
"page_size",
|
||||
"user",
|
||||
"port",
|
||||
"ip_version",
|
||||
"mode",
|
||||
"quality",
|
||||
"scale",
|
||||
"resize",
|
||||
"cursor",
|
||||
];
|
||||
|
||||
function filterAllowedParams(raw: string): string {
|
||||
@@ -39,13 +32,6 @@ function filterAllowedParams(raw: string): string {
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
function storeQueryParams(path: string, rawParams: string) {
|
||||
const filtered = filterAllowedParams(rawParams);
|
||||
if (!filtered) return;
|
||||
const data: StoredQueryParams = { path, params: filtered };
|
||||
localStorage.setItem(QUERY_PARAMS_KEY, JSON.stringify(data));
|
||||
}
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
@@ -56,15 +42,6 @@ export const SecureProvider = ({ children }: Props) => {
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
try {
|
||||
const params = window.location.search.substring(1);
|
||||
// Persist on every render while on a remote-access path. The OIDC
|
||||
// library can fire a redirect synchronously when an access token
|
||||
// expires, before the !isAuthenticated branch of this effect ever
|
||||
// runs, so the peer id would be lost across the round-trip otherwise.
|
||||
if (PRESERVE_QUERY_PARAMS_PATHS.includes(currentPath) && params) {
|
||||
storeQueryParams(currentPath, params);
|
||||
return;
|
||||
}
|
||||
const stored = localStorage.getItem(QUERY_PARAMS_KEY);
|
||||
if (stored && !window.location.search) {
|
||||
const data: StoredQueryParams = JSON.parse(stored);
|
||||
@@ -82,7 +59,14 @@ export const SecureProvider = ({ children }: Props) => {
|
||||
try {
|
||||
const params = window.location.search.substring(1);
|
||||
if (params) {
|
||||
storeQueryParams(currentPath, params);
|
||||
const filtered = filterAllowedParams(params);
|
||||
if (filtered) {
|
||||
const data: StoredQueryParams = {
|
||||
path: currentPath,
|
||||
params: filtered,
|
||||
};
|
||||
localStorage.setItem(QUERY_PARAMS_KEY, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,16 @@ import LoadingIcon from "@/assets/icons/LoadingIcon";
|
||||
|
||||
type Props = {
|
||||
fullScreen?: boolean;
|
||||
// label, when set, renders under the spinner so callers can spell out
|
||||
// which step of a multi-stage flow is in progress.
|
||||
label?: string;
|
||||
};
|
||||
export default function FullScreenLoading({ fullScreen = true, label }: Props) {
|
||||
export default function FullScreenLoading({ fullScreen = true }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-center gap-4 w-screen",
|
||||
"flex items-center justify-center w-screen",
|
||||
fullScreen && "h-screen",
|
||||
)}
|
||||
>
|
||||
<LoadingIcon className="fill-netbird" size={44} />
|
||||
{label && <div className="text-sm text-nb-gray-400">{label}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export interface Account {
|
||||
local_mfa_enabled?: boolean;
|
||||
ipv6_enabled_groups?: string[];
|
||||
network_range_v6?: string;
|
||||
debug_bundle_upload_url?: string;
|
||||
agent_network_only?: boolean;
|
||||
dashboard_features?: AccountDashboardFeatures;
|
||||
};
|
||||
|
||||
@@ -17,14 +17,7 @@ export interface Workload {
|
||||
// Parameters for bundle job
|
||||
export interface BundleJobParameters {
|
||||
anonymize: boolean;
|
||||
// Anonymization level applied when anonymize is enabled. "default" keeps
|
||||
// internal IP ranges readable; "strict" also anonymizes them. Omitted or
|
||||
// empty resolves to the default on the peer.
|
||||
anonymize_level?: "default" | "strict";
|
||||
bundle_for: boolean;
|
||||
bundle_for_time: number;
|
||||
log_file_count: number;
|
||||
// Upload service URL the peer requests an upload URL from. Empty selects the
|
||||
// default upload server.
|
||||
upload_url?: string;
|
||||
}
|
||||
Loaded 20 of 49 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user