mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-10 18:53:03 +02:00
Add 'Reverse Link' button to trace pane. Closes #287.
This commit is contained in:
@@ -318,6 +318,26 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
|
|||||||
clearPendingResult();
|
clearPendingResult();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Append the reversed hop chain (minus the current endpoint) to build a return
|
||||||
|
// path, e.g. [R1, R2, R3] -> [R1, R2, R3, R2, R1]. A single hop is left as-is.
|
||||||
|
// See issue #287. Reverses every queued hop, including custom prefixes.
|
||||||
|
const handleReverseLink = () => {
|
||||||
|
setDraftHops((current) => {
|
||||||
|
if (current.length < 2) return current;
|
||||||
|
const returnHops = [...current]
|
||||||
|
.reverse()
|
||||||
|
.slice(1)
|
||||||
|
.map(
|
||||||
|
(hop, i): TraceDraftHop => ({
|
||||||
|
...hop,
|
||||||
|
id: nextDraftHopId(hop.kind, current.length + i),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return [...current, ...returnHops];
|
||||||
|
});
|
||||||
|
clearPendingResult();
|
||||||
|
};
|
||||||
|
|
||||||
const handleLoadRecentTrace = async (trace: SavedTrace) => {
|
const handleLoadRecentTrace = async (trace: SavedTrace) => {
|
||||||
const hops: TraceDraftHop[] = trace.hops.map((h, i) => {
|
const hops: TraceDraftHop[] = trace.hops.map((h, i) => {
|
||||||
if (h.kind === 'repeater' && h.publicKey) {
|
if (h.kind === 'repeater' && h.publicKey) {
|
||||||
@@ -616,18 +636,31 @@ export function TracePane({ contacts, config, onRunTracePath }: TracePaneProps)
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{draftHops.length > 0 ? (
|
{draftHops.length > 0 ? (
|
||||||
<Button
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
type="button"
|
<Button
|
||||||
size="sm"
|
type="button"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
className="shrink-0 text-muted-foreground"
|
variant="ghost"
|
||||||
onClick={() => {
|
className="text-muted-foreground"
|
||||||
setDraftHops([]);
|
onClick={handleReverseLink}
|
||||||
clearPendingResult();
|
disabled={draftHops.length < 2}
|
||||||
}}
|
title="Append the reversed hop chain to build a return path"
|
||||||
>
|
>
|
||||||
Clear
|
Reverse link
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-muted-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
setDraftHops([]);
|
||||||
|
clearPendingResult();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 p-4 lg:min-h-0 lg:flex-1 lg:overflow-y-auto">
|
<div className="space-y-2 p-4 lg:min-h-0 lg:flex-1 lg:overflow-y-auto">
|
||||||
|
|||||||
@@ -134,6 +134,49 @@ describe('TracePane', () => {
|
|||||||
expect(screen.getByText('No hops selected')).toBeInTheDocument();
|
expect(screen.getByText('No hops selected')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reverse link appends the reversed hop chain to build a return path (issue #287)', async () => {
|
||||||
|
const relayA = makeContact('11'.repeat(32), 'Relay Alpha');
|
||||||
|
const relayB = makeContact('22'.repeat(32), 'Relay Beta');
|
||||||
|
const relayC = makeContact('33'.repeat(32), 'Relay Charlie');
|
||||||
|
const onRunTracePath = vi.fn(
|
||||||
|
async (): Promise<RadioTraceResponse> => ({
|
||||||
|
path_len: 0,
|
||||||
|
timeout_seconds: 6,
|
||||||
|
nodes: [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TracePane
|
||||||
|
config={config}
|
||||||
|
onRunTracePath={onRunTracePath}
|
||||||
|
contacts={[relayA, relayB, relayC]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Single hop: Reverse link is a no-op (and disabled).
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /^add repeater relay alpha/i }));
|
||||||
|
expect(screen.getByRole('button', { name: /reverse link/i })).toBeDisabled();
|
||||||
|
|
||||||
|
// R1, R2, R3 -> append R2, R1 => R1, R2, R3, R2, R1.
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /^add repeater relay beta/i }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /^add repeater relay charlie/i }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /reverse link/i }));
|
||||||
|
|
||||||
|
expect(screen.getByText('5 hops selected · 4-byte trace')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /send trace/i }));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onRunTracePath).toHaveBeenCalledWith(4, [
|
||||||
|
{ public_key: relayA.public_key },
|
||||||
|
{ public_key: relayB.public_key },
|
||||||
|
{ public_key: relayC.public_key },
|
||||||
|
{ public_key: relayB.public_key },
|
||||||
|
{ public_key: relayA.public_key },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('allows adding the same repeater multiple times from the picker row', () => {
|
it('allows adding the same repeater multiple times from the picker row', () => {
|
||||||
const relayA = makeContact('11'.repeat(32), 'Relay Alpha');
|
const relayA = makeContact('11'.repeat(32), 'Relay Alpha');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user