Skip to content

Commit 2cd871c

Browse files
committed
Allow output Presets to be saved and used
1 parent 8ae5770 commit 2cd871c

14 files changed

Lines changed: 239 additions & 94 deletions

.github/workflows/npm-publish.yml

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ on:
44
release:
55
types: [created]
66

7+
permissions:
8+
id-token: write
9+
contents: read
10+
711
jobs:
812
build:
913
runs-on: ubuntu-latest
1014
steps:
11-
- uses: actions/checkout@v3
12-
- uses: actions/setup-node@v3
15+
- uses: actions/checkout@v6
16+
- uses: actions/setup-node@v6
1317
with:
14-
node-version: 16
18+
node-version: 24
19+
registry-url: 'https://registry.npmjs.org'
20+
package-manager-cache: false
1521
- run: npm ci
1622
# - run: npm test
1723
- run: npm run build
@@ -20,13 +26,12 @@ jobs:
2026
needs: build
2127
runs-on: ubuntu-latest
2228
steps:
23-
- uses: actions/checkout@v3
24-
- uses: actions/setup-node@v3
29+
- uses: actions/checkout@v6
30+
- uses: actions/setup-node@v6
2531
with:
26-
node-version: 16
27-
registry-url: https://registry.npmjs.org/
32+
node-version: 24
33+
registry-url: 'https://registry.npmjs.org'
34+
package-manager-cache: false
2835
- run: npm ci
29-
- run: npm run build
36+
- run: npm run build --if-present
3037
- run: npm publish
31-
env:
32-
NODE_AUTH_TOKEN: ${{secrets.npm_token}}

CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# v1.6.0
2+
3+
**Date:** 2026/05/26
4+
5+
### New features
6+
7+
- Add stored presets for commonly-used Output settings ([#24](https://github.com/jreyesr/insomnia-plugin-batch-requests/discussions/24)) ([docs](./README.md#output-presets))
8+
19
# v1.5.1
210

311
**Date:** 2025/08/28
@@ -13,7 +21,7 @@
1321

1422
### New features
1523

16-
- Add a new template tag that can send different files on each request
24+
- Add a new template tag that can send different files on each request ([docs](./README.md#sending-files))
1725

1826
# v1.4.0
1927

@@ -23,7 +31,7 @@
2331

2432
- Add a way to read outputs (that will be written back to the CSV) from the response's headers, status code and request
2533
time, in addition to the response body as
26-
JSON ([#13](https://github.com/jreyesr/insomnia-plugin-batch-requests/pull/13))
34+
JSON ([#13](https://github.com/jreyesr/insomnia-plugin-batch-requests/pull/13)) ([docs](./README.md#sources-of-output-data))
2735

2836
# v1.3.0
2937

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,22 @@ Since `v1.4.0`, it's possible to extract data from several places in the respons
115115

116116
The source of data is chosen in the center dropdown of each Output. If required, the right-hand text field will appear and must contain something, otherwise it'll be hidden.
117117

118+
### Output presets
119+
120+
Since `v1.6.0`, it's possible to store and load different settings for the Outputs section (see [#24](https://github.com/jreyesr/insomnia-plugin-batch-requests/discussions/24)).
121+
For example, if you usually load CSV files that have the same columns, call the same Request and extract the same fields
122+
to the same CSV columns, that can be stored into a Preset for subsequent reuse. This is controlled by the following buttons:
123+
124+
![img.png](images/presets.png)
125+
126+
* To store a Preset for later, first configure the desired Outputs (CSV column + source + JSONPath query or header name).
127+
Then, click the Save preset button and give the preset a name. This will record the currently configured Outputs
128+
* To load a Preset, choose a CSV file (otherwise the preset picker won't be enabled), and then select the desired preset
129+
from the list. If you haven't saved a preset, the picker won't activate. Once you choose a preset, the configuration
130+
in the Outputs will be overwritten with what was stored in the preset.
131+
* To delete a Preset, first load it with the picker, and then click the Delete button to the right of the preset picker.
132+
If you haven't chosen a preset, the button will be disabled.
133+
118134
### Extra settings
119135

120136
There are two additional options that can be set when sending batch requests. They appear in the **Run Config** section, above the progress bar:

__tests__/BatchDialog.test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import BatchDialog from '../components/BatchDialog'
66

77

88
const mockAlert = jest.fn();
9+
const mockAllItems = jest.fn();
910
const mockGetItem = jest.fn();
1011
const mockSetItem = jest.fn();
1112
const mockRemoveItem = jest.fn();
@@ -19,6 +20,7 @@ utils.selectFile = jest.fn();
1920

2021
beforeEach(() => {
2122
mockGetItem.mockReturnValue(JSON.stringify({defaultDelay: 0.1}));
23+
mockAllItems.mockReturnValue([])
2224
utils.readCsv.mockImplementation(originalReadCsv);
2325
utils.selectFile.mockImplementation(originalSelectFile);
2426
})
@@ -30,6 +32,7 @@ afterEach(() => {
3032
const mockContext = {
3133
store: {
3234
hasItem: jest.fn(k => true),
35+
all: mockAllItems,
3336
getItem: mockGetItem,
3437
setItem: mockSetItem,
3538
removeItem: mockRemoveItem,
@@ -67,7 +70,7 @@ it('does not enable the buttons on load', async () => {
6770
await waitFor(() => expect(delayField).toHaveValue(0.1));
6871

6972
expect(getByText(/run!/i)).toBeDisabled();
70-
expect(getByText(/save/i)).toBeDisabled();
73+
expect(getByText(/^save$/i)).toBeDisabled();
7174
});
7275

7376
it('prompts the user to choose a file', async () => {

__tests__/BatchDialogSettings.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ it('rejects negative numbers', async () => {
6363

6464
await user.clear(delayField);
6565
await user.type(delayField, "-0.1")
66-
expect(delayField).toHaveValue(0.1);
66+
expect(delayField).toHaveValue(0.1); // The field rejects the - so it becomes 0.1
6767
});
6868

6969
it("doesn't enable the Save button when loading", async () => {

__tests__/OutputFieldsChooser.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import OutputFieldsChooser from '../components/OutputFieldsChooser'
55

66
it('displays default UI', () => {
77
const {container} = render(
8-
<OutputFieldsChooser colNames={["a", "b"]} />,
8+
<OutputFieldsChooser colNames={["a", "b"]} presets={[]}/>,
99
);
1010

1111
expect(container).toHaveTextContent("Add");
@@ -14,7 +14,7 @@ it('displays default UI', () => {
1414
it('can add a field', async () => {
1515
const user = userEvent.setup();
1616
const {getByText, getAllByTestId} = render(
17-
<OutputFieldsChooser colNames={["a", "b"]} onChange={jest.fn()} />,
17+
<OutputFieldsChooser colNames={["a", "b"]} onChange={jest.fn()} presets={[]}/>,
1818
);
1919

2020
await user.click(getByText("Add"));
@@ -26,7 +26,7 @@ it('notifies parent when field is added', async () => {
2626
const onChange = jest.fn();
2727
const user = userEvent.setup();
2828
const {getByText} = render(
29-
<OutputFieldsChooser colNames={["a", "b"]} onChange={onChange} />,
29+
<OutputFieldsChooser colNames={["a", "b"]} onChange={onChange} presets={[]}/>,
3030
);
3131

3232
await user.click(getByText("Add"));
@@ -38,7 +38,7 @@ it('notifies parent when field is deleted', async () => {
3838
const onChange = jest.fn();
3939
const user = userEvent.setup();
4040
const {getByText, getByTestId, queryAllByTestId} = render(
41-
<OutputFieldsChooser colNames={["a", "b"]} onChange={onChange} />,
41+
<OutputFieldsChooser colNames={["a", "b"]} onChange={onChange} presets={[]}/>,
4242
);
4343
await user.click(getByText("Add"));
4444
onChange.mockClear();
@@ -54,7 +54,7 @@ it('notifies parent when field is updated', async () => {
5454
const onChange = jest.fn();
5555
const user = userEvent.setup();
5656
const {getByText, getByTestId} = render(
57-
<OutputFieldsChooser colNames={["a", "b"]} onChange={onChange} />,
57+
<OutputFieldsChooser colNames={["a", "b"]} onChange={onChange} presets={[]}/>,
5858
);
5959
await user.click(getByText("Add"));
6060
onChange.mockClear();
@@ -70,7 +70,7 @@ it("tracks the output's context", async () => {
7070
const onChange = jest.fn();
7171
const user = userEvent.setup();
7272
const {getByText, getByTestId} = render(
73-
<OutputFieldsChooser colNames={["a", "b"]} onChange={onChange} />,
73+
<OutputFieldsChooser colNames={["a", "b"]} onChange={onChange} presets={[]}/>,
7474
);
7575
await user.click(getByText("Add"));
7676
onChange.mockClear();

components/BatchDialog.js

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
import React, { useCallback, useEffect, useState } from 'react';
2-
import { stringify } from 'csv-stringify/sync';
3-
4-
import { writeFile, readCsv, readSettings, makeRequest } from '../utils';
1+
import React, {useCallback, useEffect, useState} from 'react';
2+
import {stringify} from 'csv-stringify/sync';
3+
4+
import {
5+
writeFile,
6+
readCsv,
7+
readSettings,
8+
makeRequest,
9+
getAllPresets,
10+
savePreset,
11+
getPreset,
12+
deletePreset
13+
} from '../utils';
514
import Queue from "queue-promise"
615

716
import SampleTable from './SampleTable';
@@ -23,15 +32,28 @@ export default function BatchDialog({context, request}) {
2332
const [delay, setDelay] = useState(0);
2433
const [parallelism, setParallelism] = useState(1);
2534

35+
const [presets, setPresets] = useState([])
36+
const [loadedPreset, setLoadedPreset] = useState(null)
37+
2638
// Load default delay from plugin settings on mount
2739
useEffect(() => {
2840
async function loadSettings() {
2941
const settings = await readSettings(context.store);
3042
setDelay(parseFloat(settings.defaultDelay ?? 0))
3143
}
44+
3245
loadSettings();
3346
}, [])
3447

48+
async function updatePresets() {
49+
setPresets(await getAllPresets(context.store))
50+
}
51+
52+
// fill up the presets on component load
53+
useEffect(() => {
54+
updatePresets()
55+
}, [])
56+
3557
const onFileChosen = async (path) => {
3658
setCsvPath(path);
3759
const {headers, results} = await readCsv(path);
@@ -57,10 +79,10 @@ export default function BatchDialog({context, request}) {
5779
interval: 0,
5880
start: false,
5981
});
60-
82+
6183
// Hook a promise to the queue's "end" event
6284
const isDone = new Promise(resolve => queue.on("end", resolve))
63-
for(const [i, row] of csvData.entries()) {
85+
for (const [i, row] of csvData.entries()) {
6486
queue.enqueue(() => makeRequest(context, request, i, row, delay, outputConfig, setSent, setCsvData))
6587
}
6688

@@ -76,10 +98,28 @@ export default function BatchDialog({context, request}) {
7698
setOutputConfig(x)
7799
}
78100

79-
console.debug("canRun", outputConfig.map(x => `${x.name} - ${x.jsonPath} - ${x.context} - ${Boolean(x.name && (x.jsonPath || ["statusCode", "reqTime"].includes(x.context))) ? "T" : "F"}`))
101+
const onSavePresetButtonClicked = async () => {
102+
const presetName = await context.app.prompt("Preset name", {
103+
label: "Provide a name for the new saved preset",
104+
defaultValue: `New preset ${new Date().toDateString()}`,
105+
})
106+
await savePreset(context.store, presetName, outputConfig)
107+
updatePresets()
108+
}
109+
110+
const onPresetSelectedForLoad = async ({target: {value}}) => {
111+
const preset = await getPreset(context.store, value)
112+
setOutputConfig(preset.outputs)
113+
setLoadedPreset(preset.outputs) // this notifies the child OutputFieldsChooser via an effect
114+
}
115+
116+
const onDeletePresetButtonClicked = async (presetName) => {
117+
await deletePreset(context.store, presetName)
118+
updatePresets()
119+
}
80120

81121
const onChangeDelay = ({target: {value}}) => {
82-
if(value < 0) return;
122+
if (value < 0) return;
83123
setDelay(value)
84124
}
85125

@@ -89,21 +129,26 @@ export default function BatchDialog({context, request}) {
89129
</FormRow>
90130
{csvData.length ? (
91131
<FormRow label="Sample data" insideLabel={false}>
92-
<SampleTable columnNames={csvHeaders} data={csvData} />
132+
<SampleTable columnNames={csvHeaders} data={csvData}/>
93133
</FormRow>
94134
) : <p>Choose a file above to preview it!</p>}
95135

96-
<OutputFieldsChooser colNames={csvHeaders} onChange={onChangeOutputFields} />
136+
<OutputFieldsChooser colNames={csvHeaders} presets={presets} onChange={onChangeOutputFields}
137+
onSavePreset={onSavePresetButtonClicked} onLoadPreset={onPresetSelectedForLoad}
138+
loadedPreset={loadedPreset}
139+
onDeletePreset={onDeletePresetButtonClicked}/>
97140

98141
<FormRow label="Run config">
99-
<DelaySelector value={delay} onChange={onChangeDelay}/>
100-
<ParallelSelector value={parallelism} onChange={({target: {value}}) => setParallelism(value)}/>
142+
<div style={{display: "flex", flexDirection: "row", gap: "1em"}}>
143+
<DelaySelector value={delay} onChange={onChangeDelay}/>
144+
<ParallelSelector value={parallelism} onChange={({target: {value}}) => setParallelism(value)}/>
145+
</div>
101146
</FormRow>
102147

103148
<FormRow label="Progress">
104-
<ProgressBar bgcolor="#ff6b6b" completed={sent * 100 / totalRequests} text={`${sent}/${totalRequests}`} />
149+
<ProgressBar bgcolor="#ff6b6b" completed={sent * 100 / totalRequests} text={`${sent}/${totalRequests}`}/>
105150
</FormRow>
106-
151+
107152
<ActionButton title="Run!" icon="fa-person-running" onClick={onRun} disabled={!canRun}/>
108153
<ActionButton title="Save" icon="fa-save" onClick={saveCsv} disabled={!canRun} style={{marginLeft: 5}}/>
109154
</React.Fragment>);

components/DelaySelector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export default function DelaySelector({value, onChange}) {
2-
return <label>
2+
return <label style={{flexGrow: 1}}>
33
Delay in seconds
44
<input
55
type="number"

components/OutputFieldsChooser.js

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import React, { useCallback, useState } from 'react';
1+
import React, {useCallback, useEffect, useState} from 'react';
22

33
import ActionButton from './ActionButton';
44
import FormRow from './FormRow';
55
import OutputField from './OutputField';
66

7-
export default function OutputFieldsChooser({colNames, onChange}) {
7+
export default function OutputFieldsChooser({colNames, presets, onChange, onSavePreset, onLoadPreset, loadedPreset, onDeletePreset}) {
88
const [outputs, setOutputs] = useState([]);
99

1010
const addNew = useCallback(() => {
@@ -20,22 +20,50 @@ export default function OutputFieldsChooser({colNames, onChange}) {
2020
setOutputs(cloned);
2121
onChange(cloned);
2222
}, [outputs, setOutputs, onChange]);
23-
23+
2424
const deleteField = useCallback((i) => () => {
2525
let cloned = JSON.parse(JSON.stringify(outputs))
2626
cloned.splice(i, 1); // Remove one element at position i
2727
setOutputs(cloned);
2828
onChange(cloned);
2929
}, [outputs, setOutputs, onChange]);
3030

31+
// runs every time parent loads a new preset
32+
useEffect(() => {
33+
if (!loadedPreset) return
34+
35+
setOutputs(loadedPreset)
36+
}, [setOutputs, loadedPreset]);
37+
38+
const [selectedPreset, setSelectedPreset] = useState("")
39+
const onSelectedPresetChange = useCallback((ev) => {
40+
setSelectedPreset(ev.target.value)
41+
onLoadPreset(ev)
42+
}, [setSelectedPreset, onLoadPreset])
43+
3144
return <FormRow label="Outputs">
32-
{outputs.map((o, i) =>
33-
<OutputField key={i}
34-
options={colNames}
35-
name={o.name} context={o.context} jsonPath={o.jsonPath}
36-
onChange={updateField(i)} onDelete={deleteField(i)}
45+
{outputs.map((o, i) =>
46+
<OutputField key={i}
47+
options={colNames}
48+
name={o.name} context={o.context} jsonPath={o.jsonPath}
49+
onChange={updateField(i)} onDelete={deleteField(i)}
3750
/>
3851
)}
39-
<ActionButton title="Add" icon="fa-plus" onClick={addNew}/>
52+
53+
<div style={{display: "flex", flexDirection: "row", gap: "1em"}}>
54+
<ActionButton title="Add" icon="fa-plus" onClick={addNew}/>
55+
<ActionButton title="Save preset" icon="fa-upload" onClick={onSavePreset} disabled={outputs.length === 0}/>
56+
<select
57+
value={selectedPreset}
58+
onChange={onSelectedPresetChange}
59+
data-testid="loadPreset"
60+
disabled={presets.length === 0 || colNames.length === 0}
61+
style={{width: "auto"}}
62+
>
63+
<option value="">Load preset...</option>
64+
{presets.map(o => <option key={o.name} value={o.name}>{o.name}</option>)}
65+
</select>
66+
<ActionButton title="" icon="fa-trash" onClick={() => onDeletePreset(selectedPreset)} disabled={presets.length === 0 || selectedPreset === ""}/>
67+
</div>
4068
</FormRow>
4169
}

0 commit comments

Comments
 (0)