Skip to content

Commit 79da93e

Browse files
tsemachhjunie-agent
andcommitted
feat(popup): collapsible advanced replay settings, compact fit-to-display table, status/hits as colored icon badges with tooltips
Co-authored-by: Junie <junie@jetbrains.com>
1 parent 40352e9 commit 79da93e

5 files changed

Lines changed: 86 additions & 79 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on Keep a Changelog,
66
and this project adheres to Semantic Versioning.
77

8+
## [1.0.15] - 2026-05-03
9+
### Changed
10+
- Removed the Replay stats panel (Matched / Unmatched / Recent unmatched). The per-row green ● match indicator on the API requests list is now the single source of truth.
11+
- Replay configuration (fallback matching, latency, latency range, URL mappings) is collapsed under a toggleable **Advanced settings** `<details>` block in the Replay tab to reduce vertical clutter.
12+
- Captured Requests table is now fixed-layout: long URLs are truncated in the **Path** cell with the full URL shown on hover via a `title` tooltip.
13+
- **Status** column is now a colored circular badge (green 2xx / blue 3xx / amber 4xx / red 5xx / gray for none); click the badge to edit the status. The numeric value is in the tooltip.
14+
- **Hits** column is now a small colored pill badge with the hit count and a descriptive tooltip.
15+
816
## [1.0.14] - 2026-05-03
917
### Changed
1018
- Replay status indicator moved above the Record/Replay tab buttons so the current state is visible regardless of the active tab.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apireplay",
3-
"version": "1.0.14",
3+
"version": "1.0.15",
44
"private": true,
55
"type": "module",
66
"scripts": {

src/popup.html

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,23 @@ <h3 class="text-base font-semibold">Select Recording</h3>
9898
</div>
9999
</div>
100100

101-
<div class="mb-4">
102-
<label class="flex items-center space-x-2" title="If enabled, replay can match by method + path/query even when host differs">
103-
<input type="checkbox" id="fallbackMatching" class="form-checkbox" title="Match requests by method and path/query when exact URL host does not match">
104-
<span>Enable fallback matching for similar paths</span>
105-
</label>
106-
<div class="grid grid-cols-2 gap-2 mt-2">
107-
<input id="latencyMs" type="number" min="0" placeholder="Latency ms" class="p-1 text-sm border rounded dark:bg-gray-700" title="Optional fixed delay in milliseconds before each mocked response is returned">
108-
<input id="latencyRange" type="text" placeholder="Range: min,max" class="p-1 text-sm border rounded dark:bg-gray-700" title="Optional random delay range in milliseconds as min,max (for example: 100,500)">
109-
</div>
110-
<div class="mt-2">
111-
<label class="block text-xs font-semibold mb-1" for="urlMappings" title="Map recorded URL prefixes to different prefixes during replay. One mapping per line in the format: /from -> /to (for example: /microservice1/api -> /api)">URL Mappings (one per line: /from -&gt; /to)</label>
112-
<textarea id="urlMappings" rows="3" placeholder="/microservice1/api -> /api" class="w-full p-1 text-sm border rounded dark:bg-gray-700 font-mono" title="Map recorded URL path prefixes to replay path prefixes. Format: /recorded-prefix -> /replay-prefix. Example: /microservice1/api -> /api"></textarea>
101+
<details class="mb-4 mt-3 border border-gray-300 dark:border-gray-600 rounded">
102+
<summary class="cursor-pointer px-2 py-1 text-sm font-semibold bg-gray-100 dark:bg-gray-800 rounded" title="Toggle advanced replay configuration">Advanced settings</summary>
103+
<div class="p-2">
104+
<label class="flex items-center space-x-2" title="If enabled, replay can match by method + path/query even when host differs">
105+
<input type="checkbox" id="fallbackMatching" class="form-checkbox" title="Match requests by method and path/query when exact URL host does not match">
106+
<span>Enable fallback matching for similar paths</span>
107+
</label>
108+
<div class="grid grid-cols-2 gap-2 mt-2">
109+
<input id="latencyMs" type="number" min="0" placeholder="Latency ms" class="p-1 text-sm border rounded dark:bg-gray-700" title="Optional fixed delay in milliseconds before each mocked response is returned">
110+
<input id="latencyRange" type="text" placeholder="Range: min,max" class="p-1 text-sm border rounded dark:bg-gray-700" title="Optional random delay range in milliseconds as min,max (for example: 100,500)">
111+
</div>
112+
<div class="mt-2">
113+
<label class="block text-xs font-semibold mb-1" for="urlMappings" title="Map recorded URL prefixes to different prefixes during replay. One mapping per line in the format: /from -> /to (for example: /microservice1/api -> /api)">URL Mappings (one per line: /from -&gt; /to)</label>
114+
<textarea id="urlMappings" rows="3" placeholder="/microservice1/api -> /api" class="w-full p-1 text-sm border rounded dark:bg-gray-700 font-mono" title="Map recorded URL path prefixes to replay path prefixes. Format: /recorded-prefix -> /replay-prefix. Example: /microservice1/api -> /api"></textarea>
115+
</div>
113116
</div>
114-
</div>
115-
116-
<div id="replayStatsPanel" class="mb-4 p-2 rounded bg-gray-200 dark:bg-gray-700 text-xs hidden"></div>
117+
</details>
117118

118119
<div>
119120
<h3 class="text-base font-semibold mb-1">Captured Requests</h3>

src/popup/components/ApiPreview.ts

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,36 @@ export function renderApiPaths(
1313
const isReplaying = options.isReplaying === true;
1414
const rows = Object.entries(requests).sort(([a], [b]) => a.localeCompare(b));
1515

16+
function statusColorClass(status: number | undefined): string {
17+
if (typeof status !== 'number') return 'bg-gray-300 dark:bg-gray-600';
18+
if (status >= 500) return 'bg-red-500';
19+
if (status >= 400) return 'bg-amber-500';
20+
if (status >= 300) return 'bg-blue-500';
21+
if (status >= 200) return 'bg-green-500';
22+
return 'bg-gray-400';
23+
}
24+
1625
container.innerHTML = `
1726
<h4 class="font-semibold mb-1 text-xs">API Requests:</h4>
1827
${rows.length === 0 ? '<div class="text-xs opacity-70">No matching requests.</div>' : `
19-
<div class="overflow-auto border border-gray-300 dark:border-gray-600 rounded">
20-
<table class="w-full text-xs">
28+
<div class="border border-gray-300 dark:border-gray-600 rounded">
29+
<table class="w-full text-xs table-fixed">
30+
<colgroup>
31+
<col style="width: 2.2rem">
32+
<col style="width: 1.4rem">
33+
<col style="width: 3.2rem">
34+
<col>
35+
<col style="width: 1.6rem">
36+
<col style="width: 1.6rem">
37+
</colgroup>
2138
<thead class="bg-gray-200 dark:bg-gray-800">
2239
<tr>
23-
<th class="text-left p-1" title="Include this request when replaying">Replay</th>
40+
<th class="text-left p-1" title="Include this request when replaying"></th>
2441
<th class="text-left p-1" title="${isReplaying ? 'Green = matched at least once during replay; gray = not yet matched' : 'Match indicator is shown during replay'}">●</th>
2542
<th class="text-left p-1">Method</th>
2643
<th class="text-left p-1">Path</th>
27-
<th class="text-left p-1">Status</th>
28-
<th class="text-left p-1">Hits</th>
44+
<th class="text-left p-1" title="HTTP response status (color-coded). Click to edit.">St</th>
45+
<th class="text-left p-1" title="Replay hits for this recorded path">#</th>
2946
</tr>
3047
</thead>
3148
<tbody>
@@ -35,7 +52,8 @@ export function renderApiPaths(
3552
const path = url.pathname + url.search;
3653
const pathWithoutQuery = path.split('?')[0];
3754
const replayCount = requestHitCounts[pathWithoutQuery] || 0;
38-
const statusValue = typeof request.status === 'number' ? String(request.status) : '';
55+
const statusNum = typeof request.status === 'number' ? request.status : undefined;
56+
const statusText = statusNum !== undefined ? String(statusNum) : '—';
3957
const enabled = request.enabled !== false;
4058
const matchHits = matchedKeys[key] || 0;
4159
const matchColor = !isReplaying
@@ -48,26 +66,40 @@ export function renderApiPaths(
4866
: matchHits > 0
4967
? `Matched ${matchHits} time(s) during current replay`
5068
: 'Not matched yet during current replay';
69+
const fullUrlAttr = request.url.replace(/"/g, '&quot;');
70+
const pathAttr = path.replace(/"/g, '&quot;');
71+
const statusTitle = statusNum !== undefined
72+
? `Response status: ${statusNum} (click to edit)`
73+
: 'No status recorded (click to set)';
74+
const hitsTitle = replayCount > 0
75+
? `${replayCount} replay hit(s) on this path`
76+
: 'No replay hits yet on this path';
77+
const hitsClass = replayCount > 0
78+
? 'bg-green-500 text-white'
79+
: 'bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300';
5180
return `
5281
<tr class="border-t border-gray-200 dark:border-gray-700${isReplaying && matchHits > 0 ? ' bg-green-50 dark:bg-green-900/20' : ''}">
5382
<td class="p-1">
5483
<input type="checkbox" class="request-enabled-toggle" data-request-key="${encodeURIComponent(key)}" ${enabled ? 'checked' : ''}>
5584
</td>
5685
<td class="p-1 text-center ${matchColor}" title="${matchTitle}">●</td>
5786
<td class="p-1">${request.method}</td>
58-
<td class="p-1 cursor-pointer hover:text-blue-500" data-path="${path}">${path}</td>
59-
<td class="p-1">
60-
<input
61-
type="number"
62-
min="100"
63-
max="599"
64-
class="request-status-input w-16 px-1 py-0.5 rounded border border-gray-300 dark:border-gray-600 bg-transparent"
87+
<td class="p-1 cursor-pointer hover:text-blue-500 truncate" data-path="${pathAttr}" title="${fullUrlAttr}">${path}</td>
88+
<td class="p-1 text-center">
89+
<button
90+
type="button"
91+
class="request-status-icon inline-flex items-center justify-center w-5 h-5 rounded-full text-[9px] font-bold text-white ${statusColorClass(statusNum)}"
6592
data-request-key="${encodeURIComponent(key)}"
66-
value="${statusValue}"
67-
placeholder="-"
68-
>
93+
data-status="${statusNum ?? ''}"
94+
title="${statusTitle}"
95+
>${statusText}</button>
96+
</td>
97+
<td class="p-1 text-center">
98+
<span
99+
class="inline-flex items-center justify-center min-w-[1.1rem] h-5 px-1 rounded-full text-[10px] font-semibold ${hitsClass}"
100+
title="${hitsTitle}"
101+
>${replayCount}</span>
69102
</td>
70-
<td class="p-1">${replayCount}</td>
71103
</tr>
72104
`;
73105
})
@@ -98,37 +130,30 @@ export function renderApiPaths(
98130
});
99131
});
100132

101-
container.querySelectorAll('.request-status-input').forEach((element) => {
102-
const input = element as HTMLInputElement;
103-
const applyStatusUpdate = () => {
104-
const encodedKey = input.getAttribute('data-request-key');
133+
container.querySelectorAll('.request-status-icon').forEach((element) => {
134+
const button = element as HTMLButtonElement;
135+
button.addEventListener('click', (event) => {
136+
event.stopPropagation();
137+
const encodedKey = button.getAttribute('data-request-key');
105138
if (!encodedKey) {
106139
return;
107140
}
108-
109-
const trimmed = input.value.trim();
141+
const current = button.getAttribute('data-status') || '';
142+
const input = window.prompt('Set response status (100-599, empty to clear):', current);
143+
if (input === null) {
144+
return;
145+
}
146+
const trimmed = input.trim();
110147
if (trimmed.length === 0) {
111148
onUpdateStatus(decodeURIComponent(encodedKey), undefined);
112149
return;
113150
}
114-
115151
const parsed = Number(trimmed);
116152
if (!Number.isFinite(parsed) || parsed < 100 || parsed > 599) {
117-
input.classList.add('border-red-500');
153+
alert('Status must be a number between 100 and 599.');
118154
return;
119155
}
120-
121-
input.classList.remove('border-red-500');
122156
onUpdateStatus(decodeURIComponent(encodedKey), Math.floor(parsed));
123-
};
124-
125-
input.addEventListener('blur', applyStatusUpdate);
126-
input.addEventListener('keydown', (event) => {
127-
if (event.key === 'Enter') {
128-
event.preventDefault();
129-
applyStatusUpdate();
130-
input.blur();
131-
}
132157
});
133158
});
134159
}

src/popup/main.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ document.addEventListener('DOMContentLoaded', () => {
4646
const presetsList = document.getElementById('presetsList');
4747
const savePresetBtn = document.getElementById('savePreset');
4848
const closePresetsBtn = document.getElementById('closePresets');
49-
const replayStatsPanel = document.getElementById('replayStatsPanel');
5049
const latencyMsInput = document.getElementById('latencyMs');
5150
const latencyRangeInput = document.getElementById('latencyRange');
5251
const urlMappingsInput = document.getElementById('urlMappings') as HTMLTextAreaElement | null;
@@ -131,18 +130,11 @@ document.addEventListener('DOMContentLoaded', () => {
131130
recordingSelect.value = response.currentRecordingName;
132131
setActiveTab('replay');
133132
document.body.classList.add('is-replaying');
134-
replayStatsPanel?.classList.remove('hidden');
135-
void refreshReplayStats();
136133
}
137134
});
138135

139136
loadRecordings();
140137
void loadPresets();
141-
setInterval(() => {
142-
if (isReplaying) {
143-
void refreshReplayStats();
144-
}
145-
}, 1000);
146138

147139
recordButton.addEventListener('click', () => {
148140
if (isRecording) {
@@ -262,10 +254,8 @@ document.addEventListener('DOMContentLoaded', () => {
262254
isReplaying = true;
263255
updateButtonStates();
264256
updateStatusIndicator();
265-
replayStatsPanel.classList.remove('hidden');
266257
document.body.classList.add('is-replaying');
267258
setActiveTab('replay');
268-
void refreshReplayStats();
269259
} else {
270260
alert('Failed to start replaying: ' + (response ? response.error : 'Unknown error'));
271261
}
@@ -281,7 +271,6 @@ document.addEventListener('DOMContentLoaded', () => {
281271
isReplaying = false;
282272
updateButtonStates();
283273
updateStatusIndicator();
284-
replayStatsPanel.classList.add('hidden');
285274
document.body.classList.remove('is-replaying');
286275
console.log('Replaying stopped successfully');
287276
} else {
@@ -816,22 +805,6 @@ document.addEventListener('DOMContentLoaded', () => {
816805
await loadPresets();
817806
});
818807

819-
async function refreshReplayStats() {
820-
chrome.runtime.sendMessage({ action: 'getReplayStats' }, (response) => {
821-
if (!response?.success || !response.replayStats) {
822-
return;
823-
}
824-
const stats = response.replayStats;
825-
const unmatched = stats.unmatched || 0;
826-
const recent = (stats.unmatchedUrls || []).slice(-5).join(' | ');
827-
replayStatsPanel.innerHTML = `
828-
<div><strong>Replay stats</strong> <span class="opacity-60 text-xs">(only requests within the recording's URL filter are counted)</span></div>
829-
<div>Matched: <strong>${stats.matched || 0}</strong></div>
830-
<div class="opacity-70 text-xs">Unmatched (in-scope only): ${unmatched}</div>
831-
${unmatched > 0 && recent ? `<div class="opacity-60 text-xs">Recent unmatched: ${recent}</div>` : ''}
832-
`;
833-
});
834-
}
835808

836809

837810
exportImportDropdown.addEventListener('click', () => {

0 commit comments

Comments
 (0)