33 * Author: skhell *
44 * Description: Net Commander is the extension for Visual Studio Code *
55 * dedicated to Network Engineers, DevOps Engineers and *
6- * Solution Architects streamlining everyday workflows and *
6+ * Solution Architects streamlining everyday workflows and *
77 * accelerating data-driven root-cause analysis. *
88 * *
99 * Github: https://github.com/skhell/net-commander *
1919
2020// media/module-ping/main.js
2121
22- // suppress ResizeObserver loop errors
2322window . addEventListener ( 'error' , event => {
24- if (
25- event . message &&
26- event . message . includes ( 'ResizeObserver loop completed with undelivered notifications' )
27- ) {
23+ if ( event . message && event . message . includes ( 'ResizeObserver loop completed with undelivered notifications' ) ) {
2824 event . preventDefault ( ) ;
2925 return false ;
3026 }
3127 } ) ;
3228
3329document . addEventListener ( 'DOMContentLoaded' , ( ) => {
34- const vscode = acquireVsCodeApi ( ) ;
35- const targetsInput = document . getElementById ( 'pingtargets' ) ;
36- const countInput = document . getElementById ( 'pingcount' ) ;
37- const sizeInput = document . getElementById ( 'pingsize' ) ;
38- const pingBtn = document . getElementById ( 'pingBtn' ) ;
39- const clearBtn = document . getElementById ( 'clearBtn' ) ;
40- const exportBtn = document . getElementById ( 'exportBtn' ) ;
41- const resultsDiv = document . getElementById ( 'results' ) ;
30+ const vscode = acquireVsCodeApi ( ) ;
31+ const targetsInput = document . getElementById ( 'pingtargets' ) ;
32+ const countInput = document . getElementById ( 'pingcount' ) ;
33+ const sizeInput = document . getElementById ( 'pingsize' ) ;
34+ const pingBtn = document . getElementById ( 'pingBtn' ) ;
35+ const stopBtn = document . getElementById ( 'stopBtn' ) ;
36+ const clearBtn = document . getElementById ( 'clearBtn' ) ;
37+ const resultsDiv = document . getElementById ( 'results' ) ;
38+
4239 let resultsByTarget = { } ;
43-
44- if ( ! targetsInput || ! countInput || ! sizeInput || ! pingBtn || ! clearBtn || ! exportBtn || ! resultsDiv ) {
40+ let totalTargets = 0 ;
41+ let completedTargets = 0 ;
42+ let inputLabel = '' ;
43+ let running = false ;
44+
45+ if ( ! targetsInput || ! countInput || ! sizeInput || ! pingBtn || ! stopBtn || ! clearBtn || ! resultsDiv ) {
4546 console . error ( 'Missing DOM elements in Ping UI' ) ;
4647 return ;
4748 }
48-
49+
4950 pingBtn . addEventListener ( 'click' , ( ) => {
50- const targets = targetsInput . value . split ( / , | \n / )
51- . map ( t => t . trim ( ) ) . filter ( t => t ) ;
52- resultsByTarget = { } ;
53- renderClear ( ) ;
51+ const targets = targetsInput . value . split ( / [ , \n ] / ) . map ( t => t . trim ( ) ) . filter ( t => t ) ;
52+ if ( targets . length === 0 ) return ;
53+ inputLabel = targetsInput . value . trim ( ) ;
54+ resultsByTarget = { } ;
55+ totalTargets = 0 ;
56+ completedTargets = 0 ;
57+ running = true ;
58+ setRunning ( true ) ;
59+ resultsDiv . innerHTML = '<div style="padding:12px;">Preparing targets\u2026</div>' ;
5460 vscode . postMessage ( {
5561 command : 'ping' ,
56- data : {
57- targets,
58- count : parseInt ( countInput . value , 10 ) || 4 ,
59- size : parseInt ( sizeInput . value , 10 ) || 56
60- }
62+ data : { targets, count : parseInt ( countInput . value , 10 ) || 4 , size : parseInt ( sizeInput . value , 10 ) || 56 }
6163 } ) ;
6264 } ) ;
63-
65+
66+ stopBtn . addEventListener ( 'click' , ( ) => {
67+ running = false ;
68+ setRunning ( false ) ;
69+ resultsDiv . innerHTML = '<div style="padding:12px;">Stopped.</div>' ;
70+ vscode . postMessage ( { command : 'stop' } ) ;
71+ } ) ;
72+
6473 clearBtn . addEventListener ( 'click' , ( ) => {
65- resultsByTarget = { } ;
66- renderClear ( ) ;
74+ running = false ;
75+ resultsByTarget = { } ;
76+ totalTargets = 0 ;
77+ completedTargets = 0 ;
78+ inputLabel = '' ;
79+ setRunning ( false ) ;
80+ resultsDiv . innerHTML = '' ;
6781 vscode . postMessage ( { command : 'clear' } ) ;
6882 } ) ;
69-
70- exportBtn . addEventListener ( 'click' , ( ) => {
71- // I export one file per target
72- Object . values ( resultsByTarget ) . forEach ( group => {
73- let csv = '' ;
74- group . replies . forEach ( r => {
75- csv += [
76- r . seq || '' ,
77- r . bytes , r . ttl , r . time ,
78- r . target , r . localIP , r . macAddress , r . timestamp
79- ] . join ( ',' ) + '\n' ;
80- } ) ;
81- const s = group . summary ;
82- if ( s ) {
83- csv += `Summary:,Sent=${ s . transmitted } ,Rec=${ s . received } ,Loss=${ s . loss } ,Time=${ s . totalTime } ,RTT=${ s . rtt
84- ? `${ s . rtt . min } /${ s . rtt . avg } /${ s . rtt . max } /${ s . rtt . mdev } `
85- : 'N/A' } \n`;
86- }
87- vscode . postMessage ( { command : 'exportCSV' , data : { csv, targets : [ group . target ] } } ) ;
88- } ) ;
89- } ) ;
90-
83+
84+ function setRunning ( on ) {
85+ pingBtn . style . display = on ? 'none' : '' ;
86+ stopBtn . style . display = on ? '' : 'none' ;
87+ }
88+
9189 window . addEventListener ( 'message' , ( { data } ) => {
9290 switch ( data . command ) {
91+ case 'pingTotal' :
92+ totalTargets = data . total ;
93+ completedTargets = 0 ;
94+ renderProgress ( 0 , totalTargets ) ;
95+ break ;
9396 case 'pingResult' :
9497 handlePingResult ( data ) ;
9598 break ;
9699 case 'clearResults' :
97- renderClear ( ) ;
100+ resultsByTarget = { } ;
101+ totalTargets = 0 ;
102+ completedTargets = 0 ;
103+ resultsDiv . innerHTML = '' ;
98104 break ;
99105 case 'toggleStop' :
100106 break ;
101107 }
102108 } ) ;
103-
109+
104110 function handlePingResult ( { data } ) {
111+ if ( ! running ) return ;
105112 const { target, type } = data ;
106113 if ( ! resultsByTarget [ target ] ) {
107114 resultsByTarget [ target ] = { target, replies : [ ] , summary : null } ;
108115 }
109116 if ( type === 'reply' ) {
110- resultsByTarget [ target ] . replies . push ( data . row ) ;
117+ const entry = resultsByTarget [ target ] ;
118+ entry . replies . push ( data . row ) ;
119+
120+ // Compute cumulative running stats up to this reply
121+ const replies = entry . replies ;
122+ const received = replies . length ;
123+ const sent = parseInt ( data . row . seq , 10 ) || received ;
124+ const lossStr = sent > 0 ? ( ( sent - received ) / sent * 100 ) . toFixed ( 1 ) + '%' : '0%' ;
125+ const rtts = replies . map ( r => parseFloat ( r . time ) ) . filter ( v => ! isNaN ( v ) ) ;
126+ data . row . _running = {
127+ sent,
128+ received,
129+ loss : lossStr ,
130+ rttMin : rtts . length ? Math . min ( ...rtts ) . toFixed ( 3 ) + ' ms' : '' ,
131+ rttAvg : rtts . length ? ( rtts . reduce ( ( a , b ) => a + b , 0 ) / rtts . length ) . toFixed ( 3 ) + ' ms' : '' ,
132+ rttMax : rtts . length ? Math . max ( ...rtts ) . toFixed ( 3 ) + ' ms' : ''
133+ } ;
111134 } else if ( type === 'summary' ) {
112135 resultsByTarget [ target ] . summary = data . summary ;
136+ completedTargets ++ ;
137+ renderProgress ( completedTargets , totalTargets ) ;
138+ if ( totalTargets > 0 && completedTargets >= totalTargets ) {
139+ running = false ;
140+ setRunning ( false ) ;
141+ autoExportAll ( ) ;
142+ }
113143 }
114- renderAll ( ) ;
115144 }
116-
117- function renderClear ( ) {
118- resultsDiv . innerHTML = '' ;
145+
146+ function renderProgress ( done , total ) {
147+ const pct = total > 0 ? Math . round ( done / total * 100 ) : 0 ;
148+ resultsDiv . innerHTML = `<div style="padding:12px;">
149+ <div>Pinging <strong>${ total } </strong> host${ total !== 1 ? 's' : '' } … <strong>${ done } / ${ total } </strong> completed (${ pct } %)</div>
150+ <div style="background:var(--vscode-editorWidget-border,#454545);height:6px;border-radius:3px;margin-top:10px;overflow:hidden;">
151+ <div style="background:var(--vscode-progressBar-background,#0e70c0);height:100%;width:${ pct } %;transition:width 0.15s;"></div>
152+ </div>
153+ </div>` ;
119154 }
120-
121- function renderAll ( ) {
122- resultsDiv . innerHTML = '' ;
123- for ( const key in resultsByTarget ) {
124- const group = resultsByTarget [ key ] ;
125- const title = document . createElement ( 'h3' ) ;
126- title . textContent = `Ping: ${ group . target } ` ;
127- resultsDiv . appendChild ( title ) ;
128-
129- const table = document . createElement ( 'vscode-table' ) ;
130- table . zebra = true ;
131- table [ 'bordered-rows' ] = true ;
132-
133- const header = document . createElement ( 'vscode-table-header' ) ;
134- header . slot = 'header' ;
135- [ 'Seq' , 'Bytes' , 'TTL' , 'Time' , 'Target' , 'Source' , 'Source Mac' , 'Timestamp' ]
136- . forEach ( col => {
137- const th = document . createElement ( 'vscode-table-header-cell' ) ;
138- th . textContent = col ;
139- header . appendChild ( th ) ;
155+
156+ function autoExportAll ( ) {
157+ let csv = 'Target,Seq,Bytes,TTL,Time,Sent,Received,Loss,RTT Min,RTT Avg,RTT Max,Source,Source Mac,Timestamp\n ' ;
158+ Object . values ( resultsByTarget ) . forEach ( group => {
159+ const s = group . summary ;
160+ if ( group . replies . length > 0 ) {
161+ group . replies . forEach ( ( r ) => {
162+ const rs = r . _running || { } ;
163+ csv += [
164+ group . target ,
165+ r . seq || '' ,
166+ r . bytes , r . ttl , r . time ,
167+ rs . sent !== undefined ? rs . sent : '' ,
168+ rs . received !== undefined ? rs . received : '' ,
169+ rs . loss || '' ,
170+ rs . rttMin || '' ,
171+ rs . rttAvg || '' ,
172+ rs . rttMax || '' ,
173+ r . localIP , r . macAddress , r . timestamp
174+ ] . join ( ',' ) + '\n' ;
140175 } ) ;
141- table . appendChild ( header ) ;
142-
143- const body = document . createElement ( 'vscode-table-body' ) ;
144- body . slot = 'body' ;
145- group . replies . forEach ( row => {
146- const tr = document . createElement ( 'vscode-table-row' ) ;
147- [ row . seq || '' , row . bytes , row . ttl , row . time , row . target , row . localIP , row . macAddress , row . timestamp ]
148- . forEach ( val => {
149- const td = document . createElement ( 'vscode-table-cell' ) ;
150- td . textContent = val ;
151- tr . appendChild ( td ) ;
152- } ) ;
153- body . appendChild ( tr ) ;
154- } ) ;
155-
156- if ( group . summary ) {
157- const tr = document . createElement ( 'vscode-table-row' ) ;
158- const td = document . createElement ( 'vscode-table-cell' ) ;
159- td . colspan = 8 ;
160- td . style . fontWeight = 'bold' ;
161- const s = group . summary ;
162- td . textContent = `Sent ${ s . transmitted } , Rec ${ s . received } , Loss ${ s . loss } , Time ${ s . totalTime } , RTT ${ s . rtt
163- ? `${ s . rtt . min } /${ s . rtt . avg } /${ s . rtt . max } /${ s . rtt . mdev } `
164- : 'N/A' } `;
165- tr . appendChild ( td ) ;
166- body . appendChild ( tr ) ;
176+ } else {
177+ // unreachable host — one row with summary stats so it appears in the CSV
178+ csv += [
179+ group . target , '' , '' , '' , '' ,
180+ s ? s . transmitted : '' ,
181+ s ? s . received : '' ,
182+ s ? s . loss : '' ,
183+ '' , '' , '' , '' , '' , ''
184+ ] . join ( ',' ) + '\n' ;
167185 }
168-
169- table . appendChild ( body ) ;
170- resultsDiv . appendChild ( table ) ;
171- }
186+ } ) ;
187+ vscode . postMessage ( { command : 'exportCSV' , data : { csv, targets : [ inputLabel ] } } ) ;
188+ resultsDiv . innerHTML = `<div style="padding:12px;">
189+ <strong>${ totalTargets } host${ totalTargets !== 1 ? 's' : '' } </strong> completed. CSV exported automatically.
190+ </div>` ;
172191 }
173192 } ) ;
174-
0 commit comments