Compare commits

...
8 Commits
Author SHA1 Message Date
Eduard Gert 818ba5daa4 Allow wildcard dns zone records (#536) 2026-01-20 17:32:14 +01:00
Ali AmerandEduard Gert 3a30f76629 Add Frontend Support for Peer Debug Bundle Trigger and History (#485)
* implement debug ui

* update job ui

* Add type cell, show tooltip if peer is offline, add copy to clipboard for upload key, show error reason in tooltip

* update job event description

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-01-20 17:12:33 +01:00
Misha Bragin 34dc21c89d Add password change (embedded Idp) (#535) 2026-01-20 15:00:14 +01:00
Eduard Gert 2e37703622 Update CONTRIBUTOR_LICENSE_AGREEMENT.md (#534) 2026-01-19 14:55:04 +01:00
Eduard Gert 8aec338c43 Fix dns doc link (#533) 2026-01-19 10:01:55 +01:00
Viktor Liu f4f0c240fd Bump wasm to v0.63.0 (#531) 2026-01-19 09:49:26 +01:00
Viktor Liu 04e22a3c7e Enable SSH for Windows and Android peers (#532)
* Enable SSH for Windows and Android peers, hide update badge for temporary peers

* Fix RDP to use tcp protocol instead of netbird-ssh
2026-01-19 09:49:08 +01:00
Eduard Gert 54ef076303 Fix config vars (#529) 2026-01-16 19:59:42 +01:00
25 changed files with 1016 additions and 34 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
## Contributor License Agreement
This Contributor License Agreement (referred to as the "Agreement") is entered into by the individual
submitting this Agreement and NetBird GmbH, c/o Max-Beer-StraĂźe 2-4 MĂĽnzstraĂźe 12 10178 Berlin, Germany,
submitting this Agreement and NetBird GmbH, BrunnenstraĂźe 196, 10119 Berlin, Germany,
referred to as "NetBird" (collectively, the "Parties"). The Agreement outlines the terms and conditions
under which NetBird may utilize software contributions provided by the Contributor for inclusion in
its software development projects. By submitting this Agreement, the Contributor confirms their acceptance
+16 -8
View File
@@ -1,10 +1,18 @@
{
"auth0Auth": "true",
"authAuthority": "https://netbird-localdev.eu.auth0.com",
"authClientId": "kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt",
"authScopesSupported": "openid profile email api offline_access email_verified",
"authAudience": "http://localhost:3000/",
"apiOrigin": "http://localhost",
"grpcApiOrigin": "http://localhost:80",
"latestVersion": "v0.6.3"
"auth0Auth": "$USE_AUTH0",
"authAuthority": "$AUTH_AUTHORITY",
"authClientId": "$AUTH_CLIENT_ID",
"authClientSecret": "$AUTH_CLIENT_SECRET",
"authScopesSupported": "$AUTH_SUPPORTED_SCOPES",
"authAudience": "$AUTH_AUDIENCE",
"apiOrigin": "$NETBIRD_MGMT_API_ENDPOINT",
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT",
"redirectURI": "$AUTH_REDIRECT_URI",
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID",
"wasmPath": "$NETBIRD_WASM_PATH"
}
+18 -3
View File
@@ -40,6 +40,8 @@ import {
MonitorSmartphoneIcon,
NetworkIcon,
PencilIcon,
RadioTowerIcon,
TimerResetIcon,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { toASCII } from "punycode";
@@ -60,6 +62,7 @@ import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
import { PeerRemoteJobsSection } from "@/modules/peer/PeerRemoteJobsSection";
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
@@ -326,6 +329,13 @@ const PeerOverviewTabs = () => {
Accessible Peers
</TabsTrigger>
)}
{peer?.id && permission.peers.delete && (
<TabsTrigger value={"peer-job"}>
<RadioTowerIcon size={16} />
Remote Jobs
</TabsTrigger>
)}
</TabsList>
{permission.routes.read && (
@@ -339,6 +349,11 @@ const PeerOverviewTabs = () => {
<AccessiblePeersSection peerID={peer.id} />
</TabsContent>
)}
{peer.id && permission.peers.delete && (
<TabsContent value={"peer-job"} className={"pb-8"}>
<PeerRemoteJobsSection peerID={peer.id} />
</TabsContent>
)}
</Tabs>
);
};
@@ -526,9 +541,9 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
peer.connected
? "just now"
: dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
" (" +
dayjs().to(peer.last_seen) +
")"
" (" +
dayjs().to(peer.last_seen) +
")"
}
/>
+1 -5
View File
@@ -20,7 +20,6 @@ import {
useNetBirdClient,
} from "@/modules/remote-access/useNetBirdClient";
import { cn } from "@utils/helpers";
import { isNetbirdSSHProtocolSupported } from "@utils/version";
export default function RDPPage() {
const { peerId } = useRDPQueryParams();
@@ -85,11 +84,8 @@ function RDPSession({ peer }: Props) {
try {
setCredentials(rdpCredentials);
setIsNetBirdConnecting(true);
const protocol = isNetbirdSSHProtocolSupported(peer.version)
? "netbird-ssh"
: "tcp";
await client.connectTemporary(peer.id, [
`${protocol}/${rdpCredentials.port}`,
`tcp/${rdpCredentials.port}`,
]);
setIsNetBirdConnecting(false);
} catch (error) {
+36
View File
@@ -0,0 +1,36 @@
import { cn } from "@utils/helpers";
import * as React from "react";
export const TooltipListItem = ({
icon,
label,
value,
className,
labelClassName,
}: {
icon?: React.ReactNode;
label: string;
value: string | React.ReactNode;
className?: string;
labelClassName?: string;
}) => {
return (
<div
className={cn(
"flex justify-between gap-12 border-b border-nb-gray-920 py-2 px-4 last:border-b-0",
className,
)}
>
<div
className={cn(
"flex items-center gap-2 text-nb-gray-100 font-medium",
labelClassName,
)}
>
{icon}
{label}
</div>
<div className={"text-nb-gray-300"}>{value}</div>
</div>
);
};
+36 -6
View File
@@ -11,7 +11,7 @@ import {
} from "@components/DropdownMenu";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import { UserAvatar } from "@components/ui/UserAvatar";
import { LogOutIcon, User2 } from "lucide-react";
import { KeyRound, LogOutIcon, User2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
@@ -19,9 +19,13 @@ import { useApplicationContext } from "@/contexts/ApplicationProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { useLoggedInUser } from "@/contexts/UsersProvider";
import useOSDetection from "@/hooks/useOperatingSystem";
import { ChangePasswordModalContent } from "@/modules/users/ChangePasswordModal";
import { isNetBirdHosted } from "@utils/netbird";
import { Modal } from "@components/modal/Modal";
export default function UserDropdown() {
const [dropdownOpen, setDropdownOpen] = useState(false);
const [changePasswordModal, setChangePasswordModal] = useState(false);
const { user } = useApplicationContext();
const { loggedInUser, logout } = useLoggedInUser();
const { isRestricted, permission } = usePermissions();
@@ -31,11 +35,22 @@ export default function UserDropdown() {
useHotkeys("shift+mod+l", () => logout(), []);
return (
<DropdownMenu
modal={false}
open={dropdownOpen}
onOpenChange={setDropdownOpen}
>
<>
<Modal
open={changePasswordModal}
onOpenChange={setChangePasswordModal}
key={changePasswordModal ? 1 : 0}
>
<ChangePasswordModalContent
userId={loggedInUser?.id}
onSuccess={() => setChangePasswordModal(false)}
/>
</Modal>
<DropdownMenu
modal={false}
open={dropdownOpen}
onOpenChange={setDropdownOpen}
>
<DropdownMenuTrigger>
<UserAvatar size={"medium"} />
</DropdownMenuTrigger>
@@ -72,6 +87,20 @@ export default function UserDropdown() {
/>
)}
{!isNetBirdHosted() && loggedInUser?.idp_id === "local" && (
<DropdownMenuItem
onClick={() => {
setDropdownOpen(false);
setChangePasswordModal(true);
}}
>
<div className={"flex gap-3 items-center"}>
<KeyRound size={14} />
Change Password
</div>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={logout}>
<div className={"flex gap-3 items-center"}>
<LogOutIcon size={14} />
@@ -81,6 +110,7 @@ export default function UserDropdown() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
}
+4 -2
View File
@@ -19,5 +19,7 @@ export interface DNSRecord {
export type DNSRecordType = "A" | "AAAA" | "CNAME";
export const DNS_ZONE_DOCS_LINK = "https://docs.netbird.io/manage/dns/zones";
export const DNS_RECORDS_DOCS_LINK = "https://docs.netbird.io/manage/dns/zones";
export const DNS_ZONE_DOCS_LINK =
"https://docs.netbird.io/manage/dns/custom-zones";
export const DNS_RECORDS_DOCS_LINK =
"https://docs.netbird.io/manage/dns/custom-zones#adding-records-to-a-zone";
+23
View File
@@ -0,0 +1,23 @@
export interface Job {
id: string;
triggered_by: string;
completed_at: Date | null;
created_at: Date;
failed_reason: string | null;
workload: Workload;
status: "pending" | "succeeded" | "failed";
}
export interface Workload {
type: "bundle";
parameters: BundleJobParameters;
result: string | null;
}
// Parameters for bundle job
export interface BundleJobParameters {
anonymize: boolean;
bundle_for: boolean;
bundle_for_time: number;
log_file_count: number;
}
@@ -277,6 +277,14 @@ export default function ActivityDescription({ event }: Props) {
</div>
);
if (event.activity_code == "user.password.change")
return (
<div className={"inline"}>
Password was changed for user <Value>{event.meta.username}</Value>{" "}
<Value>{event.meta.email}</Value>
</div>
);
/**
* Service User
*/
@@ -693,6 +701,16 @@ export default function ActivityDescription({ event }: Props) {
</div>
);
/**
* Jobs
*/
if (event.activity_code == "peer.job.create")
return (<div className={"inline"}>
Remote job <Value>{m.job_type}</Value> created for peer <Value>{m.for_peer_name}</Value>
</div>
)
if (event.activity_code == "account.settings.extra.flow.group.remove")
return (
<div className={"inline"}>
+1
View File
@@ -33,6 +33,7 @@ const ACTION_COLOR_MAPPING: Record<string, ActionStatus> = {
rename: ActionStatus.INFO,
unblock: ActionStatus.INFO,
login: ActionStatus.INFO,
change: ActionStatus.INFO,
};
export function getColorFromCode(code: string): string {
+5 -3
View File
@@ -101,8 +101,9 @@ export function DNSRecordModalContent({
const domainError = useMemo(() => {
if (domain == "") return "";
if (domain === "*") return "";
const valid = validator.isValidDomain(domain, {
allowWildcard: false,
allowWildcard: true,
allowOnlyTld: true,
});
if (!valid) {
@@ -210,12 +211,13 @@ export function DNSRecordModalContent({
<div className={"w-full mb-3"}>
<Label>Hostname</Label>
<HelpText>
Enter a subdomain or leave empty to use the primary domain.
Enter a subdomain, wildcard or leave empty to use the primary
domain.
</HelpText>
<div className={"flex w-full"}>
<Input
autoFocus={true}
placeholder={"Subdomain (leave empty for primary domain)"}
placeholder={"E.g., dev, * or leave empty for primary domain"}
errorTooltip={true}
errorTooltipPosition={"bottom"}
error={domainError}
+194
View File
@@ -0,0 +1,194 @@
import {
AlarmClock,
BugPlay,
FileText,
PlusCircle,
Shield,
} from "lucide-react";
import { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import Button from "@/components/Button";
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
import HelpText from "@/components/HelpText";
import { Input } from "@/components/Input";
import { Label } from "@/components/Label";
import {
ModalClose,
ModalContent,
ModalFooter,
} from "@/components/modal/Modal";
import ModalHeader from "@/components/modal/ModalHeader";
import { notify } from "@/components/Notification";
import Separator from "@/components/Separator";
import { Workload } from "@/interfaces/Job";
import { useApiCall } from "@/utils/api";
type Props = {
peerID: string;
onSuccess: () => void;
};
export function CreateDebugJobModalContent({ peerID, onSuccess }: Props) {
const jobRequest = useApiCall<Workload>(`/peers/${peerID}/jobs`, true);
const { mutate } = useSWRConfig();
const [bundleForTimeEnabled, setBundleForTimeEnabled] = useState(false);
const [bundleForTime, setBundleForTime] = useState<string>("");
const [logFileCount, setLogFileCount] = useState<string>("10");
const [anonymize, setAnonymize] = useState<boolean>(false);
const isValid = useMemo(() => {
let validBundleFor = true;
let validLogFileCount = true;
const logFileCountNumber = Number(logFileCount);
const bundleForTimeNumber = Number(bundleForTime);
if (bundleForTime) {
validBundleFor = bundleForTimeNumber >= 1 && bundleForTimeNumber <= 5;
}
validLogFileCount = logFileCountNumber >= 1 && logFileCountNumber <= 1000;
return validLogFileCount && validBundleFor;
}, [bundleForTime, logFileCount]);
const createDebugJob = async () => {
notify({
title: "Create Debug Job",
description: "Debug job triggered successfully.",
loadingMessage: "Creating job...",
promise: jobRequest
.post({
workload: {
type: "bundle",
parameters: {
anonymize,
bundle_for: bundleForTimeEnabled,
bundle_for_time: bundleForTimeEnabled
? Number(bundleForTime)
: undefined,
log_file_count: logFileCount ? Number(logFileCount) : 10,
},
},
})
.then((job) => {
mutate(`/peers/${peerID}/jobs`);
onSuccess();
return job;
}),
});
};
return (
<ModalContent maxWidthClass="max-w-xl">
<ModalHeader
icon={<BugPlay size={20} />}
title="Debug Bundle"
description="Generate a debug bundle on this peer with logs and diagnostics. Useful for troubleshooting without CLI access."
color="netbird"
/>
<Separator />
<div className={"px-8 py-6 flex flex-col gap-4"}>
{/* Log File Count */}
<div className="flex justify-between gap-6">
<div className={"max-w-[300px]"}>
<Label>Log File Count</Label>
<HelpText>
Sets the limit for how many individual log files will be included
in the debug bundle.
</HelpText>
</div>
<Input
type="number"
min={1}
placeholder={"10"}
max={50}
value={logFileCount}
onChange={(e) => setLogFileCount(e.target.value)}
maxWidthClass="w-[220px]"
customPrefix={<FileText size={16} className="text-nb-gray-300" />}
customSuffix="File(s)"
/>
</div>
{/* Bundle Duration */}
<div>
<FancyToggleSwitch
value={bundleForTimeEnabled}
onChange={(enabled) => {
setBundleForTimeEnabled(enabled);
if (!enabled) {
setBundleForTime("");
} else {
setBundleForTime("2");
}
}}
label={
<>
<AlarmClock size={15} />
Enable Bundle Duration
</>
}
helpText="When enabled, allows you to specify a time period for log collection before generating the debug bundle."
/>
{bundleForTimeEnabled && (
<div className="flex justify-between gap-6 mt-6 mb-3">
<div className={"max-w-[300px]"}>
<Label>Duration</Label>
<HelpText>
Time period for which logs should be collected before creating
the debug bundle.
</HelpText>
</div>
<Input
type="number"
min={1}
max={60}
value={bundleForTime}
onChange={(e) => setBundleForTime(e.target.value)}
maxWidthClass="w-[220px]"
placeholder={"2"}
customPrefix={
<AlarmClock size={16} className="text-nb-gray-300" />
}
customSuffix="Minute(s)"
/>
</div>
)}
</div>
{/* Anonymize Data */}
<FancyToggleSwitch
value={anonymize}
onChange={setAnonymize}
label={
<>
<Shield size={15} />
Anonymize Log Data
</>
}
helpText="Remove sensitive information (IP addresses, domains etc.) before creating the debug bundle."
/>
</div>
<ModalFooter className="items-center">
<div className="flex gap-3 w-full justify-end">
<ModalClose asChild>
<Button variant="secondary">Cancel</Button>
</ModalClose>
<Button
variant="primary"
disabled={!isValid}
onClick={createDebugJob}
>
<PlusCircle size={16} />
Create Debug Bundle
</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
+60
View File
@@ -0,0 +1,60 @@
import Badge from "@components/Badge";
import CopyToClipboardText from "@components/CopyToClipboardText";
import FullTooltip from "@components/FullTooltip";
import { Input } from "@components/Input";
import * as React from "react";
import { Job } from "@/interfaces/Job";
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
type Props = {
job: Job;
};
export const JobOutputCell = ({ job }: Props) => {
if (job.status === "succeeded" && job.workload.result) {
return (
<div className="flex flex-col gap-1 items-start justify-center pb-1">
{Object.entries(job.workload.result).map(([key, value]) => (
<div key={key} className="text-sm max-w-[200px]">
<span className="font-normal capitalize text-nb-gray-300 text-xs">
{key.replaceAll("_", " ")}
</span>
<br />
<span className="text-nb-gray-200 truncate">
<CopyToClipboardText
message={"Upload key has been copied to your clipboard"}
alwaysShowIcon={true}
>
<span className={"font-mono truncate"}>
{typeof value === "boolean"
? value
? "Yes"
: "No"
: String(value)}
</span>
</CopyToClipboardText>
</span>
</div>
))}
</div>
);
}
if (job.status === "failed" && job.failed_reason) {
return (
<div className={"flex"}>
<FullTooltip
content={
<div className={"max-w-xs text-xs"}>{job.failed_reason}</div>
}
>
<Badge variant={"red"} className={"px-3 max-w-[200px]"}>
<div className={"truncate"}>{job.failed_reason}</div>
</Badge>
</FullTooltip>
</div>
);
}
return <EmptyRow />;
};
@@ -0,0 +1,56 @@
import Badge from "@components/Badge";
import FullTooltip from "@components/FullTooltip";
import { TooltipListItem } from "@components/TooltipListItem";
import { InfoIcon } from "lucide-react";
import React from "react";
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
export const JobParametersCell = ({ parameters }: { parameters: any }) => {
if (!parameters || Object.keys(parameters).length === 0) {
return <EmptyRow />;
}
const entries = Object.entries(parameters);
return (
<FullTooltip
side={"top"}
interactive={true}
delayDuration={250}
skipDelayDuration={100}
contentClassName={"p-0"}
content={
<div
className={"text-xs flex flex-col"}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
{entries.map(([key, value]) => (
<TooltipListItem
label={key.replaceAll("_", " ")}
labelClassName={"capitalize"}
value={
typeof value === "boolean"
? value
? "Yes"
: "No"
: String(value)
}
key={key}
/>
))}
</div>
}
>
<Badge
variant="gray"
className="flex items-center gap-1.5 cursor-default"
>
<InfoIcon size={12} />
{entries.length} Parameters
</Badge>
</FullTooltip>
);
};
+30
View File
@@ -0,0 +1,30 @@
import { cn } from "@utils/helpers";
import React from "react";
import { Job } from "@/interfaces/Job";
type Props = {
job: Job;
};
export default function JobStatusCell({ job }: Readonly<Props>) {
const status = job.status;
return (
<div
className={cn("flex gap-2.5 items-center text-nb-gray-300 text-sm")}
data-cy={"job-status-cell"}
>
<span
className={cn(
"h-2 w-2 rounded-full",
status == "pending" && "bg-yellow-400",
status == "failed" && "bg-red-500",
status == "succeeded" && "bg-green-500",
)}
></span>
{status == "pending" && "Pending"}
{status == "failed" && "Failed"}
{status == "succeeded" && "Completed"}
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { BugIcon } from "lucide-react";
import * as React from "react";
import { Job } from "@/interfaces/Job";
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
type Props = {
job: Job;
};
export const JobTypeCell = ({ job }: Props) => {
if (job.workload.type === "bundle") {
return (
<div
className={"flex items-center gap-2 whitespace-nowrap text-nb-gray-200"}
>
<BugIcon size={14} />
<span>Debug Bundle</span>
</div>
);
}
return <EmptyRow />;
};
@@ -0,0 +1,141 @@
import Card from "@components/Card";
import { DataTable } from "@components/table/DataTable";
import DataTableHeader from "@components/table/DataTableHeader";
import NoResults from "@components/ui/NoResults";
import { ColumnDef, SortingState } from "@tanstack/react-table";
import { ClipboardList } from "lucide-react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import DataTableRefreshButton from "@/components/table/DataTableRefreshButton";
import { DataTableRowsPerPage } from "@/components/table/DataTableRowsPerPage";
import { Job } from "@/interfaces/Job";
import EmptyRow from "@/modules/common-table-rows/EmptyRow";
import LastTimeRow from "@/modules/common-table-rows/LastTimeRow";
import { JobOutputCell } from "@/modules/jobs/table/JobOutputCell";
import { JobParametersCell } from "@/modules/jobs/table/JobParametersCell";
import JobStatusCell from "@/modules/jobs/table/JobStatusCell";
import { JobTypeCell } from "@/modules/jobs/table/JobTypeCell";
import { RemoteJobDropdownButton } from "@/modules/peer/RemoteJobDropdownButton";
type Props = {
jobs?: Job[];
peerID: string;
isLoading: boolean;
headingTarget?: HTMLHeadingElement | null;
};
const PeerRemoteJobsColumns: ColumnDef<Job>[] = [
{
accessorKey: "Type",
header: ({ column }) => (
<DataTableHeader column={column}>Type</DataTableHeader>
),
cell: ({ row }) => <JobTypeCell job={row.original} />,
},
{
accessorKey: "CreatedAt",
header: ({ column }) => (
<DataTableHeader column={column}>Created</DataTableHeader>
),
sortingFn: "datetime",
cell: ({ row }) => (
<LastTimeRow date={row.original.created_at} text="Created at" />
),
},
{
accessorKey: "Status",
header: ({ column }) => (
<DataTableHeader column={column}>Status</DataTableHeader>
),
cell: ({ row }) => <JobStatusCell job={row.original} />,
},
{
accessorKey: "CompletedAt",
header: ({ column }) => (
<DataTableHeader column={column}>Completed</DataTableHeader>
),
sortingFn: "datetime",
cell: ({ row }) =>
row.original.completed_at ? (
<LastTimeRow date={row.original.completed_at} text="Completed at" />
) : (
<EmptyRow />
),
},
{
accessorKey: "Parameters",
header: ({ column }) => (
<DataTableHeader column={column}>Parameters</DataTableHeader>
),
cell: ({ row }) => (
<JobParametersCell parameters={row.original.workload.parameters} />
),
},
{
id: "ResultOrReason",
header: ({ column }) => (
<DataTableHeader column={column}>Output</DataTableHeader>
),
cell: ({ row }) => <JobOutputCell job={row.original} />,
},
];
export default function PeerRemoteJobsTable({
jobs,
isLoading,
headingTarget,
peerID,
}: Props) {
const { mutate } = useSWRConfig();
const [sorting, setSorting] = useState<SortingState>([
{ id: "CreatedAt", desc: true },
]);
return (
<DataTable
rightSide={() => (
<div className={"gap-x-4 ml-auto flex"}>
<RemoteJobDropdownButton />
</div>
)}
wrapperComponent={Card}
wrapperProps={{ className: "mt-6 w-full" }}
headingTarget={headingTarget}
useRowId={true}
sorting={sorting}
setSorting={setSorting}
minimal={true}
showSearchAndFilters={true}
inset={false}
tableClassName="mt-0"
text="Jobs"
columns={PeerRemoteJobsColumns}
keepStateInLocalStorage={false}
data={jobs}
searchPlaceholder="Search by type, status, or parameters..."
isLoading={isLoading}
getStartedCard={
<NoResults
className="py-4"
title="This peer has no remote jobs"
description="Create a debug bundle or trigger other remote jobs to see them listed here."
icon={<ClipboardList size={20} className="text-nb-gray-300" />}
/>
}
paginationPaddingClassName="px-0 pt-8"
>
{(table) => (
<>
<DataTableRowsPerPage table={table} disabled={jobs?.length == 0} />
<DataTableRefreshButton
isDisabled={jobs?.length == 0}
onClick={() => {
mutate(`/peers/${peerID}/jobs`).then();
}}
/>
</>
)}
</DataTable>
);
}
@@ -58,6 +58,7 @@ export default function AddRouteDropdownButton() {
icon={<PlusCircle size={14} />}
color={"green"}
margin={""}
size={"small"}
/>
<div className={"flex flex-col text-left"}>
<div className={"text-left text-white"}>New Network Route</div>
@@ -79,6 +80,7 @@ export default function AddRouteDropdownButton() {
}
color={"netbird"}
margin={""}
size={"small"}
/>
<div className={"flex flex-col text-left"}>
<div className={"text-left text-white"}>Existing Network</div>
@@ -0,0 +1,64 @@
import { ExternalLinkIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import InlineLink from "@/components/InlineLink";
import Paragraph from "@/components/Paragraph";
import SkeletonTable, {
SkeletonTableHeader,
} from "@/components/skeletons/SkeletonTable";
import { usePortalElement } from "@/hooks/usePortalElement";
import { Job } from "@/interfaces/Job";
import useFetchApi from "@/utils/api";
const PeerRemoteJobsTable = lazy(
() => import("@/modules/jobs/table/PeerRemoteJobsTable"),
);
type Props = {
peerID: string;
};
export const PeerRemoteJobsSection = ({ peerID }: Props) => {
const { data: jobs, isLoading } = useFetchApi<Job[]>(`/peers/${peerID}/jobs`);
const { ref: headingRef, portalTarget } =
usePortalElement<HTMLHeadingElement>();
return (
<div className="pb-10 px-8">
<div className="max-w-6xl">
<div className="flex justify-between items-center mb-5">
<div>
<h2 ref={headingRef}>Remote Jobs</h2>
<Paragraph>
Remotely trigger actions such as debug bundles or other tasks on
this peer, without requiring CLI access.
</Paragraph>
<Paragraph>
Learn more about{" "}
<InlineLink href={"https://docs.netbird.io"} target={"_blank"}>
Remote Jobs <ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
</div>
<Suspense
fallback={
<div>
<SkeletonTableHeader className="!p-0" />
<div className="mt-8 w-full">
<SkeletonTable withHeader={false} />
</div>
</div>
}
>
<PeerRemoteJobsTable
peerID={peerID}
jobs={jobs}
isLoading={isLoading}
headingTarget={portalTarget}
/>
</Suspense>
</div>
</div>
);
};
@@ -0,0 +1,87 @@
import Button from "@components/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@components/DropdownMenu";
import { Modal } from "@components/modal/Modal";
import SquareIcon from "@components/SquareIcon";
import { BugPlay, ChevronDown } from "lucide-react";
import React, { useState } from "react";
import { usePeer } from "@/contexts/PeerProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { CreateDebugJobModalContent } from "../jobs/CreateDebugJobModal";
export const RemoteJobDropdownButton = () => {
const [modal, setModal] = useState(false);
const { peer } = usePeer();
const { permission } = usePermissions();
const isConnected = peer?.connected;
const disabled = !permission.peers.delete;
return (
<>
<Modal open={modal} onOpenChange={setModal} key={modal ? 1 : 0}>
<CreateDebugJobModalContent
peerID={peer.id!}
onSuccess={() => setModal(false)}
/>
</Modal>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
asChild={true}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<Button variant={"primary"} disabled={disabled}>
Run Remote Job
<ChevronDown size={16} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-auto" align="end" sideOffset={10}>
{!isConnected && (
<>
<div
className={
"text-xs flex items-center w-full justify-center max-w-xs px-3 py-3 text-nb-gray-200 font-light"
}
>
<div>
Peer{" "}
<span className={"text-white font-medium"}>{peer.name}</span>{" "}
is currently offline. Please connect the peer to run remote
jobs.
</div>
</div>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
onClick={() => setModal(true)}
disabled={disabled || !isConnected}
>
<div className={"flex gap-3 items-center justify-center pr-3"}>
<SquareIcon
icon={<BugPlay size={14} />}
margin={""}
size={"small"}
/>
<div className={"flex flex-col text-left"}>
<div className={"text-left text-white"}>Debug Bundle</div>
<div className={"text-xs"}>
Collect debug information for troubleshooting
</div>
</div>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
};
Loaded 20 of 25 files, more files were not shown because too many files have changed in this diff. Show more