Skip to content

Commit 1b09bad

Browse files
committed
Add pause/resume control and tooling
1 parent 55317b2 commit 1b09bad

7 files changed

Lines changed: 500 additions & 11 deletions

File tree

.github/workflows/ci-cd.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: CI/CD Pipeline
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
pull_request:
7+
branches: [master, main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
matrix:
15+
node-version: [18.x, 20.x]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Use Node.js ${{ matrix.node-version }}
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: ${{ matrix.node-version }}
24+
cache: "npm"
25+
26+
- name: Install dependencies
27+
run: npm ci
28+
29+
- name: Run linter
30+
run: npm run lint
31+
32+
- name: Run tests
33+
run: npm test
34+
35+
- name: Build project
36+
run: npm run browserify
37+
38+
deploy:
39+
needs: test
40+
runs-on: ubuntu-latest
41+
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
42+
43+
steps:
44+
- uses: actions/checkout@v4
45+
46+
- name: Use Node.js
47+
uses: actions/setup-node@v4
48+
with:
49+
node-version: "20.x"
50+
cache: "npm"
51+
52+
- name: Install dependencies
53+
run: npm ci
54+
55+
- name: Build
56+
run: npm run browserify
57+
58+
- name: Deploy to GitHub Pages
59+
uses: peaceiris/actions-gh-pages@v3
60+
with:
61+
github_token: ${{ secrets.GITHUB_TOKEN }}
62+
publish_dir: ./
63+
publish_branch: gh-pages
64+
exclude_assets: "node_modules,test,.git*,*.md,lib,*.jsx"

index.html

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,83 @@
2020
<title>Github Azure DevOps SpeedTest</title>
2121
<link rel="icon" type="image/png" href="/favicon.png" />
2222
<style>
23+
:root {
24+
--bg-color: #ffffff;
25+
--text-color: #212529;
26+
--jumbotron-bg: #e9ecef;
27+
--table-bg: #ffffff;
28+
--table-stripe: #f8f9fa;
29+
--border-color: #dee2e6;
30+
}
31+
32+
body.dark-mode {
33+
--bg-color: #1a1a1a;
34+
--text-color: #e9ecef;
35+
--jumbotron-bg: #2d2d2d;
36+
--table-bg: #2d2d2d;
37+
--table-stripe: #3d3d3d;
38+
--border-color: #454545;
39+
}
40+
41+
body {
42+
background-color: var(--bg-color);
43+
color: var(--text-color);
44+
transition: background-color 0.3s, color 0.3s;
45+
}
46+
47+
.jumbotron {
48+
background-color: var(--jumbotron-bg);
49+
color: var(--text-color);
50+
}
51+
52+
.table {
53+
background-color: var(--table-bg);
54+
color: var(--text-color);
55+
}
56+
57+
.table tbody tr:hover {
58+
background-color: var(--table-stripe);
59+
}
60+
61+
.dark-mode-toggle {
62+
position: fixed;
63+
top: 20px;
64+
right: 20px;
65+
padding: 10px 20px;
66+
border: none;
67+
border-radius: 25px;
68+
background-color: var(--jumbotron-bg);
69+
color: var(--text-color);
70+
cursor: pointer;
71+
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
72+
z-index: 1000;
73+
transition: all 0.3s;
74+
}
75+
76+
.dark-mode-toggle:hover {
77+
transform: scale(1.05);
78+
}
79+
80+
.export-buttons {
81+
margin: 20px 0;
82+
}
83+
84+
.export-buttons button {
85+
margin-right: 10px;
86+
margin-bottom: 10px;
87+
}
88+
89+
.btn-warning {
90+
background-color: #ffc107;
91+
border-color: #ffc107;
92+
color: #212529;
93+
}
94+
95+
.btn-warning:hover {
96+
background-color: #e0a800;
97+
border-color: #d39e00;
98+
}
99+
23100
.container {
24101
/*width: 970px;*/
25102
}
@@ -52,6 +129,13 @@
52129
.no-mobile {
53130
display: none;
54131
}
132+
133+
.dark-mode-toggle {
134+
top: 10px;
135+
right: 10px;
136+
padding: 8px 16px;
137+
font-size: 14px;
138+
}
55139
}
56140
</style>
57141
</head>

index.jsx

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,143 @@ speedtest.on(history.record);
1111

1212
speedtest.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

1818
let globalBlockList = [];
19+
let globalIsPaused = false;
20+
1921
speedtest.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+
2130
function render(jsx) {
2231
ReactDom.render(jsx, document.getElementById("content"));
2332
}
2433

2534
const 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

Comments
 (0)