Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/app/(dashboard)/peer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
MonitorSmartphoneIcon,
NetworkIcon,
PencilIcon,
RadioTowerIcon,
TerminalSquare,
TimerResetIcon,
} from "lucide-react";
Expand All @@ -65,6 +66,7 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
import { PeerExpirationToggle } from "@/modules/peer/PeerExpirationToggle";
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
import { PeerRemoteJobsSection } from "@/modules/peer/PeerRemoteJobsSection";

export default function PeerPage() {
const queryParameter = useSearchParams();
Expand Down Expand Up @@ -397,6 +399,13 @@ const PeerOverviewTabs = () => {
Accessible Peers
</TabsTrigger>
)}

{peer?.id && permission.peers.delete && (
<TabsTrigger value={"peer-job"}>
<RadioTowerIcon size={16} />
Remote Job
</TabsTrigger>
)}
</TabsList>

{permission.routes.read && (
Expand All @@ -410,6 +419,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>
);
};
Expand Down Expand Up @@ -580,9 +594,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) +
")"
}
/>

Expand Down
26 changes: 26 additions & 0 deletions src/interfaces/Job.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Parameters for bundle job
export interface BundleJobParameters {
anonymize: boolean
bundle_for: boolean
bundle_for_time: number
log_file_count: number
}

// Base job
interface BaseJob {
ID: string
AccountID: string
CompletedAt: Date | null
CreatedAt: Date
FailedReason: string | null
PeerID: string
Result: string | null
Status: "pending" | "successed" | "failed"
TriggeredBy: string
}

// Discriminated union
export type Job =
| (BaseJob & { Type: "bundle"; Parameters: BundleJobParameters })
| (BaseJob & { Type: "other"; Parameters: Record<string, any> }) // fallback for unknown types

119 changes: 119 additions & 0 deletions src/modules/jobs/CreateDebugJobModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import FancyToggleSwitch from "@/components/FancyToggleSwitch";
import { ModalClose, ModalContent, ModalFooter } from "@/components/modal/Modal";
import ModalHeader from "@/components/modal/ModalHeader";
import { BugPlay, PlusCircle } from "lucide-react";
import { Label } from "@/components/Label";
import { Input } from "@/components/Input";
import Button from "@/components/Button";
import { useState, useMemo } from "react";
import HelpText from "@/components/HelpText";
import { useApiCall } from "@/utils/api";
import { useSWRConfig } from "swr";
import { notify } from "@/components/Notification";

type Props = {
peerID: string;
onSuccess: () => void;
};

export function CreateDebugJobModalContent({ peerID, onSuccess }: Props) {
const jobRequest = useApiCall(`/peers/${peerID}/jobs`, true);
const { mutate } = useSWRConfig();

const [bundleForTime, setBundleForTime] = useState<number>(5);
const [logFileCount, setLogFileCount] = useState<number>(10);
const [anonymize, setAnonymize] = useState<boolean>(true);

const isValid = useMemo(() => {
return bundleForTime > 0 && logFileCount > 0;
}, [bundleForTime, logFileCount]);

const createDebugJob = async () => {
notify({
title: "Create Debug Job",
description: "Debug job triggered successfully.",
loadingMessage: "Creating job...",
promise: jobRequest
.post({
Type: "bundle",
Parameters: {
anonymize,
bundle_for: true,
bundle_for_time: bundleForTime,
log_file_count: logFileCount,
},
})
.then((job) => {
mutate(`/peers/${peerID}/jobs`);
onSuccess();
return job;
}),
});
}; return (
<ModalContent maxWidthClass="max-w-lg">
<ModalHeader
icon={<BugPlay size={20} />}
title="Create Debug Job"
description="Generate a debug bundle on this peer with logs and diagnostics. Useful for troubleshooting without CLI access."
color="netbird"
/>

<div className="pb-6">
<div className="px-8 flex flex-col gap-6">
<div>
<Label>Bundle Duration (minutes)</Label>
<HelpText>
Defines how long logs will be collected for the debug bundle.
</HelpText>
<Input
type="number"
min={1}
max={60}
value={bundleForTime}
onChange={(e) => setBundleForTime(Number(e.target.value))}
/>
</div>

<div>
<Label>Log File Count</Label>
<HelpText>
Maximum number of log files to include in the bundle.
</HelpText>
<Input
type="number"
min={1}
max={50}
value={logFileCount}
onChange={(e) => setLogFileCount(Number(e.target.value))}
/>
</div>

<FancyToggleSwitch
value={anonymize}
onChange={setAnonymize}
label="Anonymize Data"
helpText="Remove sensitive information (IP addresses, peer IDs) from the bundle."
/>
</div>
</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 Job
</Button>
</div>
</ModalFooter>
</ModalContent>
);
}

56 changes: 56 additions & 0 deletions src/modules/peer/PeerRemoteJobsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Paragraph from '@/components/Paragraph';
import SkeletonTable, { SkeletonTableHeader } from '@/components/skeletons/SkeletonTable';
import { usePortalElement } from '@/hooks/usePortalElement';
import React, { Suspense, lazy } from 'react'
import { RemoteJobDropdownButton } from './RemoteJobDropdownButton';
import useFetchApi from '@/utils/api';
import { Job } from '@/interfaces/Job';
const PeerRemoteJobsTable = lazy(
() => import("@/modules/peer/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>
</div>

<div className="inline-flex gap-4 justify-end">
<RemoteJobDropdownButton />
</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>)
}
Loading