@@ -11,26 +11,143 @@ speedtest.on(history.record);
1111
1212speedtest . on ( ( ) => {
1313 const scrollPosition = window . scrollY ;
14- render ( < Table history = { history . read ( ) } blockList = { globalBlockList } /> ) ;
14+ render ( < Table history = { history . read ( ) } blockList = { globalBlockList } isPaused = { globalIsPaused } /> ) ;
1515 window . scrollY = scrollPosition ;
1616} ) ;
1717
1818let globalBlockList = [ ] ;
19+ let globalIsPaused = false ;
20+
1921speedtest . onBlocklistUpdate ( ( blockList ) => ( globalBlockList = blockList ) ) ;
2022
23+ speedtest . onStatusChange ( ( status ) => {
24+ globalIsPaused = status . paused ;
25+ const scrollPosition = window . scrollY ;
26+ render ( < Table history = { history . read ( ) } blockList = { globalBlockList } isPaused = { globalIsPaused } /> ) ;
27+ window . scrollY = scrollPosition ;
28+ } ) ;
29+
2130function render ( jsx ) {
2231 ReactDom . render ( jsx , document . getElementById ( "content" ) ) ;
2332}
2433
2534const Table = class extends React . Component {
2635 constructor ( props ) {
2736 super ( props ) ;
37+ this . state = {
38+ darkMode : localStorage . getItem ( 'darkMode' ) === 'true' ,
39+ isPaused : props . isPaused || false
40+ } ;
2841 this . renderButton = this . renderButton . bind ( this ) ;
2942 this . renderFlag = this . renderFlag . bind ( this ) ;
3043 this . renderFlag2 = this . renderFlag2 . bind ( this ) ;
3144 this . renderRow = this . renderRow . bind ( this ) ;
3245 this . renderError = this . renderError . bind ( this ) ;
46+ this . toggleDarkMode = this . toggleDarkMode . bind ( this ) ;
47+ this . exportToCSV = this . exportToCSV . bind ( this ) ;
48+ this . exportToJSON = this . exportToJSON . bind ( this ) ;
49+ this . togglePauseResume = this . togglePauseResume . bind ( this ) ;
50+ }
51+
52+ componentDidMount ( ) {
53+ // Apply dark mode on mount
54+ if ( this . state . darkMode ) {
55+ document . body . classList . add ( 'dark-mode' ) ;
56+ }
57+ // Save history to localStorage
58+ this . saveHistoryToLocalStorage ( ) ;
59+ }
60+
61+ componentDidUpdate ( prevProps ) {
62+ // Save history to localStorage on updates
63+ this . saveHistoryToLocalStorage ( ) ;
64+
65+ // Update paused state if prop changed
66+ if ( prevProps . isPaused !== this . props . isPaused ) {
67+ this . setState ( { isPaused : this . props . isPaused } ) ;
68+ }
69+ }
70+
71+ saveHistoryToLocalStorage ( ) {
72+ try {
73+ const historyData = {
74+ timestamp : new Date ( ) . toISOString ( ) ,
75+ results : this . props . history . slice ( 0 , 20 ) // Save last 20 results
76+ } ;
77+ localStorage . setItem ( 'speedTestHistory' , JSON . stringify ( historyData ) ) ;
78+ } catch ( e ) {
79+ console . error ( 'Failed to save to localStorage' , e ) ;
80+ }
81+ }
82+
83+ toggleDarkMode ( ) {
84+ const newDarkMode = ! this . state . darkMode ;
85+ this . setState ( { darkMode : newDarkMode } ) ;
86+ localStorage . setItem ( 'darkMode' , newDarkMode ) ;
87+
88+ if ( newDarkMode ) {
89+ document . body . classList . add ( 'dark-mode' ) ;
90+ } else {
91+ document . body . classList . remove ( 'dark-mode' ) ;
92+ }
3393 }
94+
95+ exportToCSV ( ) {
96+ const headers = [ 'Data Center' , 'Average Latency (ms)' , 'Min' , 'Max' ] ;
97+ const rows = this . props . history . map ( item => [
98+ item . name ,
99+ Math . round ( item . average ) ,
100+ item . values && item . values . length > 0 ? Math . min ( ...item . values ) : 'N/A' ,
101+ item . values && item . values . length > 0 ? Math . max ( ...item . values ) : 'N/A'
102+ ] ) ;
103+
104+ const csvContent = [
105+ headers . join ( ',' ) ,
106+ ...rows . map ( row => row . join ( ',' ) )
107+ ] . join ( '\n' ) ;
108+
109+ const blob = new Blob ( [ csvContent ] , { type : 'text/csv' } ) ;
110+ const url = window . URL . createObjectURL ( blob ) ;
111+ const a = document . createElement ( 'a' ) ;
112+ a . href = url ;
113+ a . download = `azure-devops-speed-test-${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } .csv` ;
114+ a . click ( ) ;
115+ window . URL . revokeObjectURL ( url ) ;
116+ }
117+
118+ exportToJSON ( ) {
119+ const data = {
120+ timestamp : new Date ( ) . toISOString ( ) ,
121+ results : this . props . history . map ( item => ( {
122+ name : item . name ,
123+ domain : item . domain ,
124+ average : Math . round ( item . average ) ,
125+ values : item . values || [ ] ,
126+ icon : item . icon ,
127+ icon2 : item . icon2
128+ } ) )
129+ } ;
130+
131+ const blob = new Blob ( [ JSON . stringify ( data , null , 2 ) ] , { type : 'application/json' } ) ;
132+ const url = window . URL . createObjectURL ( blob ) ;
133+ const a = document . createElement ( 'a' ) ;
134+ a . href = url ;
135+ a . download = `azure-devops-speed-test-${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } .json` ;
136+ a . click ( ) ;
137+ window . URL . revokeObjectURL ( url ) ;
138+ }
139+
140+ togglePauseResume ( ) {
141+ const newPausedState = ! this . state . isPaused ;
142+ this . setState ( { isPaused : newPausedState } ) ;
143+
144+ if ( newPausedState ) {
145+ speedtest . pause ( ) ;
146+ } else {
147+ speedtest . resume ( ) ;
148+ }
149+ }
150+
34151 renderButton ( ) {
35152 let item = this . props . history [ 0 ] ;
36153
@@ -125,6 +242,25 @@ const Table = class extends React.Component {
125242 render ( ) {
126243 return (
127244 < div >
245+ < button className = "dark-mode-toggle" onClick = { this . toggleDarkMode } >
246+ { this . state . darkMode ? '☀️ Light Mode' : '🌙 Dark Mode' }
247+ </ button >
248+
249+ < div className = "export-buttons" >
250+ < button
251+ className = { `btn ${ this . state . isPaused ? 'btn-success' : 'btn-warning' } ` }
252+ onClick = { this . togglePauseResume }
253+ >
254+ { this . state . isPaused ? '▶️ Resume Testing' : '⏸️ Pause Testing' }
255+ </ button >
256+ < button className = "btn btn-success" onClick = { this . exportToCSV } >
257+ 📊 Export to CSV
258+ </ button >
259+ < button className = "btn btn-info" onClick = { this . exportToJSON } >
260+ 📄 Export to JSON
261+ </ button >
262+ </ div >
263+
128264 < table className = "table results-table" >
129265 < thead >
130266 < tr >
0 commit comments