Skip to content

Commit 2416e76

Browse files
committed
request section enhancements
1 parent 83ae5d5 commit 2416e76

39 files changed

Lines changed: 1451 additions & 227 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "restbro",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"description": "A modern API testing tool similar to Postman and Insomnia",
55
"main": "dist/main/main/index.js",
66
"scripts": {

src/renderer/components/environments/environment-manager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Environment } from '../../../shared/types';
22
import { EnvironmentDialogs } from './environment-dialogs';
33

44
import { iconHtml } from '../../utils/icons';
5+
import { attachThemedSelect } from '../../utils/themed-select';
56

67
export class EnvironmentManager {
78
private environments: Environment[] = [];
@@ -146,6 +147,9 @@ export class EnvironmentManager {
146147
container.appendChild(select);
147148
container.appendChild(manageBtn);
148149

150+
// Replace the native (OS-themed) option popup with a themed dropdown.
151+
attachThemedSelect(select);
152+
149153
// Append to header-left section
150154
headerLeft.appendChild(container);
151155

src/renderer/components/history-panel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export class HistoryPanel {
156156
<span class="history-row__method method-${item.request.method.toLowerCase()}">${item.request.method}</span>
157157
<span class="history-row__url" title="${this.escapeAttr(item.request.url)}">${this.escapeText(item.request.url || '\u2013')}</span>
158158
<span class="history-row__status ${statusClass}">${status}</span>
159-
<span class="history-row__time">${ts.toLocaleTimeString()}</span>
159+
<span class="history-row__time">${ts.toLocaleTimeString(undefined, { hourCycle: 'h12' })}</span>
160160
</div>
161161
`;
162162
})

src/renderer/components/loadtest/LoadTestForm.ts

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -234,25 +234,6 @@ export class LoadTestForm {
234234
</div>
235235
<small class="form-help">1 second to 24 hours maximum</small>
236236
</div>
237-
</div>
238-
</div>
239-
240-
<div class="form-section compact compact-advanced">
241-
<h3>Advanced Options</h3>
242-
<div class="form-grid two-column">
243-
<div class="form-field">
244-
<label class="checkbox-label">
245-
<input type="checkbox" id="follow-redirects" checked>
246-
<span class="checkbox-text">Follow Redirects</span>
247-
</label>
248-
</div>
249-
250-
<div class="form-field">
251-
<label class="checkbox-label">
252-
<input type="checkbox" id="insecure-tls">
253-
<span class="checkbox-text">Allow Insecure TLS</span>
254-
</label>
255-
</div>
256237
257238
<div class="form-field">
258239
<label for="timeout-input">Request Timeout (ms)</label>
@@ -265,7 +246,10 @@ export class LoadTestForm {
265246
266247
<div class="form-column">
267248
<div class="form-section compact">
268-
<h3>Target Request</h3>
249+
<div class="section-header">
250+
<h3>Target Request</h3>
251+
<button id="start-test-btn" class="btn btn-primary section-header-action" type="button">Start Load Test</button>
252+
</div>
269253
<div class="target-selector">
270254
<div class="radio-group">
271255
<label class="radio-label">
@@ -300,10 +284,6 @@ export class LoadTestForm {
300284
</div>
301285
</div>
302286
</div>
303-
304-
<div class="form-actions form-actions-inline">
305-
<button id="start-test-btn" class="btn btn-primary" type="button">Start Load Test</button>
306-
</div>
307287
</div>
308288
</div>
309289
@@ -584,12 +564,6 @@ export class LoadTestForm {
584564
const targetType = this.container.querySelector(
585565
'input[name="target-type"]:checked'
586566
) as HTMLInputElement;
587-
const followRedirects = this.container.querySelector(
588-
'#follow-redirects'
589-
) as HTMLInputElement;
590-
const insecureTLS = this.container.querySelector(
591-
'#insecure-tls'
592-
) as HTMLInputElement;
593567
const timeout = this.container.querySelector(
594568
'#timeout-input'
595569
) as HTMLInputElement;
@@ -620,8 +594,8 @@ export class LoadTestForm {
620594
durationSec,
621595
target,
622596
environmentId,
623-
followRedirects: followRedirects.checked,
624-
insecureTLS: insecureTLS.checked,
597+
followRedirects: true,
598+
insecureTLS: false,
625599
requestTimeoutMs: parseInt(timeout.value),
626600
};
627601
}
@@ -715,12 +689,6 @@ export class LoadTestForm {
715689
const durationUnit = this.container.querySelector(
716690
'#duration-unit'
717691
) as HTMLSelectElement;
718-
const followRedirects = this.container.querySelector(
719-
'#follow-redirects'
720-
) as HTMLInputElement;
721-
const insecureTLS = this.container.querySelector(
722-
'#insecure-tls'
723-
) as HTMLInputElement;
724692
const timeout = this.container.querySelector(
725693
'#timeout-input'
726694
) as HTMLInputElement;
@@ -735,8 +703,6 @@ export class LoadTestForm {
735703
durationUnit.value = 'seconds';
736704
}
737705

738-
followRedirects.checked = config.followRedirects !== false;
739-
insecureTLS.checked = config.insecureTLS === true;
740706
timeout.value = (config.requestTimeoutMs || 30000).toString();
741707

742708
if (config.target.kind === 'collection') {

src/renderer/components/request/MonacoJsonEditor.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import * as monaco from 'monaco-editor';
7+
import { forceInitialViewportTokenization } from './monaco-tokenization';
78

89
export interface MonacoJsonEditorOptions {
910
container: HTMLElement;
@@ -135,6 +136,7 @@ export class MonacoJsonEditor {
135136
theme: 'restbro-json',
136137
automaticLayout: true,
137138
minimap: { enabled: false },
139+
overviewRulerBorder: false,
138140
scrollBeyondLastLine: false,
139141
fontSize: 12,
140142
lineNumbers: 'on',
@@ -146,9 +148,9 @@ export class MonacoJsonEditor {
146148
fontFamily:
147149
"'SF Mono', 'Cascadia Code', Monaco, Menlo, Consolas, 'Courier New', monospace",
148150
letterSpacing: -0.3,
149-
glyphMargin: true,
151+
glyphMargin: false,
150152
lineDecorationsWidth: 0,
151-
lineNumbersMinChars: 3,
153+
lineNumbersMinChars: 1,
152154
wordWrap: 'on',
153155
tabSize: 2,
154156
insertSpaces: true,
@@ -162,6 +164,10 @@ export class MonacoJsonEditor {
162164
},
163165
});
164166

167+
// Tokenize the initial viewport synchronously so the first paint is already
168+
// themed (avoids Monaco's white-then-colored syntax-highlight flash).
169+
forceInitialViewportTokenization(this.editor);
170+
165171
// Listen to content changes
166172
this.editor.onDidChangeModelContent(() => {
167173
const newValue = this.editor?.getValue() || '';
@@ -325,6 +331,29 @@ export class MonacoJsonEditor {
325331
this.editor.getAction('actions.find')?.run();
326332
}
327333

334+
/**
335+
* Toggle Monaco's built-in find widget: open + focus it when hidden, close
336+
* it when it's already showing. Reads the find controller's live state so it
337+
* stays in sync no matter how the widget was opened (icon, Cmd/Ctrl+F, Esc).
338+
*/
339+
public toggleFind(): void {
340+
if (!this.editor) return;
341+
const findController = this.editor.getContribution(
342+
'editor.contrib.findController'
343+
) as unknown as {
344+
getState?: () => { isRevealed?: boolean } | undefined;
345+
closeFindWidget?: () => void;
346+
} | null;
347+
348+
if (findController?.getState?.()?.isRevealed) {
349+
findController.closeFindWidget?.();
350+
return;
351+
}
352+
353+
this.editor.focus();
354+
this.editor.getAction('actions.find')?.run();
355+
}
356+
328357
/** Find matches in the model (for external search bars) */
329358
public findMatches(query: string): monaco.editor.FindMatch[] {
330359
if (!this.editor || !query) return [];

src/renderer/components/request/MonacoXmlEditor.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import * as monaco from 'monaco-editor';
7+
import { forceInitialViewportTokenization } from './monaco-tokenization';
78

89
export interface MonacoXmlEditorOptions {
910
container: HTMLElement;
@@ -48,6 +49,7 @@ export class MonacoXmlEditor {
4849
theme: 'restbro-json',
4950
automaticLayout: true,
5051
minimap: { enabled: false },
52+
overviewRulerBorder: false,
5153
scrollBeyondLastLine: false,
5254
fontSize: 12,
5355
lineNumbers: 'on',
@@ -60,7 +62,7 @@ export class MonacoXmlEditor {
6062
letterSpacing: -0.3,
6163
glyphMargin: false,
6264
lineDecorationsWidth: 0,
63-
lineNumbersMinChars: 3,
65+
lineNumbersMinChars: 1,
6466
wordWrap: 'on',
6567
tabSize: 2,
6668
insertSpaces: true,
@@ -69,6 +71,10 @@ export class MonacoXmlEditor {
6971
padding: { top: 12, bottom: 12 },
7072
});
7173

74+
// Tokenize the initial viewport synchronously so the first paint is already
75+
// themed (avoids Monaco's white-then-colored syntax-highlight flash).
76+
forceInitialViewportTokenization(this.editor);
77+
7278
this.editor.onDidChangeModelContent(() => {
7379
this.onChange(this.editor?.getValue() || '');
7480
});

src/renderer/components/request/RequestBodyEditor.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,16 @@ export class RequestBodyEditor {
310310
this.monacoEditor = null;
311311
}
312312

313+
// Dispose any lingering Monaco XML editor too. Switching SOAP (XML) -> REST
314+
// (JSON) lands here; without this the old XML editor would stay mounted in
315+
// the same container and stack on top of the new JSON editor, breaking
316+
// layout and syntax colouring. Symmetric with switchToMonacoXmlEditor /
317+
// switchToTextareaEditor, which already dispose both.
318+
if (this.monacoXmlEditor) {
319+
this.monacoXmlEditor.dispose();
320+
this.monacoXmlEditor = null;
321+
}
322+
313323
// Get current value from textarea if no initial value provided
314324
const valueToSet = initialValue || textarea?.value || '';
315325

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Shared helper to eliminate Monaco's "white then colored" syntax-highlight flash.
3+
*
4+
* Monaco defers syntactic tokenization to a background scheduler, so a freshly
5+
* created editor paints its text with the default (white) foreground until that
6+
* async pass runs. Under main-thread contention (e.g. switching requests, or a
7+
* large response arriving) that pass can be queued behind other work and lag by
8+
* up to ~1s, producing a visible flash where JSON/XML keys start white and only
9+
* later pick up the theme color.
10+
*
11+
* Forcing tokenization of the initial viewport synchronously — in the same task
12+
* as editor creation, before the browser paints — makes the first paint already
13+
* colored. It is bounded so very large documents don't block the UI thread; the
14+
* remaining lines tokenize lazily in the background as the user scrolls.
15+
*/
16+
17+
import type * as monaco from 'monaco-editor';
18+
19+
/**
20+
* `forceTokenization` lives on Monaco's runtime model (ITokenizationTextModelPart)
21+
* but is intentionally omitted from the public `monaco.d.ts`. Narrow the shape so
22+
* we stay type-safe (and feature-detect it) at this boundary instead of `any`.
23+
*/
24+
type TokenizableModel = monaco.editor.ITextModel & {
25+
tokenization?: {
26+
forceTokenization?: (lineNumber: number) => void;
27+
};
28+
};
29+
30+
/**
31+
* Upper bound on lines tokenized synchronously. A viewport is only tens of lines;
32+
* a few hundred covers the initial view plus scroll headroom without freezing the
33+
* UI thread on very large payloads.
34+
*/
35+
const MAX_SYNC_TOKENIZE_LINES = 500;
36+
37+
/**
38+
* Synchronously tokenize the initial viewport of a just-created editor so its
39+
* first paint is already themed. Safe no-op if the runtime API is unavailable.
40+
*/
41+
export function forceInitialViewportTokenization(
42+
editor: monaco.editor.IStandaloneCodeEditor | null
43+
): void {
44+
const model = editor?.getModel() as TokenizableModel | null | undefined;
45+
if (!model?.tokenization?.forceTokenization) return;
46+
47+
const lines = Math.min(model.getLineCount(), MAX_SYNC_TOKENIZE_LINES);
48+
model.tokenization.forceTokenization(lines);
49+
}

src/renderer/components/request/request-form-handler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { setupAutocomplete } from './variable-autocomplete';
88
import { VariableContextHandler } from './variable-context-handler';
99
import { FormUIHandler } from './form-ui-handler';
10+
import { attachThemedSelect } from '../../utils/themed-select';
1011

1112
/**
1213
* Main handler for the request form - coordinates between variable context,
@@ -33,6 +34,9 @@ export class RequestFormHandler {
3334
// Apply initial method color class
3435
this.uiHandler.updateMethodSelectColor(methodSelect);
3536

37+
// Replace the native (OS-themed) option popup with a themed dropdown.
38+
attachThemedSelect(methodSelect);
39+
3640
methodSelect.addEventListener('change', () => {
3741
this.onRequestUpdate({
3842
method: methodSelect.value as HttpMethod,

0 commit comments

Comments
 (0)