mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
71 Commits
v2.8.0
...
oauth2_add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0b3b1ad4b | ||
|
|
f9d29f821c | ||
|
|
4454f4f7b8 | ||
|
|
c4cacf284b | ||
|
|
311a232968 | ||
|
|
97aff84157 | ||
|
|
ef12401d2e | ||
|
|
8dde2701f4 | ||
|
|
cd00c21781 | ||
|
|
efb2e83ad9 | ||
|
|
e5a608f962 | ||
|
|
3e3e2e0563 | ||
|
|
8d1f292b83 | ||
|
|
953024dae7 | ||
|
|
146c8462ea | ||
|
|
77c96c4821 | ||
|
|
060c613aa1 | ||
|
|
b804ff6dfd | ||
|
|
ce0fc08500 | ||
|
|
fc53dd88e2 | ||
|
|
c2063ce71b | ||
|
|
acc8e9deba | ||
|
|
bf145a71f5 | ||
|
|
7de3e6e3ff | ||
|
|
c33bf9f88e | ||
|
|
ceab0b4dc1 | ||
|
|
7ccbea7ced | ||
|
|
51163a7282 | ||
|
|
1f0b1cb5a7 | ||
|
|
ec151ac2e5 | ||
|
|
c4356411c9 | ||
|
|
84cca6f92b | ||
|
|
f1f1c1fe5b | ||
|
|
20ffae86e4 | ||
|
|
d031687ee9 | ||
|
|
86901c1e89 | ||
|
|
7cb80abdfc | ||
|
|
99c8fd5240 | ||
|
|
0e81c14b96 | ||
|
|
110d93a983 | ||
|
|
e2ecd7bfa9 | ||
|
|
98c09db820 | ||
|
|
8938b04faf | ||
|
|
81b5e3c539 | ||
|
|
ec51ebba45 | ||
|
|
31027cb2e0 | ||
|
|
60a0a32743 | ||
|
|
aae4f03fdf | ||
|
|
5150251698 | ||
|
|
b571c1a1a5 | ||
|
|
62151330f2 | ||
|
|
780beb832e | ||
|
|
29e6470f7a | ||
|
|
78b8b7f6e4 | ||
|
|
63f5108dfd | ||
|
|
6daaf90667 | ||
|
|
0fec0003f2 | ||
|
|
4badee903a | ||
|
|
b20de42598 | ||
|
|
e5d30c2920 | ||
|
|
c2271945c4 | ||
|
|
0e6c36f62c | ||
|
|
6d38f2b38c | ||
|
|
84ef5b1044 | ||
|
|
3c85f44ed9 | ||
|
|
dd7ff97090 | ||
|
|
b9c2a42344 | ||
|
|
f06eb86574 | ||
|
|
84cd91b798 | ||
|
|
b1911d80e9 | ||
|
|
3c0d0c95ea |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno
|
||||
* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno
|
||||
|
||||
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
npm run build --workspace=packages/bruno-filestore
|
||||
|
||||
- name: Lint Check
|
||||
run: npm run lint
|
||||
@@ -80,6 +81,7 @@ jobs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
npm run build --workspace=packages/bruno-filestore
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
@@ -125,6 +127,7 @@ jobs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
npm run build:bruno-filestore
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
|
||||
@@ -2,4 +2,4 @@ import { test, expect } from '../../playwright';
|
||||
|
||||
test('Check if the logo on top left is visible', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
43
e2e-tests/002-cookies-tests/001-cookie-persistence.spec.ts
Normal file
43
e2e-tests/002-cookies-tests/001-cookie-persistence.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('should persist cookies across app restarts', async ({ createTmpDir, launchElectronApp }) => {
|
||||
// Create a temporary user-data directory so we control where the cookies store file is written.
|
||||
const userDataPath = await createTmpDir('cookie-persistence');
|
||||
|
||||
const app1 = await launchElectronApp({ userDataPath });
|
||||
const page1 = await app1.firstWindow();
|
||||
await page1.waitForSelector('[data-trigger="cookies"]');
|
||||
|
||||
// Open Cookies modal via the status-bar button.
|
||||
await page1.click('[data-trigger="cookies"]');
|
||||
|
||||
// When no cookies are present the modal shows a centred "Add Cookie" button.
|
||||
await page1.getByRole('button', { name: /Add Cookie/i }).click();
|
||||
|
||||
// Fill out the form.
|
||||
await page1.fill('input[name="domain"]', 'example.com');
|
||||
await page1.fill('input[name="path"]', '/');
|
||||
await page1.fill('input[name="key"]', 'session');
|
||||
await page1.fill('input[name="value"]', 'abc123');
|
||||
await page1.check('input[name="secure"]');
|
||||
await page1.check('input[name="httpOnly"]');
|
||||
|
||||
await page1.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page1.getByText('example.com')).toBeVisible();
|
||||
|
||||
await app1.close();
|
||||
|
||||
// Second launch – verify the cookie was persisted and re-loaded
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await app2.firstWindow();
|
||||
|
||||
// Open the Cookies modal again.
|
||||
await page2.waitForSelector('[data-trigger="cookies"]');
|
||||
await page2.click('[data-trigger="cookies"]');
|
||||
|
||||
// The domain we added earlier should still be present.
|
||||
await expect(page2.getByText('example.com')).toBeVisible();
|
||||
|
||||
await app2.close();
|
||||
});
|
||||
47
e2e-tests/002-cookies-tests/002-corrupted-passkey.spec.ts
Normal file
47
e2e-tests/002-cookies-tests/002-corrupted-passkey.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
test('should handle corrupted passkey and still display saved cookie list', async ({ createTmpDir, launchElectronApp }) => {
|
||||
const userDataPath = await createTmpDir('corrupted-passkey');
|
||||
|
||||
const app1 = await launchElectronApp({ userDataPath });
|
||||
// 1. First run – add a cookie via the UI so `cookies.json` is created.
|
||||
const page1 = await app1.firstWindow();
|
||||
|
||||
await page1.waitForSelector('[data-trigger="cookies"]');
|
||||
await page1.click('[data-trigger="cookies"]');
|
||||
await page1.getByRole('button', { name: /Add Cookie/i }).click();
|
||||
|
||||
await page1.fill('input[name="domain"]', 'example.com');
|
||||
await page1.fill('input[name="path"]', '/');
|
||||
await page1.fill('input[name="key"]', 'session');
|
||||
await page1.fill('input[name="value"]', 'abc123');
|
||||
await page1.check('input[name="secure"]');
|
||||
await page1.check('input[name="httpOnly"]');
|
||||
|
||||
await page1.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page1.getByText('example.com')).toBeVisible();
|
||||
|
||||
await app1.close();
|
||||
|
||||
// 2. Corrupt the encryptedPasskey in cookies.json
|
||||
const cookiesFilePath = path.join(userDataPath, 'cookies.json');
|
||||
const raw = await fs.readFile(cookiesFilePath, 'utf-8');
|
||||
const cookiesJson = JSON.parse(raw);
|
||||
cookiesJson.encryptedPasskey = 'deadbeef'; // clearly invalid value
|
||||
await fs.writeFile(cookiesFilePath, JSON.stringify(cookiesJson, null, 2));
|
||||
|
||||
// 3. Second run – Bruno should recover and still list the cookie domain
|
||||
const app2 = await launchElectronApp({ userDataPath });
|
||||
const page2 = await app2.firstWindow();
|
||||
|
||||
await page2.waitForSelector('[data-trigger="cookies"]');
|
||||
await page2.click('[data-trigger="cookies"]');
|
||||
|
||||
// The domain row should still be visible (even if cookie values are blank).
|
||||
await expect(page2.getByText('example.com')).toBeVisible();
|
||||
|
||||
await app2.close();
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe.serial('Persistent Environment Test', () => {
|
||||
test.setTimeout(2 * 10 * 1000);
|
||||
|
||||
test('add env using script', async ({ pageWithUserData: page, restartApp }) => {
|
||||
await page.locator('#sidebar-collection-name').click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByText('ping', { exact: true }).click();
|
||||
await page.getByText('No Environment').click();
|
||||
await page.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click();
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('div').filter({ hasText: /^Env$/ }).nth(3).click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
|
||||
await page.getByText('×').click();
|
||||
|
||||
const newApp = await restartApp();
|
||||
const newPage = await newApp.firstWindow();
|
||||
await newPage.locator('#sidebar-collection-name').click();
|
||||
await newPage.getByRole('button', { name: 'Save' }).click();
|
||||
await newPage.getByText('ping', { exact: true }).click();
|
||||
await newPage.getByText('No Environment').click();
|
||||
await newPage.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click();
|
||||
await newPage.locator('div').filter({ hasText: /^Env$/ }).nth(3).click();
|
||||
await newPage.getByText('Configure', { exact: true }).click();
|
||||
await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
|
||||
await newPage.getByText('×').click();
|
||||
await newPage.waitForTimeout(1000);
|
||||
await newPage.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe.serial('Persistent Environment Test', () => {
|
||||
test.setTimeout(2 * 10 * 1000);
|
||||
|
||||
test('add env using script', async ({ pageWithUserData: page, restartApp }) => {
|
||||
await page.locator('#sidebar-collection-name').click();
|
||||
await page.getByText('ping2', { exact: true }).click();
|
||||
await page.getByText('Env', { exact: true }).click();
|
||||
await page.getByText('Stage', { exact: true }).click();
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Stage$/ })
|
||||
.nth(3)
|
||||
.click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
|
||||
await page.getByText('×').click();
|
||||
|
||||
const newApp = await restartApp();
|
||||
const newPage = await newApp.firstWindow();
|
||||
await newPage.locator('#sidebar-collection-name').click();
|
||||
await newPage.getByRole('button', { name: 'Save' }).click();
|
||||
await newPage.getByText('ping2', { exact: true }).click();
|
||||
await newPage.getByText('No Environment').click();
|
||||
await newPage.getByText('Stage').click();
|
||||
await newPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Stage$/ })
|
||||
.nth(3)
|
||||
.click();
|
||||
await newPage.getByText('Configure', { exact: true }).click();
|
||||
await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).not.toBeVisible();
|
||||
await newPage.getByText('×').click();
|
||||
await newPage.waitForTimeout(1000);
|
||||
await newPage.close();
|
||||
});
|
||||
});
|
||||
5
e2e-tests/persistent-env-tests/collection/bruno.json
Normal file
5
e2e-tests/persistent-env-tests/collection/bruno.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "collection",
|
||||
"type": "collection"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
persistent-env-test: persistent-env-test-value
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: ping2
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setEnvVar("persistent-env-test", "persistent-env-test-value");
|
||||
}
|
||||
15
e2e-tests/persistent-env-tests/collection/request.bru
Normal file
15
e2e-tests/persistent-env-tests/collection/request.bru
Normal file
@@ -0,0 +1,15 @@
|
||||
meta {
|
||||
name: ping
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{host}}/ping
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setEnvVar("persistent-env-test", "persistent-env-test-value", { persist: true });
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/e2e-tests/persistent-env-tests/collection"
|
||||
]
|
||||
}
|
||||
644
package-lock.json
generated
644
package-lock.json
generated
@@ -18,12 +18,14 @@
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests"
|
||||
"packages/bruno-requests",
|
||||
"packages/bruno-filestore"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
@@ -3650,9 +3652,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4489,6 +4491,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.3.tgz",
|
||||
"integrity": "sha512-FTXHdOoPbZrBjlVLHuKbDZnsTxXv2BlHF57xw6LuThXacXvtkahEPED0CKMk6obZDf65Hv4k3z62eyPNpvinIg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
||||
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.2.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "1.7.19",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz",
|
||||
@@ -5273,6 +5306,16 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-sdsl/ordered-map": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsep-plugin/assignment": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz",
|
||||
@@ -6359,6 +6402,70 @@
|
||||
"node": ">=16.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||
@@ -7052,6 +7159,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-json": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
|
||||
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-node-resolve": {
|
||||
"version": "15.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
|
||||
@@ -8392,6 +8520,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/google-protobuf": {
|
||||
"version": "3.15.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
|
||||
"integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
@@ -8542,6 +8676,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash.set": {
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.9.tgz",
|
||||
"integrity": "sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "12.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
|
||||
@@ -8571,7 +8714,6 @@
|
||||
"version": "22.15.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
|
||||
"integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -8725,6 +8867,10 @@
|
||||
"integrity": "sha512-khvEnRF6/UVDw4df06j+6lFWGNDYWlcWnxfmEgU2o/CdsGY291NC1Cexz99ud7sbGBQP2d8JUXZe4zXPkGNJpQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@usebruno/filestore": {
|
||||
"resolved": "packages/bruno-filestore",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@usebruno/graphql-docs": {
|
||||
"resolved": "packages/bruno-graphql-docs",
|
||||
"link": true
|
||||
@@ -10641,6 +10787,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
|
||||
"integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-status-codes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
|
||||
@@ -11519,6 +11678,33 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
|
||||
"integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-regexp": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp/node_modules/is-regexp": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz",
|
||||
"integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-response": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
@@ -11951,6 +12137,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-hrtime": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
|
||||
"integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -13423,15 +13621,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "33.2.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz",
|
||||
"integrity": "sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==",
|
||||
"version": "37.2.6",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-37.2.6.tgz",
|
||||
"integrity": "sha512-Ns6xyxE+hIK5UlujtRlw7w4e2Ju/ImCWXf1Q/PoOhc0N3/6SN6YW7+ujCarsHbxWnolbW+1RlkHtdklUJpjbPA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/node": "^22.7.7",
|
||||
"extract-zip": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
@@ -13656,23 +13854,6 @@
|
||||
"integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "20.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz",
|
||||
"integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/elliptic": {
|
||||
"version": "6.6.1",
|
||||
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
|
||||
@@ -13696,15 +13877,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emitter-component": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz",
|
||||
"integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/emittery": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
|
||||
@@ -13910,6 +14082,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
@@ -14849,9 +15036,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -15384,13 +15571,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
@@ -15556,6 +15745,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/function-timeout": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz",
|
||||
"integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/functions-have-names": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||
@@ -15819,9 +16020,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent/node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
@@ -15872,6 +16073,12 @@
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/google-protobuf": {
|
||||
"version": "3.21.4",
|
||||
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz",
|
||||
"integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==",
|
||||
"license": "(BSD-3-Clause AND Apache-2.0)"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -15971,19 +16178,37 @@
|
||||
}
|
||||
},
|
||||
"node_modules/graphql-request/node_modules/form-data": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz",
|
||||
"integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
|
||||
"integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/grpc-reflection-js": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/grpc-reflection-js/-/grpc-reflection-js-0.3.0.tgz",
|
||||
"integrity": "sha512-3lhTlQluPxVgbowCXA3tAZC3RJW+GSOUkguLNYl1QffYRiutUB3RDfPkQFTcrCFJgNiIIxx+iJkr8s3uSp3zWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/lodash.set": "^4.3.6",
|
||||
"google-protobuf": "^3.12.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"protobufjs": "^7.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grpc/grpc-js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
@@ -16086,7 +16311,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -19440,7 +19664,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.curry": {
|
||||
@@ -19518,6 +19741,12 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
@@ -19559,6 +19788,12 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -22900,6 +23135,30 @@
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
|
||||
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -25760,16 +26019,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/stream": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz",
|
||||
"integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emitter-component": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-browserify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
|
||||
@@ -26252,9 +26501,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sumchecker/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -26276,6 +26525,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/super-regex": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz",
|
||||
"integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"clone-regexp": "^3.0.0",
|
||||
"function-timeout": "^0.1.0",
|
||||
"time-span": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -26808,6 +27074,21 @@
|
||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/time-span": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
|
||||
"integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"convert-hrtime": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/timers-browserify": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
|
||||
@@ -26842,6 +27123,22 @@
|
||||
"@popperjs/core": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.12",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.12.tgz",
|
||||
"integrity": "sha512-M9ZQBPp6FyqhMcl233vHYyYRkxXOA1SKGlnq13S0mJdUhRSwr2w6I8rlchPL73wBwRlyIZpFvpu2VcdSMWLYXw==",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.0.12"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.0.12",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.12.tgz",
|
||||
"integrity": "sha512-3K76aXywJFduGRsOYoY5JzINLs/WMlOkeDwPL+8OCPq2Rh39gkSDtWAxdJQlWjpun/xF/LHf29yqCi6VC/rHDA=="
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||
@@ -26905,6 +27202,7 @@
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
@@ -26920,6 +27218,7 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
@@ -27169,7 +27468,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
@@ -29922,6 +30220,7 @@
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/filestore": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/requests": "^0.1.0",
|
||||
@@ -29941,7 +30240,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
},
|
||||
@@ -31005,6 +31303,9 @@
|
||||
"name": "@usebruno/common",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tough-cookie": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-typescript": "^7.27.0",
|
||||
@@ -31015,6 +31316,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
@@ -31535,6 +31837,34 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/ip-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/is-ip": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz",
|
||||
"integrity": "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ip-regex": "^5.0.0",
|
||||
"super-regex": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -31542,6 +31872,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-common/node_modules/tough-cookie": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common/node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
@@ -31576,7 +31917,7 @@
|
||||
"@web/rollup-plugin-copy": "^0.5.1",
|
||||
"babel-jest": "^29.7.0",
|
||||
"rimraf": "^5.0.7",
|
||||
"rollup": "3.2.5",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
@@ -31664,30 +32005,16 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/bruno-converters/node_modules/rollup": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.2.5.tgz",
|
||||
"integrity": "sha512-/Ha7HhVVofduy+RKWOQJrxe4Qb3xyZo+chcpYiD8SoQa4AG7llhupUtyfKSSrdBM2mWJjhM8wZwmbY23NmlIYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron": {
|
||||
"name": "bruno",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.750.0",
|
||||
"@grpc/grpc-js": "^1.13.2",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/converters": "^0.1.0",
|
||||
"@usebruno/filestore": "^0.1.0",
|
||||
"@usebruno/js": "0.12.0",
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"@usebruno/node-machine-id": "^2.0.0",
|
||||
@@ -31720,12 +32047,12 @@
|
||||
"nanoid": "3.3.8",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "33.2.1",
|
||||
"electron": "~37.2.6",
|
||||
"electron-builder": "25.1.8",
|
||||
"electron-devtools-installer": "^4.0.0"
|
||||
},
|
||||
@@ -32803,6 +33130,124 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/tough-cookie": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore": {
|
||||
"name": "@usebruno/filestore",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@usebruno/lang": "0.12.0",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
"@babel/preset-typescript": "^7.22.0",
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^24.1.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"jest": "^29.2.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/@types/node": {
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"packages/bruno-filestore/node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-graphql-docs": {
|
||||
"name": "@usebruno/graphql-docs",
|
||||
"version": "0.1.0",
|
||||
@@ -32887,9 +33332,7 @@
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"stream": "^0.0.2",
|
||||
"util": "^0.12.5"
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@usebruno/vm2": "^3.9.13"
|
||||
@@ -32955,17 +33398,24 @@
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^9.7.0",
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/proto-loader": "^0.7.15",
|
||||
"@types/qs": "^6.9.18",
|
||||
"axios": "^1.9.0"
|
||||
"axios": "^1.9.0",
|
||||
"grpc-reflection-js": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
"@babel/preset-typescript": "^7.22.0",
|
||||
"@rollup/plugin-alias": "^5.1.1",
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"@types/jest": "^29.5.11",
|
||||
"babel-jest": "^29.7.0",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"jest": "^29.2.0",
|
||||
"rollup": "3.29.5",
|
||||
"rollup-plugin-dts": "^5.0.0",
|
||||
@@ -32974,6 +33424,22 @@
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-requests/node_modules/@faker-js/faker": {
|
||||
"version": "9.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
|
||||
"integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-requests/node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests"
|
||||
"packages/bruno-requests",
|
||||
"packages/bruno-filestore"
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
@@ -40,6 +42,7 @@
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"watch": "npm run dev:watch",
|
||||
"dev:watch": "node ./scripts/dev-hot-reload.js",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
@@ -48,6 +51,7 @@
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
|
||||
"build:bruno-filestore": "npm run build --workspace=packages/bruno-filestore",
|
||||
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
|
||||
@@ -111,5 +111,10 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1"
|
||||
},
|
||||
"overrides": {
|
||||
"httpsnippet": {
|
||||
"form-data": "4.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const AwsV4Auth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -131,7 +135,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -140,6 +144,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const BasicAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = get(collection, 'root.request.auth.basic', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -55,7 +59,7 @@ const BasicAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -64,6 +68,7 @@ const BasicAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const BearerAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -30,7 +34,7 @@ const BearerAuth = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
@@ -39,6 +43,7 @@ const BearerAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const DigestAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = get(collection, 'root.request.auth.digest', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -55,7 +59,7 @@ const DigestAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -64,6 +68,7 @@ const DigestAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -18,6 +20,8 @@ const NTLMAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -82,7 +86,7 @@ const NTLMAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -91,6 +95,7 @@ const NTLMAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const WsseAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -55,14 +59,16 @@ const WsseAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,48 @@ const StyledWrapper = styled.div`
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-placeholder {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.protocol-https,
|
||||
.protocol-grpcs {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.protocol-https {
|
||||
animation: slideUpDown 6s infinite;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.protocol-grpcs {
|
||||
animation: slideUpDown 6s infinite 3s;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
@keyframes slideUpDown {
|
||||
0%, 45% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50%, 95% {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -132,7 +132,10 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
</label>
|
||||
<div className="relative flex items-center">
|
||||
<div className="absolute left-0 pl-2 text-gray-400 pointer-events-none flex items-center h-full">
|
||||
https://
|
||||
<span className="protocol-placeholder">
|
||||
<span className="protocol-https">https://</span>
|
||||
<span className="protocol-grpcs">grpcs://</span>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
id="domain"
|
||||
|
||||
@@ -46,7 +46,7 @@ const Docs = ({ collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
|
||||
<StyledWrapper className="h-full w-full relative flex flex-col">
|
||||
<div className='flex flex-row w-full justify-between items-center mb-4'>
|
||||
<div className='text-lg font-medium flex items-center gap-2'>
|
||||
<IconFileText size={20} strokeWidth={1.5} />
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.available-certificates {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
|
||||
button.remove-certificate {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,263 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconFile, IconFileImport, IconAlertCircle } from '@tabler/icons';
|
||||
import { getRelativePath, getBasename, getDirPath } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { existsSync, resolvePath } from '../../../utils/filesystem';
|
||||
|
||||
const GrpcSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
brunoConfig: { grpc: grpcConfig = {} }
|
||||
} = collection;
|
||||
|
||||
const fileInputRef = useRef(null);
|
||||
const [protoFileValidity, setProtoFileValidity] = useState({});
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
protoFiles: grpcConfig.protoFiles || []
|
||||
},
|
||||
onSubmit: (newGrpcConfig) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.grpc = newGrpcConfig;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
toast.success('gRPC settings updated');
|
||||
}
|
||||
});
|
||||
|
||||
// Get file path using the ipcRenderer
|
||||
const getProtoFile = (event) => {
|
||||
const files = event?.files;
|
||||
if (files && files.length > 0) {
|
||||
const newProtoFiles = [...formik.values.protoFiles];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[i]);
|
||||
if (filePath) {
|
||||
const relativePath = getRelativePath(filePath, collection.pathname);
|
||||
const protoFileObj = {
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
};
|
||||
|
||||
// Check if this path already exists
|
||||
const exists = newProtoFiles.some(pf => pf.path === protoFileObj.path);
|
||||
if (!exists) {
|
||||
newProtoFiles.push(protoFileObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formik.setFieldValue('protoFiles', newProtoFiles);
|
||||
// Reset the file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for removing a proto file
|
||||
const handleRemoveProtoFile = (index) => {
|
||||
const updatedProtoFiles = [...formik.values.protoFiles];
|
||||
updatedProtoFiles.splice(index, 1);
|
||||
formik.setFieldValue('protoFiles', updatedProtoFiles);
|
||||
};
|
||||
|
||||
// Handle the browse button click
|
||||
const handleBrowseClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
// Check if a proto file path is valid
|
||||
const isProtoFileValid = async (protoFile) => {
|
||||
try {
|
||||
const absolutePath = await resolvePath(protoFile.path, collection.pathname);
|
||||
return await existsSync(absolutePath);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate all proto files and update state
|
||||
useEffect(() => {
|
||||
const validateProtoFiles = async () => {
|
||||
const validityMap = {};
|
||||
for (const file of formik.values.protoFiles) {
|
||||
validityMap[file.path] = await isProtoFileValid(file);
|
||||
}
|
||||
setProtoFileValidity(validityMap);
|
||||
};
|
||||
|
||||
validateProtoFiles();
|
||||
}, [formik.values.protoFiles, collection.pathname]);
|
||||
|
||||
// Handle replacing an invalid proto file
|
||||
const handleReplaceProtoFile = (index) => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
// Store the index to replace after file selection
|
||||
fileInputRef.current.dataset.replaceIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle file input change
|
||||
const handleFileInputChange = (e) => {
|
||||
const replaceIndex = e.target.dataset.replaceIndex;
|
||||
if (replaceIndex !== undefined) {
|
||||
// Handle replacement
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[0]);
|
||||
if (filePath) {
|
||||
const relativePath = getRelativePath(filePath, collection.pathname);
|
||||
const updatedProtoFiles = [...formik.values.protoFiles];
|
||||
updatedProtoFiles[replaceIndex] = {
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
};
|
||||
formik.setFieldValue('protoFiles', updatedProtoFiles);
|
||||
}
|
||||
}
|
||||
delete e.target.dataset.replaceIndex;
|
||||
} else {
|
||||
getProtoFile(e.target);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label className="font-semibold text-sm mb-3 flex items-center" htmlFor="protoFiles">
|
||||
Add Proto Files
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Keep your proto files within the collection folder or the corresponding git repository to ensure paths remain valid when sharing the collection."
|
||||
/>
|
||||
</label>
|
||||
<div className="flex flex-col">
|
||||
{/* Hidden file input for file selection */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
accept=".proto"
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* File selection options */}
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary flex items-center"
|
||||
onClick={handleBrowseClick}
|
||||
>
|
||||
<IconFileImport size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Browse for proto files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-neutral-600 my-2"></div>
|
||||
|
||||
{/* List of added proto files */}
|
||||
<div>
|
||||
<div className="text-sm font-semibold mb-2 flex items-center">
|
||||
<IconFile size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Added Proto Files ({formik.values.protoFiles.length})
|
||||
</div>
|
||||
|
||||
{formik.values.protoFiles.length === 0 ? (
|
||||
<div className="text-neutral-500 text-sm italic">No proto files added yet</div>
|
||||
) : (
|
||||
<>
|
||||
{formik.values.protoFiles.some(file => !protoFileValidity[file.path]) && (
|
||||
<div className="text-xs text-red-500 mb-2 flex items-center bg-red-50 dark:bg-red-900/20 p-2 rounded">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some proto files cannot be found at their specified paths. Use the "Replace" option to update their locations.
|
||||
</div>
|
||||
)}
|
||||
<ul className="mt-4">
|
||||
{formik.values.protoFiles.map((file, index) => {
|
||||
const isValid = protoFileValidity[file.path];
|
||||
return (
|
||||
<li key={index} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex w-full items-center">
|
||||
<IconFile className="mr-2" size={18} strokeWidth={1.5} />
|
||||
<div
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px] text-sm"
|
||||
title={file.path}
|
||||
>
|
||||
{getBasename(file.path)}
|
||||
<span className="text-xs text-neutral-500 ml-2">
|
||||
{getDirPath(file.path)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-end">
|
||||
{!isValid && (
|
||||
<div className="flex items-center mr-2">
|
||||
<IconAlertCircle
|
||||
size={16}
|
||||
className="text-red-500"
|
||||
title="Proto file not found. Click to replace."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-red-500 ml-1 hover:underline"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="remove-certificate ml-2"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
title="Remove file"
|
||||
>
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcSettings;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
@@ -7,19 +7,30 @@ import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader
|
||||
deleteCollectionHeader,
|
||||
setCollectionHeaders
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
import BulkEditor from 'components/BulkEditor/index';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
};
|
||||
|
||||
const handleBulkHeadersChange = (newHeaders) => {
|
||||
dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: newHeaders }));
|
||||
};
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
@@ -63,6 +74,22 @@ const Headers = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Add request headers that will be sent with every request in this collection.
|
||||
</div>
|
||||
<BulkEditor
|
||||
params={headers}
|
||||
onChange={handleBulkHeadersChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
@@ -141,9 +168,14 @@ const Headers = ({ collection }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-add-header text-link pr-2 py-3 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<button className="text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const {
|
||||
brunoConfig: { presets: presets = {} }
|
||||
} = collection;
|
||||
@@ -15,10 +17,15 @@ const PresetsSettings = ({ collection }) => {
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
requestType: presets.requestType || 'http',
|
||||
requestType: presets.requestType === 'grpc' && !isGrpcEnabled ? 'http' : presets.requestType || 'http',
|
||||
requestUrl: presets.requestUrl || ''
|
||||
},
|
||||
onSubmit: (newPresets) => {
|
||||
// If gRPC is disabled but the preset is set to grpc, change it to http
|
||||
if (!isGrpcEnabled && newPresets.requestType === 'grpc') {
|
||||
newPresets.requestType = 'http';
|
||||
}
|
||||
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.presets = newPresets;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
@@ -62,6 +69,23 @@ const PresetsSettings = ({ collection }) => {
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
</label>
|
||||
|
||||
{isGrpcEnabled && (
|
||||
<>
|
||||
<input
|
||||
id="grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="grpc"
|
||||
checked={formik.values.requestType === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
@@ -74,7 +98,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
id="request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder='Request URL'
|
||||
placeholder="Request URL"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
@@ -87,6 +111,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
|
||||
@@ -13,13 +13,16 @@ import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Presets from './Presets';
|
||||
import Grpc from './Grpc';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Overview from './Overview/index';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const tab = collection.settingsSelectedTab;
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -45,7 +48,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
|
||||
const grpcConfig = get(collection, 'brunoConfig.grpc', {});
|
||||
|
||||
const onProxySettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
@@ -122,6 +125,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'grpc': {
|
||||
return <Grpc collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -132,9 +138,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4 overflow-scroll">
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4 overflow-hidden">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('overview')} role="tab" onClick={() => setTab('overview')}>
|
||||
<div className={getTabClassname('overview')} role="tab" onClick={() => setTab('overview')}>
|
||||
Overview
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
@@ -162,14 +168,20 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
{Object.keys(proxyConfig).length > 0 && <StatusDot />}
|
||||
{Object.keys(proxyConfig).length > 0 && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
|
||||
Client Certificates
|
||||
{clientCertConfig.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
{isGrpcEnabled && (
|
||||
<div className={getTabClassname('grpc')} role="tab" onClick={() => setTab('grpc')}>
|
||||
gRPC
|
||||
{grpcConfig.protoFiles && grpcConfig.protoFiles.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<section className="mt-4 h-full overflow-scroll">{getTabPanel(tab)}</section>
|
||||
<section className="mt-4 h-full overflow-auto">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ const Wrapper = styled.div`
|
||||
border-radius: 3px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
max-width: unset !important;
|
||||
|
||||
.tippy-content {
|
||||
padding-left: 0;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent }) => {
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, ...props }) => {
|
||||
return (
|
||||
<StyledWrapper className="dropdown" transparent={transparent}>
|
||||
<Tippy
|
||||
@@ -14,6 +14,7 @@ const Dropdown = ({ icon, children, onCreate, placement, transparent }) => {
|
||||
interactive={true}
|
||||
trigger="click"
|
||||
appendTo="parent"
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</Tippy>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const sensitiveFields = [
|
||||
'request.auth.oauth2.clientSecret',
|
||||
'request.auth.basic.password',
|
||||
'request.auth.digest.password',
|
||||
'request.auth.wsse.password',
|
||||
'request.auth.ntlm.password',
|
||||
'request.auth.awsv4.secretAccessKey',
|
||||
'request.auth.bearer.token'
|
||||
];
|
||||
|
||||
export { sensitiveFields };
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { get } from 'lodash';
|
||||
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -13,7 +14,9 @@ import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'utils/collections';
|
||||
import { sensitiveFields } from './constants';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -26,6 +29,50 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const nonSecretSensitiveVarUsageMap = useMemo(() => {
|
||||
const result = {};
|
||||
if (!collection || !environment?.variables) {
|
||||
return result;
|
||||
}
|
||||
const nonSecretVars = environment.variables.filter((v) => v.enabled && !v.secret && v.name);
|
||||
if (!nonSecretVars.length) {
|
||||
return result;
|
||||
}
|
||||
const varNames = new Set(nonSecretVars.map((v) => v.name));
|
||||
|
||||
const checkSensitiveField = (obj, fieldPath) => {
|
||||
const value = get(obj, fieldPath);
|
||||
if (typeof value === 'string') {
|
||||
varNames.forEach((varName) => {
|
||||
if (new RegExp(`\{\{\s*${varName}\s*\}\}`).test(value)) {
|
||||
result[varName] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getObjectToProcess = (item) => {
|
||||
if (isItemARequest(item)) {
|
||||
return item.draft || item;
|
||||
}
|
||||
return item.root;
|
||||
};
|
||||
|
||||
const collectionObj = getObjectToProcess(collection);
|
||||
sensitiveFields.forEach((fieldPath) => {
|
||||
checkSensitiveField(collectionObj, fieldPath);
|
||||
});
|
||||
|
||||
const items = flattenItems(collection.items || []);
|
||||
items.forEach((item) => {
|
||||
const objToProcess = getObjectToProcess(item);
|
||||
sensitiveFields.forEach((fieldPath) => {
|
||||
checkSensitiveField(objToProcess, fieldPath);
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}, [collection, environment]);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: environment.variables || [],
|
||||
@@ -61,6 +108,8 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
}
|
||||
});
|
||||
|
||||
const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name];
|
||||
|
||||
// Effect to track modifications.
|
||||
React.useEffect(() => {
|
||||
setIsModified(formik.dirty);
|
||||
@@ -163,7 +212,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap">
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
@@ -174,6 +223,12 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
</div>
|
||||
{!variable.secret && hasSensitiveUsage(variable.name) && (
|
||||
<SensitiveFieldWarning
|
||||
fieldName={variable.name}
|
||||
warningMessage="This variable is used in sensitive fields. Mark it as a secret for security"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addFolderHeader, updateFolderHeader, deleteFolderHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { addFolderHeader, updateFolderHeader, deleteFolderHeader, setFolderHeaders } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
import BulkEditor from 'components/BulkEditor/index';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
};
|
||||
|
||||
const handleBulkHeadersChange = (newHeaders) => {
|
||||
dispatch(setFolderHeaders({ collectionUid: collection.uid, folderUid: folder.uid, headers: newHeaders }));
|
||||
};
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
@@ -62,6 +72,22 @@ const Headers = ({ collection, folder }) => {
|
||||
);
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Request headers that will be sent with every request inside this folder.
|
||||
</div>
|
||||
<BulkEditor
|
||||
params={headers}
|
||||
onChange={handleBulkHeadersChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
@@ -141,9 +167,14 @@ const Headers = ({ collection, folder }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-add-header text-link pr-2 py-3 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<button className="text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -74,7 +74,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full overflow-scroll">
|
||||
<StyledWrapper className="flex flex-col h-full overflow-auto">
|
||||
<div className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
@@ -101,7 +101,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
Docs
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex mt-4 h-full overflow-scroll`}>{getTabPanel(tab)}</section>
|
||||
<section className={`flex mt-4 h-full overflow-auto`}>{getTabPanel(tab)}</section>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
93
packages/bruno-app/src/components/Icons/Grpc/index.js
Normal file
93
packages/bruno-app/src/components/Icons/Grpc/index.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
|
||||
// UNARY - Single request, single response (Blue)
|
||||
export const IconGrpcUnary = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
{/* Request arrow (top) - right */}
|
||||
<path d="M3 8h18" stroke="#3B82F6" strokeWidth={strokeWidth} />
|
||||
<path d="M18 5l3 3l-3 3" stroke="#3B82F6" strokeWidth={strokeWidth} />
|
||||
{/* Response arrow (bottom) - left */}
|
||||
<path d="M21 16h-18" stroke="#3B82F6" strokeWidth={strokeWidth} />
|
||||
<path d="M6 13l-3 3l3 3" stroke="#3B82F6" strokeWidth={strokeWidth} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// CLIENT_STREAMING - Streaming request, single response (Purple)
|
||||
export const IconGrpcClientStreaming = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
{/* Request arrow (top) - right with double heads */}
|
||||
<path d="M3 8h18" stroke="#8B5CF6" strokeWidth={strokeWidth} />
|
||||
<path d="M18 5l3 3l-3 3" stroke="#8B5CF6" strokeWidth={strokeWidth} />
|
||||
<path d="M14 5l3 3l-3 3" stroke="#8B5CF6" strokeWidth={strokeWidth} />
|
||||
{/* Response arrow (bottom) - left */}
|
||||
<path d="M21 16h-18" stroke="#8B5CF6" strokeWidth={strokeWidth} />
|
||||
<path d="M6 13l-3 3l3 3" stroke="#8B5CF6" strokeWidth={strokeWidth} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// SERVER_STREAMING - Single request, streaming response (Green)
|
||||
export const IconGrpcServerStreaming = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
{/* Request arrow (top) - right */}
|
||||
<path d="M3 8h18" stroke="#10B981" strokeWidth={strokeWidth} />
|
||||
<path d="M18 5l3 3l-3 3" stroke="#10B981" strokeWidth={strokeWidth} />
|
||||
{/* Response arrow (bottom) - left with double heads */}
|
||||
<path d="M21 16h-18" stroke="#10B981" strokeWidth={strokeWidth} />
|
||||
<path d="M6 13l-3 3l3 3" stroke="#10B981" strokeWidth={strokeWidth} />
|
||||
<path d="M10 13l-3 3l3 3" stroke="#10B981" strokeWidth={strokeWidth} />
|
||||
</svg>
|
||||
);
|
||||
|
||||
// BIDI_STREAMING - Streaming request, streaming response (Orange)
|
||||
export const IconGrpcBidiStreaming = ({ size = 18, strokeWidth = 1.5, className = '' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
{/* Request arrow (top) - right with double heads */}
|
||||
<path d="M3 8h18" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
<path d="M18 5l3 3l-3 3" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
<path d="M14 5l3 3l-3 3" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
{/* Response arrow (bottom) - left with double heads */}
|
||||
<path d="M21 16h-18" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
<path d="M6 13l-3 3l3 3" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
<path d="M10 13l-3 3l3 3" stroke="#F97316" strokeWidth={strokeWidth} />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.bruno-form {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.beta-feature-item {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-gray-200);
|
||||
background-color: var(--color-gray-50);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.beta-feature-item:hover {
|
||||
background-color: var(--color-gray-100);
|
||||
}
|
||||
|
||||
.beta-feature-description {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.no-features-message {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--color-gray-500);
|
||||
font-style: italic;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
125
packages/bruno-app/src/components/Preferences/Beta/index.js
Normal file
125
packages/bruno-app/src/components/Preferences/Beta/index.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconFlask } from '@tabler/icons';
|
||||
import get from 'lodash/get';
|
||||
|
||||
// Beta features configuration
|
||||
const BETA_FEATURES = [
|
||||
{
|
||||
id: 'grpc',
|
||||
label: 'gRPC Support',
|
||||
description: 'Enable gRPC request support for making gRPC calls to services'
|
||||
}
|
||||
];
|
||||
|
||||
const Beta = ({ close }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Generate validation schema dynamically from beta features
|
||||
const generateValidationSchema = () => {
|
||||
const schemaShape = {};
|
||||
BETA_FEATURES.forEach((feature) => {
|
||||
schemaShape[feature.id] = Yup.boolean();
|
||||
});
|
||||
return Yup.object().shape(schemaShape);
|
||||
};
|
||||
|
||||
// Generate initial values dynamically from beta features
|
||||
const generateInitialValues = () => {
|
||||
const initialValues = {};
|
||||
BETA_FEATURES.forEach((feature) => {
|
||||
initialValues[feature.id] = get(preferences, `beta.${feature.id}`, false);
|
||||
});
|
||||
return initialValues;
|
||||
};
|
||||
|
||||
const betaSchema = generateValidationSchema();
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: generateInitialValues(),
|
||||
validationSchema: betaSchema,
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
const newPreferences = await betaSchema.validate(values, { abortEarly: true });
|
||||
handleSave(newPreferences);
|
||||
} catch (error) {
|
||||
console.error('Beta preferences validation error:', error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleSave = (newBetaPreferences) => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
beta: newBetaPreferences
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('Beta preferences saved successfully');
|
||||
close();
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update beta preferences'));
|
||||
};
|
||||
|
||||
const hasAnyBetaFeatures = Object.values(formik.values).length > 0;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center mb-2">
|
||||
<IconFlask size={20} className="mr-2 text-orange-500" />
|
||||
<h2 className="text-lg font-semibold">Beta Features</h2>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4 text-wrap">
|
||||
Enable beta features, these features may be unstable or incomplete.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{BETA_FEATURES.map((feature) => (
|
||||
<div key={feature.id} className="beta-feature-item">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id={feature.id}
|
||||
type="checkbox"
|
||||
name={feature.id}
|
||||
checked={formik.values[feature.id]}
|
||||
onChange={formik.handleChange}
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
<label className="block ml-2 select-none font-medium" htmlFor={feature.id}>
|
||||
{feature.label}
|
||||
</label>
|
||||
</div>
|
||||
<div className="beta-feature-description ml-6 text-xs text-gray-500 dark:text-gray-400">
|
||||
{feature.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!hasAnyBetaFeatures && (
|
||||
<div className="no-features-message">
|
||||
<p>No beta features are currently available</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-10">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Beta;
|
||||
@@ -7,6 +7,7 @@ import General from './General';
|
||||
import Proxy from './ProxySettings';
|
||||
import Display from './Display';
|
||||
import Keybindings from './Keybindings';
|
||||
import Beta from './Beta';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -37,6 +38,10 @@ const Preferences = ({ onClose }) => {
|
||||
return <Keybindings close={onClose} />;
|
||||
}
|
||||
|
||||
case 'beta': {
|
||||
return <Beta close={onClose} />;
|
||||
}
|
||||
|
||||
case 'support': {
|
||||
return <Support />;
|
||||
}
|
||||
@@ -46,7 +51,7 @@ const Preferences = ({ onClose }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="lg" title="Preferences" handleCancel={onClose} hideFooter={true}>
|
||||
<div className='flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem] py-2'>
|
||||
<div className="flex flex-row gap-2 mx-[-1rem] !my-[-1.5rem] py-2">
|
||||
<div className="flex flex-col items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
|
||||
General
|
||||
@@ -63,6 +68,9 @@ const Preferences = ({ onClose }) => {
|
||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||
Support
|
||||
</div>
|
||||
<div className={getTabClassname('beta')} role="tab" onClick={() => setTab('beta')}>
|
||||
Beta
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-grow px-2 pt-2 pb-6 tab-panel">{getTabPanel(tab)}</section>
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,16 @@ import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { update } from 'lodash';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
|
||||
const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(request, 'auth.awsv4', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -144,7 +147,7 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -155,6 +158,8 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
|
||||
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = get(request, 'auth.basic', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -63,7 +67,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -74,6 +78,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -13,6 +15,8 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
|
||||
// Use the request prop directly like OAuth2ClientCredentials does
|
||||
const bearerToken = get(request, 'auth.bearer.token', '');
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -36,7 +40,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
@@ -47,6 +51,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -11,6 +13,8 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = get(request, 'auth.digest', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -62,7 +66,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -73,6 +77,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(request, 'auth.ntlm', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -80,7 +84,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -91,6 +95,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.tabs {
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px !important;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.mode === 'dark' ? 'rgba(99, 102, 241, 0.1)' : 'rgba(99, 102, 241, 0.1)'};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: ${(props) => props.theme.mode === 'dark' ? 'rgba(99, 102, 241, 0.2)' : 'rgba(99, 102, 241, 0.1)'};
|
||||
color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'};
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.additional-parameter-sends-in-selector {
|
||||
select {
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: 4px;
|
||||
padding: 0 8px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-additional-param-actions {
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.mode === 'dark' ? '#6366f1' : '#4f46e5'};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,306 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { IconPlus, IconTrash, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import { cloneDeep } from "lodash";
|
||||
import SingleLineEditor from "components/SingleLineEditor/index";
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import Table from "components/Table/index";
|
||||
|
||||
const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleSave }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const {
|
||||
grantType,
|
||||
additionalParameters = {}
|
||||
} = oAuth;
|
||||
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
(grantType == 'authorization_code' || grantType == 'implicit') ? 'authorization' : 'token'
|
||||
);
|
||||
|
||||
const isEmptyParam = (param) => {
|
||||
return !param.name.trim() && !param.value.trim();
|
||||
};
|
||||
|
||||
const hasEmptyRow = () => {
|
||||
const tabParams = additionalParameters[activeTab] || [];
|
||||
return tabParams.some(isEmptyParam);
|
||||
};
|
||||
|
||||
const updateAdditionalParameters = ({ updatedAdditionalParameters }) => {
|
||||
const filteredParams = cloneDeep(updatedAdditionalParameters);
|
||||
|
||||
Object.keys(filteredParams).forEach(paramType => {
|
||||
if (filteredParams[paramType]?.length) {
|
||||
filteredParams[paramType] = filteredParams[paramType].filter(param =>
|
||||
param.name.trim() || param.value.trim()
|
||||
);
|
||||
|
||||
if (filteredParams[paramType].length === 0) {
|
||||
delete filteredParams[paramType];
|
||||
}
|
||||
} else if (Array.isArray(filteredParams[paramType]) && filteredParams[paramType].length === 0) {
|
||||
// Remove empty arrays
|
||||
delete filteredParams[paramType];
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
...oAuth,
|
||||
additionalParameters: Object.keys(filteredParams).length > 0 ? filteredParams : undefined
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const handleUpdateAdditionalParam = ({ paramType, key, paramIndex, value }) => {
|
||||
const updatedAdditionalParameters = cloneDeep(additionalParameters);
|
||||
|
||||
if (!updatedAdditionalParameters[paramType]) {
|
||||
updatedAdditionalParameters[paramType] = [];
|
||||
}
|
||||
|
||||
if (!updatedAdditionalParameters[paramType][paramIndex]) {
|
||||
updatedAdditionalParameters[paramType][paramIndex] = {
|
||||
name: '',
|
||||
value: '',
|
||||
sendIn: 'headers',
|
||||
enabled: true
|
||||
};
|
||||
}
|
||||
|
||||
updatedAdditionalParameters[paramType][paramIndex][key] = value;
|
||||
|
||||
// Only filter when updating a parameter
|
||||
updateAdditionalParameters({ updatedAdditionalParameters });
|
||||
}
|
||||
|
||||
const handleDeleteAdditionalParam = ({ paramType, paramIndex }) => {
|
||||
const updatedAdditionalParameters = cloneDeep(additionalParameters);
|
||||
|
||||
if (updatedAdditionalParameters[paramType]?.length) {
|
||||
updatedAdditionalParameters[paramType] = updatedAdditionalParameters[paramType].filter((_, index) => index !== paramIndex);
|
||||
|
||||
// If the array is now empty, ensure we're not sending empty arrays
|
||||
if (updatedAdditionalParameters[paramType].length === 0) {
|
||||
delete updatedAdditionalParameters[paramType];
|
||||
}
|
||||
}
|
||||
|
||||
updateAdditionalParameters({ updatedAdditionalParameters });
|
||||
}
|
||||
|
||||
const handleAddNewAdditionalParam = () => {
|
||||
// Prevent adding multiple empty rows
|
||||
if (hasEmptyRow()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paramType = activeTab;
|
||||
const localAdditionalParameters = cloneDeep(additionalParameters);
|
||||
|
||||
if (!localAdditionalParameters[paramType]) {
|
||||
localAdditionalParameters[paramType] = [];
|
||||
}
|
||||
|
||||
localAdditionalParameters[paramType] = [
|
||||
...localAdditionalParameters[paramType],
|
||||
{
|
||||
name: '',
|
||||
value: '',
|
||||
sendIn: 'headers',
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
|
||||
// Don't filter here to allow the empty row to display in UI
|
||||
// But don't permanently store it in state until it has values
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
...oAuth,
|
||||
additionalParameters: localAdditionalParameters,
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Add a class to the Add Parameter button if it's disabled
|
||||
const addButtonDisabled = hasEmptyRow();
|
||||
|
||||
// Define available tabs for each grant type
|
||||
const getAvailableTabs = (grantType) => {
|
||||
const tabConfig = {
|
||||
'authorization_code': ['authorization', 'token', 'refresh'],
|
||||
'implicit': ['authorization'],
|
||||
'password': ['token', 'refresh'],
|
||||
'client_credentials': ['token', 'refresh']
|
||||
};
|
||||
return tabConfig[grantType] || ['token', 'refresh'];
|
||||
};
|
||||
|
||||
const availableTabs = getAvailableTabs(grantType);
|
||||
|
||||
const renderTab = (tabKey, tabLabel) => (
|
||||
<div
|
||||
key={tabKey}
|
||||
className={`tab ${activeTab === tabKey ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tabKey)}
|
||||
>
|
||||
{tabLabel}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-4">
|
||||
<div className="flex items-center gap-2.5 mb-3">
|
||||
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
|
||||
<IconAdjustmentsHorizontal size={14} className="text-indigo-500 dark:text-indigo-400" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
Additional Parameters
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="tabs flex w-full gap-2 my-2">
|
||||
{availableTabs.includes('authorization') && renderTab('authorization', 'Authorization')}
|
||||
{availableTabs.includes('token') && renderTab('token', 'Token')}
|
||||
{availableTabs.includes('refresh') && renderTab('refresh', 'Refresh')}
|
||||
</div>
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Key', accessor: 'name', width: '30%' },
|
||||
{ name: 'Value', accessor: 'value', width: '30%' },
|
||||
{ name: 'Send In', accessor: 'sendIn', width: '150px' },
|
||||
{ name: '', accessor: '', width: '15%' }
|
||||
]}
|
||||
>
|
||||
<tbody>
|
||||
{(additionalParameters?.[activeTab] || []).map((param, index) =>
|
||||
<tr key={index}>
|
||||
<td className='flex relative'>
|
||||
<SingleLineEditor
|
||||
value={param?.name || ''}
|
||||
theme={storedTheme}
|
||||
onChange={(value) => handleUpdateAdditionalParam({
|
||||
paramType: activeTab,
|
||||
key: 'name',
|
||||
paramIndex: index,
|
||||
value
|
||||
})}
|
||||
collection={collection}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={param?.value || ''}
|
||||
theme={storedTheme}
|
||||
onChange={(value) => handleUpdateAdditionalParam({
|
||||
paramType: activeTab,
|
||||
key: 'value',
|
||||
paramIndex: index,
|
||||
value
|
||||
})}
|
||||
collection={collection}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="w-full additional-parameter-sends-in-selector">
|
||||
<select
|
||||
value={param?.sendIn || 'headers'}
|
||||
onChange={e => {
|
||||
handleUpdateAdditionalParam({
|
||||
paramType: activeTab,
|
||||
key: 'sendIn',
|
||||
paramIndex: index,
|
||||
value: e.target.value
|
||||
})
|
||||
}}
|
||||
className="mousetrap bg-transparent"
|
||||
>
|
||||
{sendInOptionsMap[grantType || 'authorization_code'][activeTab].map((optionValue) => (
|
||||
<option key={optionValue} value={optionValue}>
|
||||
{optionValue}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param?.enabled ?? true}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => {
|
||||
handleUpdateAdditionalParam({
|
||||
paramType: activeTab,
|
||||
key: 'enabled',
|
||||
paramIndex: index,
|
||||
value: e.target.checked
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
tabIndex="-1"
|
||||
onClick={() => {
|
||||
handleDeleteAdditionalParam({
|
||||
paramType: activeTab,
|
||||
paramIndex: index
|
||||
})
|
||||
}}
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
<div
|
||||
className={`add-additional-param-actions w-fit flex items-center mt-2 ${addButtonDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
onClick={addButtonDisabled ? null : handleAddNewAdditionalParam}
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} style={{ marginLeft: '2px' }} />
|
||||
<span className="ml-1 text-sm text-gray-500">Add Parameter</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdditionalParams;
|
||||
|
||||
const sendInOptionsMap = {
|
||||
'authorization_code': {
|
||||
'authorization': ['headers', 'queryparams'],
|
||||
'token': ['headers', 'queryparams', 'body'],
|
||||
'refresh': ['headers', 'queryparams', 'body']
|
||||
},
|
||||
'password': {
|
||||
'token': ['headers', 'queryparams', 'body'],
|
||||
'refresh': ['headers', 'queryparams', 'body']
|
||||
},
|
||||
'client_credentials': {
|
||||
'token': ['headers', 'queryparams', 'body'],
|
||||
'refresh': ['headers', 'queryparams', 'body']
|
||||
},
|
||||
'implicit': {
|
||||
'authorization': ['headers', 'queryparams']
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -9,13 +10,15 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const {
|
||||
callbackUrl,
|
||||
@@ -33,7 +36,8 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
autoFetchToken,
|
||||
additionalParameters
|
||||
} = oAuth;
|
||||
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
@@ -83,6 +87,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters,
|
||||
[key]: value,
|
||||
}
|
||||
})
|
||||
@@ -110,6 +115,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
tokenHeaderPrefix,
|
||||
tokenQueryKey,
|
||||
autoFetchToken,
|
||||
additionalParameters,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
@@ -129,12 +135,15 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="single-line-editor-wrapper flex-1 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -143,6 +152,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -326,6 +336,13 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AdditionalParams
|
||||
item={item}
|
||||
request={request}
|
||||
collection={collection}
|
||||
updateAuth={updateAuth}
|
||||
handleSave={handleSave}
|
||||
/>
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -9,13 +10,15 @@ import { inputsConfig } from './inputsConfig';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
|
||||
const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
|
||||
const {
|
||||
@@ -30,7 +33,8 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
autoFetchToken,
|
||||
additionalParameters
|
||||
} = oAuth;
|
||||
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
@@ -77,6 +81,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -96,12 +101,15 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="single-line-editor-wrapper flex-1 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -110,6 +118,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -284,7 +293,13 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AdditionalParams
|
||||
item={item}
|
||||
request={request}
|
||||
collection={collection}
|
||||
updateAuth={updateAuth}
|
||||
handleSave={handleSave}
|
||||
/>
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import React, { useRef, forwardRef, useState, useMemo } from 'react';
|
||||
import React, { useRef, forwardRef, useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { IconCaretDown, IconLoader2, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import { IconCaretDown, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { clearOauth2Cache, fetchOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Wrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import toast from 'react-hot-toast';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
|
||||
@@ -19,7 +18,6 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const [fetchingToken, toggleFetchingToken] = useState(false);
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const {
|
||||
@@ -49,38 +47,6 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
);
|
||||
});
|
||||
|
||||
const handleFetchOauth2Credentials = async () => {
|
||||
let requestCopy = cloneDeep(request);
|
||||
requestCopy.oauth2 = requestCopy?.auth.oauth2;
|
||||
requestCopy.headers = {};
|
||||
toggleFetchingToken(true);
|
||||
try {
|
||||
const result = await dispatch(fetchOauth2Credentials({
|
||||
itemUid: item.uid,
|
||||
request: requestCopy,
|
||||
collection,
|
||||
folderUid: folder?.uid || null,
|
||||
forceGetToken: true
|
||||
}));
|
||||
|
||||
toggleFetchingToken(false);
|
||||
|
||||
// Check if the result contains error or if access_token is missing
|
||||
if (result?.error || !result?.access_token) {
|
||||
const errorMessage = result?.error || 'No access token received from authorization server';
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('Token fetched successfully!');
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
toggleFetchingToken(false);
|
||||
toast.error(error?.message || 'An error occurred while fetching token!');
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => { save(); };
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
@@ -111,16 +77,6 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
handleChange('autoFetchToken', e.target.checked);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAuthUrl, credentialsId }))
|
||||
.then(() => {
|
||||
toast.success('Cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
<Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={authorizationUrl} credentialsId={credentialsId} />
|
||||
@@ -262,18 +218,14 @@ const OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, colle
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-4 mt-4">
|
||||
<button
|
||||
onClick={handleFetchOauth2Credentials}
|
||||
className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}
|
||||
disabled={fetchingToken}
|
||||
>
|
||||
Get Access Token{fetchingToken ? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
<AdditionalParams
|
||||
item={item}
|
||||
request={request}
|
||||
collection={collection}
|
||||
updateAuth={updateAuth}
|
||||
handleSave={handleSave}
|
||||
/>
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={interpolatedAuthUrl} credentialsId={credentialsId} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -99,12 +99,22 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-4 mt-4">
|
||||
<button onClick={handleFetchOauth2Credentials} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
|
||||
<button
|
||||
onClick={handleFetchOauth2Credentials}
|
||||
className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}
|
||||
disabled={fetchingToken || refreshingToken}
|
||||
>
|
||||
Get Access Token{fetchingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button>
|
||||
{creds?.refresh_token ? <button onClick={handleRefreshAccessToken} className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}>
|
||||
Refresh Token{refreshingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button> : null}
|
||||
{creds?.refresh_token ?
|
||||
<button
|
||||
onClick={handleRefreshAccessToken}
|
||||
className={`submit btn btn-sm btn-secondary w-fit flex flex-row`}
|
||||
disabled={fetchingToken || refreshingToken}
|
||||
>
|
||||
Refresh Token{refreshingToken? <IconLoader2 className="animate-spin ml-2" size={18} strokeWidth={1.5} /> : ""}
|
||||
</button>
|
||||
: null}
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -9,14 +10,16 @@ import { inputsConfig } from './inputsConfig';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
|
||||
|
||||
const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
|
||||
const {
|
||||
accessTokenUrl,
|
||||
@@ -32,7 +35,8 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
tokenQueryKey,
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken
|
||||
autoFetchToken,
|
||||
additionalParameters
|
||||
} = oAuth;
|
||||
|
||||
const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';
|
||||
@@ -80,6 +84,7 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
refreshTokenUrl,
|
||||
autoRefreshToken,
|
||||
autoFetchToken,
|
||||
additionalParameters,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
@@ -99,12 +104,15 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="single-line-editor-wrapper flex-1 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -113,6 +121,7 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -287,6 +296,13 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AdditionalParams
|
||||
item={item}
|
||||
request={request}
|
||||
collection={collection}
|
||||
updateAuth={updateAuth}
|
||||
handleSave={handleSave}
|
||||
/>
|
||||
<Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = get(request, 'auth.wsse', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -63,7 +67,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -74,6 +78,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" message={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -109,7 +109,7 @@ const Auth = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
|
||||
<StyledWrapper className="w-full mt-1 overflow-auto">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
/* height: 100%; */
|
||||
position: relative;
|
||||
|
||||
.grpc-message-header {
|
||||
.font-medium {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#grpc-messages-container {
|
||||
/* height: 100%; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.add-message-btn-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: 8px;
|
||||
background: ${(props) => props.theme.bg || '#fff'};
|
||||
z-index: 15;
|
||||
border-top: 1px solid ${(props) => props.theme.border || 'rgba(0, 0, 0, 0.1)'};
|
||||
|
||||
.add-message-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-top: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
354
packages/bruno-app/src/components/RequestPane/GrpcBody/index.js
Normal file
354
packages/bruno-app/src/components/RequestPane/GrpcBody/index.js
Normal file
@@ -0,0 +1,354 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sendGrpcMessage, generateGrpcSampleMessage } from 'utils/network/index';
|
||||
import useLocalStorage from 'hooks/useLocalStorage';
|
||||
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconSend, IconRefresh, IconWand, IconPlus, IconTrash, IconChevronDown, IconChevronUp } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import toast from 'react-hot-toast'
|
||||
import { getAbsoluteFilePath } from 'utils/common/path';
|
||||
|
||||
const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCollapsed, onToggleCollapse, handleRun, canClientSendMultipleMessages }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme, theme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const isConnectionActive = useSelector((state) => state.collections.activeConnections.includes(item.uid));
|
||||
|
||||
// Access gRPC method metadata from local storage
|
||||
const [reflectionCache] = useLocalStorage('bruno.grpc.reflectionCache', {});
|
||||
const [protofileCache] = useLocalStorage('bruno.grpc.protofileCache', {});
|
||||
|
||||
const canClientStream = methodType === 'client-streaming' || methodType === 'bidi-streaming';
|
||||
|
||||
const { name, content } = message;
|
||||
|
||||
const onEdit = (value) => {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: value
|
||||
};
|
||||
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSend = async () => {
|
||||
try {
|
||||
await sendGrpcMessage(item, collection.uid, content);
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
}
|
||||
}
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const onRegenerateMessage = async () => {
|
||||
try {
|
||||
const methodPath = item.draft?.request?.method || item.request?.method;
|
||||
|
||||
if (!methodPath) {
|
||||
toastError(new Error('Method path not found in request'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the URL and protoPath to determine which cache to use
|
||||
const url = item.draft?.request?.url || item.request?.url;
|
||||
const protoPath = item.draft?.request?.protoPath || item.request?.protoPath;
|
||||
|
||||
// Find the method metadata from the appropriate cache
|
||||
let methodMetadata = null;
|
||||
if (protoPath) {
|
||||
// Use protofile cache if protoPath is available
|
||||
const absolutePath = getAbsoluteFilePath(protoPath, collection.pathname);
|
||||
const cachedMethods = protofileCache[absolutePath];
|
||||
if (cachedMethods) {
|
||||
methodMetadata = cachedMethods.find(method => method.path === methodPath);
|
||||
}
|
||||
} else if (url) {
|
||||
// Use reflection cache if no protoPath (reflection mode)
|
||||
const cachedMethods = reflectionCache[url];
|
||||
if (cachedMethods) {
|
||||
methodMetadata = cachedMethods.find(method => method.path === methodPath);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await generateGrpcSampleMessage(
|
||||
methodPath,
|
||||
content,
|
||||
{
|
||||
arraySize: 2,
|
||||
methodMetadata // Pass the method metadata to the function
|
||||
}
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: result.message
|
||||
};
|
||||
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
|
||||
toast.success('Sample message generated successfully!');
|
||||
} else {
|
||||
toastError(new Error(result.error || 'Failed to generate sample message'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating sample message:', error);
|
||||
toastError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteMessage = () => {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
currentMessages.splice(index, 1);
|
||||
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onPrettify = () => {
|
||||
try {
|
||||
const edits = format(content, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const prettyBodyJson = applyEdits(content, edits);
|
||||
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: prettyBodyJson
|
||||
};
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastError(new Error('Unable to prettify. Invalid JSON format.'));
|
||||
}
|
||||
};
|
||||
|
||||
const getContainerHeight = (canClientSendMultipleMessages && body.grpc.length > 1) ? `${isCollapsed ? "" : "h-80"}` : "h-full"
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col mb-3 border border-neutral-200 dark:border-neutral-800 rounded-md overflow-hidden ${getContainerHeight} relative`}>
|
||||
<div
|
||||
className="grpc-message-header flex items-center justify-between px-3 py-2 bg-neutral-100 dark:bg-neutral-700 cursor-pointer"
|
||||
onClick={onToggleCollapse}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isCollapsed ?
|
||||
<IconChevronDown size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" /> :
|
||||
<IconChevronUp size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
}
|
||||
<span className="font-medium text-sm">{`Message ${canClientStream ? index + 1 : ''}`}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
|
||||
<ToolHint text="Format JSON with proper indentation and spacing" toolhintId={`prettify-msg-${index}`}>
|
||||
<button
|
||||
onClick={onPrettify}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconWand size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Generate a new sample message based on schema" toolhintId={`regenerate-msg-${index}`}>
|
||||
<button
|
||||
onClick={onRegenerateMessage}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconRefresh size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
{canClientStream && (
|
||||
<ToolHint text={isConnectionActive ? "Send gRPC message" : "Connection not active"} toolhintId={`send-msg-${index}`}>
|
||||
<button
|
||||
onClick={onSend}
|
||||
disabled={!isConnectionActive}
|
||||
className={`p-1 rounded ${isConnectionActive ? 'hover:bg-zinc-200 dark:hover:bg-zinc-600' : 'opacity-50 cursor-not-allowed'} transition-colors`}
|
||||
>
|
||||
<IconSend
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className={`${isConnectionActive ? 'text-zinc-700 dark:text-zinc-300' : 'text-zinc-400 dark:text-zinc-500'}`}
|
||||
/>
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
|
||||
{index > 0 && (
|
||||
<ToolHint text="Delete this message" toolhintId={`delete-msg-${index}`}>
|
||||
<button
|
||||
onClick={onDeleteMessage}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconTrash size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className={`flex ${body.grpc.length === 1 || !canClientSendMultipleMessages ? "h-full" : "h-80"} relative`}>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onRun={handleRun}
|
||||
onSave={onSave}
|
||||
mode='application/ld+json'
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
|
||||
const dispatch = useDispatch();
|
||||
const [collapsedMessages, setCollapsedMessages] = useState([]);
|
||||
const messagesContainerRef = useRef(null);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
|
||||
const methodType = item.draft ? get(item, 'draft.request.methodType') : get(item, 'request.methodType');
|
||||
const canClientSendMultipleMessages = methodType === 'client-streaming' || methodType === 'bidi-streaming';
|
||||
|
||||
// Auto-scroll to the latest message when messages are added
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current && body?.grpc?.length > 0) {
|
||||
const container = messagesContainerRef.current;
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}, [body?.grpc?.length]);
|
||||
|
||||
const toggleMessageCollapse = (index) => {
|
||||
setCollapsedMessages(prev => {
|
||||
if (prev.includes(index)) {
|
||||
return prev.filter(i => i !== index);
|
||||
} else {
|
||||
return [...prev, index];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addNewMessage = () => {
|
||||
const currentMessages = Array.isArray(body.grpc)
|
||||
? [...body.grpc]
|
||||
: [];
|
||||
|
||||
currentMessages.push({
|
||||
name: `message ${currentMessages.length + 1}`,
|
||||
content: '{}'
|
||||
});
|
||||
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
if (!body?.grpc || !Array.isArray(body.grpc)) {
|
||||
return (
|
||||
<StyledWrapper isVerticalLayout={isVerticalLayout}>
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mb-4">No gRPC messages available</p>
|
||||
<ToolHint text="Add the first message to your gRPC request" toolhintId="add-first-msg">
|
||||
<button
|
||||
onClick={addNewMessage}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors"
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
|
||||
<span className="font-medium text-sm text-neutral-700 dark:text-neutral-300">Add First Message</span>
|
||||
</button>
|
||||
</ToolHint>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper isVerticalLayout={isVerticalLayout}>
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
id="grpc-messages-container"
|
||||
className={`flex-1 ${body.grpc.length === 1 || !canClientSendMultipleMessages ? "h-full" : "overflow-y-auto"} ${canClientSendMultipleMessages && "pb-16"}`}
|
||||
>
|
||||
{body.grpc
|
||||
.filter((_, index) => canClientSendMultipleMessages || index === 0)
|
||||
.map((message, index) => (
|
||||
<SingleGrpcMessage
|
||||
key={index}
|
||||
message={message}
|
||||
item={item}
|
||||
collection={collection}
|
||||
index={index}
|
||||
methodType={methodType}
|
||||
isCollapsed={collapsedMessages.includes(index)}
|
||||
onToggleCollapse={() => toggleMessageCollapse(index)}
|
||||
handleRun={handleRun}
|
||||
canClientSendMultipleMessages={canClientSendMultipleMessages}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{canClientSendMultipleMessages && (
|
||||
<div className="add-message-btn-container">
|
||||
<ToolHint text="Add a new gRPC message to the request" toolhintId="add-msg-fixed">
|
||||
<button
|
||||
onClick={addNewMessage}
|
||||
className="add-message-btn flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors shadow-md"
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
|
||||
<span className="font-medium text-sm text-neutral-700 dark:text-neutral-300">Add Message</span>
|
||||
</button>
|
||||
</ToolHint>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcBody;
|
||||
@@ -0,0 +1,119 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 2.3rem;
|
||||
|
||||
.method-selector-container {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
|
||||
input {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.infotip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infotip:hover .infotip-text {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.infotip-text {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 34px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.infotip-text::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.connection-status-strip {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
background-color: ${(props) => props.theme.colors.text.green};
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/* Method dropdown styling */
|
||||
.method-dropdown {
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
1028
packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/index.js
Normal file
1028
packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/index.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import StyledWrapper from '../../../Auth/AuthMode/StyledWrapper';
|
||||
|
||||
const GrpcAuthMode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
|
||||
const authModes = [
|
||||
{
|
||||
name: 'Basic Auth',
|
||||
mode: 'basic'
|
||||
},
|
||||
{
|
||||
name: 'Bearer Token',
|
||||
mode: 'bearer'
|
||||
},
|
||||
{
|
||||
name: 'API Key',
|
||||
mode: 'apikey'
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
mode: 'oauth2'
|
||||
},
|
||||
{
|
||||
name: 'Inherit',
|
||||
mode: 'inherit'
|
||||
},
|
||||
{
|
||||
name: 'No Auth',
|
||||
mode: 'none'
|
||||
}
|
||||
];
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
mode: value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onClickHandler = (mode) => {
|
||||
dropdownTippyRef?.current?.hide();
|
||||
onModeChange(mode);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
{authModes.map((authMode) => (
|
||||
<div
|
||||
key={authMode.mode}
|
||||
className="dropdown-item"
|
||||
onClick={() => onClickHandler(authMode.mode)}
|
||||
>
|
||||
{authMode.name}
|
||||
</div>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcAuthMode;
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.inherit-mode-text {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import GrpcAuthMode from './GrpcAuthMode';
|
||||
import BearerAuth from '../../Auth/BearerAuth';
|
||||
import BasicAuth from '../../Auth/BasicAuth';
|
||||
import ApiKeyAuth from '../../Auth/ApiKeyAuth';
|
||||
import OAuth2 from '../../Auth/OAuth2/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import { getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
// List of auth modes supported by gRPC
|
||||
const supportedGrpcAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'none', 'inherit'];
|
||||
|
||||
const GrpcAuth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
const request = item.draft
|
||||
? get(item, 'draft.request', {})
|
||||
: get(item, 'request', {});
|
||||
|
||||
const save = () => {
|
||||
return saveRequest(item.uid, collection.uid);
|
||||
};
|
||||
|
||||
// Reset to 'none' if current auth mode is not supported by gRPC
|
||||
useEffect(() => {
|
||||
if (authMode && !supportedGrpcAuthModes.includes(authMode)) {
|
||||
dispatch(
|
||||
updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
mode: 'none'
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [authMode, collection.uid, dispatch, item.uid]);
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Check folders in reverse to find the closest auth configuration
|
||||
for (let i of [...requestTreePath].reverse()) {
|
||||
if (i.type === 'folder') {
|
||||
const folderAuth = get(i, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: i.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
case 'bearer': {
|
||||
return <BearerAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
case 'apikey': {
|
||||
return <ApiKeyAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
|
||||
// Only show inherited auth if it's one of the supported types
|
||||
if (source && supportedGrpcAuthModes.includes(source.auth?.mode)) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Inherited auth not supported by gRPC. Using no auth instead.</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<GrpcAuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcAuth;
|
||||
@@ -0,0 +1,34 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
|
||||
.content-indicator {
|
||||
color: ${(props) => props.theme.text}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import GrpcBody from 'components/RequestPane/GrpcBody';
|
||||
import GrpcAuth from './GrpcAuth/index';
|
||||
import StatusDot from 'components/StatusDot/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { find, get } from 'lodash';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import { useEffect } from 'react';
|
||||
import { getPropertyFromDraftOrRequest } from 'utils/collections/index';
|
||||
|
||||
const GrpcRequestPane = ({ item, collection, handleRun }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'body': {
|
||||
return <GrpcBody item={item} collection={collection} hideModeSelector={true} hidePrettifyButton={true} handleRun={handleRun}/>;
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} addHeaderText="Add Metadata" />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <GrpcAuth item={item} collection={collection} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === focusedTab.requestPaneTab
|
||||
});
|
||||
};
|
||||
|
||||
const isMultipleContentTab = ['script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
|
||||
const body = getPropertyFromDraftOrRequest(item, 'request.body');
|
||||
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
|
||||
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
|
||||
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
|
||||
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
const grpcMessagesCount = body?.grpc?.length || 0;
|
||||
|
||||
// Determine if this is a client streaming request
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
const isClientStreaming = request.methodType === 'client-streaming' || request.methodType === 'bidi-streaming';
|
||||
|
||||
useEffect(() => {
|
||||
// Only set the tab to 'body' if no tab is currently set
|
||||
if (!focusedTab?.requestPaneTab) {
|
||||
selectTab('body');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Message
|
||||
{grpcMessagesCount > 0 && (
|
||||
isClientStreaming ? (
|
||||
<sup className="ml-[.125rem] font-medium">{grpcMessagesCount}</sup>
|
||||
) : (
|
||||
<StatusDot />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Metadata
|
||||
{activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
{auth.mode !== 'none' && <StatusDot type="default" />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
{docs && docs.length > 0 && <StatusDot type="default" />}
|
||||
</div>
|
||||
</div>
|
||||
<section
|
||||
className={classnames('flex w-full flex-1 h-full', {
|
||||
'mt-2': !isMultipleContentTab
|
||||
})}
|
||||
>
|
||||
<HeightBoundContainer>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
</HeightBoundContainer>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcRequestPane;
|
||||
@@ -20,6 +20,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const isMac = isMacOS();
|
||||
const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';
|
||||
const editorRef = useRef(null);
|
||||
const isGrpc = item.type === 'grpc-request';
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||
@@ -80,7 +81,14 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
{isGrpc ? (
|
||||
<div className="flex items-center justify-center h-full w-16">
|
||||
<span className="text-xs text-indigo-500 font-bold">gRPC</span>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
id="request-url"
|
||||
|
||||
@@ -71,81 +71,81 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer body-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div className="label-item font-medium">Form</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('multipartForm');
|
||||
}}
|
||||
>
|
||||
Multipart Form
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('formUrlEncoded');
|
||||
}}
|
||||
>
|
||||
Form URL Encoded
|
||||
</div>
|
||||
<div className="label-item font-medium">Raw</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('json');
|
||||
}}
|
||||
>
|
||||
JSON
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('xml');
|
||||
}}
|
||||
>
|
||||
XML
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('text');
|
||||
}}
|
||||
>
|
||||
TEXT
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('sparql');
|
||||
}}
|
||||
>
|
||||
SPARQL
|
||||
</div>
|
||||
<div className="label-item font-medium">Other</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('file');
|
||||
}}
|
||||
>
|
||||
File / Binary
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Body
|
||||
</div>
|
||||
<div className="label-item font-medium">Form</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('multipartForm');
|
||||
}}
|
||||
>
|
||||
Multipart Form
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('formUrlEncoded');
|
||||
}}
|
||||
>
|
||||
Form URL Encoded
|
||||
</div>
|
||||
<div className="label-item font-medium">Raw</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('json');
|
||||
}}
|
||||
>
|
||||
JSON
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('xml');
|
||||
}}
|
||||
>
|
||||
XML
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('text');
|
||||
}}
|
||||
>
|
||||
TEXT
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('sparql');
|
||||
}}
|
||||
>
|
||||
SPARQL
|
||||
</div>
|
||||
<div className="label-item font-medium">Other</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('file');
|
||||
}}
|
||||
>
|
||||
File / Binary
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Body
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{(bodyMode === 'json' || bodyMode === 'xml') && (
|
||||
|
||||
@@ -79,4 +79,4 @@ const RequestBody = ({ item, collection }) => {
|
||||
|
||||
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
|
||||
};
|
||||
export default RequestBody;
|
||||
export default RequestBody;
|
||||
@@ -16,7 +16,7 @@ import BulkEditor from '../../BulkEditor';
|
||||
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
const RequestHeaders = ({ item, collection, addHeaderText }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
@@ -181,7 +181,7 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
</Table>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-action text-link pr-2 py-3 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
+ {addHeaderText || 'Add Header'}
|
||||
</button>
|
||||
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons';
|
||||
import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { loadLargeRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestNotLoaded = ({ collection, item }) => {
|
||||
const dispatch = useDispatch();
|
||||
const handleLoadRequestViaWorker = () => {
|
||||
!item?.loading && dispatch(loadRequestViaWorker({ collectionUid: collection?.uid, pathname: item?.pathname }));
|
||||
}
|
||||
|
||||
const handleLoadRequest = () => {
|
||||
!item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
|
||||
const handleLoadLargeRequest = () => {
|
||||
!item?.loading && dispatch(loadLargeRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
|
||||
}
|
||||
|
||||
return <StyledWrapper>
|
||||
@@ -44,23 +41,14 @@ const RequestNotLoaded = ({ collection, item }) => {
|
||||
<IconAlertTriangle size={16} className="text-yellow-500" />
|
||||
<span>The request wasn't loaded due to its large size. Please try again with the following options:</span>
|
||||
</div>
|
||||
<div className='flex flex-row mt-6 gap-2 items-center w-full'>
|
||||
<button
|
||||
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
|
||||
onClick={handleLoadRequestViaWorker}
|
||||
>
|
||||
Load in background
|
||||
</button>
|
||||
<p>(Runs in background)</p>
|
||||
</div>
|
||||
<div className='flex flex-row mt-6 items-center gap-2 w-full'>
|
||||
<button
|
||||
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
|
||||
onClick={handleLoadRequest}
|
||||
onClick={handleLoadLargeRequest}
|
||||
>
|
||||
Force load
|
||||
Load Request
|
||||
</button>
|
||||
<p>(May cause the app to freeze temporarily while it runs)</p>
|
||||
<p>(Uses a regex based parsing approach)</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -18,6 +18,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 0;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
|
||||
div.dragbar-handle {
|
||||
display: flex;
|
||||
@@ -45,6 +46,7 @@ const StyledWrapper = styled.div`
|
||||
height: 10px;
|
||||
cursor: row-resize;
|
||||
padding: 0 1rem;
|
||||
position: relative;
|
||||
|
||||
div.dragbar-handle {
|
||||
width: 100%;
|
||||
|
||||
@@ -4,13 +4,16 @@ import toast from 'react-hot-toast';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import GraphQLRequestPane from 'components/RequestPane/GraphQLRequestPane';
|
||||
import HttpRequestPane from 'components/RequestPane/HttpRequestPane';
|
||||
import GrpcRequestPane from 'components/RequestPane/GrpcRequestPane/index';
|
||||
import ResponsePane from 'components/ResponsePane';
|
||||
import GrpcResponsePane from 'components/ResponsePane/GrpcResponsePane';
|
||||
import Welcome from 'components/Welcome';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl/index';
|
||||
import GrpcQueryUrl from 'components/RequestPane/GrpcQueryUrl/index';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import RunnerResults from 'components/RunnerResults';
|
||||
import VariablesEditor from 'components/VariablesEditor';
|
||||
@@ -180,6 +183,9 @@ const RequestTabPanel = () => {
|
||||
return <div className="pb-4 px-4">Collection not found!</div>;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
const isGrpcRequest = item?.type === 'grpc-request';
|
||||
|
||||
if (focusedTab.type === 'collection-runner') {
|
||||
return <RunnerResults collection={collection} />;
|
||||
}
|
||||
@@ -201,7 +207,7 @@ const RequestTabPanel = () => {
|
||||
if (!folder) {
|
||||
return <FolderNotFound folderUid={focusedTab.folderUid} />;
|
||||
}
|
||||
|
||||
|
||||
return <FolderSettings collection={collection} folder={folder} />;
|
||||
}
|
||||
|
||||
@@ -209,20 +215,32 @@ const RequestTabPanel = () => {
|
||||
return <SecuritySettings collection={collection} />;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
if (!item || !item.uid) {
|
||||
return <RequestNotFound itemUid={activeTabUid} />;
|
||||
}
|
||||
|
||||
if (item?.partial) {
|
||||
return <RequestNotLoaded item={item} collection={collection} />
|
||||
return <RequestNotLoaded item={item} collection={collection} />;
|
||||
}
|
||||
|
||||
if (item?.loading) {
|
||||
return <RequestIsLoading item={item} />
|
||||
return <RequestIsLoading item={item} />;
|
||||
}
|
||||
|
||||
const handleRun = async () => {
|
||||
const isGrpcRequest = item?.type === 'grpc-request';
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
|
||||
if (isGrpcRequest && !request.url) {
|
||||
toast.error('Please enter a valid gRPC server URL');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isGrpcRequest && !request.method) {
|
||||
toast.error('Please select a gRPC method');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
@@ -233,9 +251,13 @@ const RequestTabPanel = () => {
|
||||
return (
|
||||
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${isVerticalLayout ? 'vertical-layout' : ''}`}>
|
||||
<div className="pt-4 pb-3 px-4">
|
||||
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
{isGrpcRequest ? (
|
||||
<GrpcQueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
) : (
|
||||
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
)}
|
||||
</div>
|
||||
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative`}>
|
||||
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative overflow-auto`}>
|
||||
<section className="request-pane">
|
||||
<div
|
||||
className="px-4 h-full"
|
||||
@@ -260,6 +282,10 @@ const RequestTabPanel = () => {
|
||||
{item.type === 'http-request' ? (
|
||||
<HttpRequestPane item={item} collection={collection} />
|
||||
) : null}
|
||||
|
||||
{isGrpcRequest ? (
|
||||
<GrpcRequestPane item={item} collection={collection} handleRun={handleRun} />
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -268,7 +294,20 @@ const RequestTabPanel = () => {
|
||||
</div>
|
||||
|
||||
<section className="response-pane flex-grow overflow-x-auto">
|
||||
<ResponsePane item={item} collection={collection} response={item.response} />
|
||||
{item.type === 'grpc-request' ? (
|
||||
<GrpcResponsePane
|
||||
item={item}
|
||||
collection={collection}
|
||||
|
||||
response={item.response}
|
||||
/>
|
||||
) : (
|
||||
<ResponsePane
|
||||
item={item}
|
||||
collection={collection}
|
||||
response={item.response}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { flattenItems } from 'utils/collections/index';
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
@@ -65,10 +66,10 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
};
|
||||
|
||||
const getMethodColor = (method = '') => {
|
||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
return theme.request.methods[method.toLocaleLowerCase()];
|
||||
};
|
||||
|
||||
|
||||
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
|
||||
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
return (
|
||||
@@ -107,6 +108,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
);
|
||||
}
|
||||
|
||||
const isGrpc = item.type === 'grpc-request';
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
|
||||
return (
|
||||
@@ -159,8 +161,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="tab-method uppercase" style={{ color: getMethodColor(method), fontSize: 12 }}>
|
||||
{method}
|
||||
<span className="tab-method uppercase" style={{ color: isGrpc ? theme.request.grpc : getMethodColor(method), fontSize: 12 }}>
|
||||
{isGrpc ? 'gRPC' : method}
|
||||
</span>
|
||||
<span className="ml-1 tab-name" title={item.name}>
|
||||
{item.name}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
border-left: 4px solid ${(props) => props.theme.colors.text.danger};
|
||||
border-top: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
max-height: 200px;
|
||||
min-height: 70px;
|
||||
overflow-y: auto;
|
||||
background-color: ${(props) => (props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.5)' : 'rgba(250, 250, 250, 0.9)')};
|
||||
|
||||
.close-button {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.375rem;
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-family: monospace;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.25rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { IconX } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GrpcError = ({ error, onClose }) => {
|
||||
if (!error) return null;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-4 mb-2">
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="error-title">gRPC Server Error</div>
|
||||
<div className="error-message">{typeof error === 'string' ? error : JSON.stringify(error, null, 2)}</div>
|
||||
</div>
|
||||
<div className="close-button flex-shrink-0 cursor-pointer" onClick={onClose}>
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcError;
|
||||
@@ -0,0 +1,96 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-radius: 4px;
|
||||
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
font-family: ${(props) => (props.font === 'default' ? 'monospace' : props.font)};
|
||||
font-size: ${(props) => (props.fontSize ? props.fontSize : '13px')};
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
|
||||
&.open {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.error-header {
|
||||
background-color: ${(props) => (props.theme.bg === '#1e1e1e' ? 'rgba(185, 28, 28, 0.1)' : '#fee2e2')};
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stream-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&.complete {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
&.streaming {
|
||||
color: ${(props) => props.theme.colors.text.blue};
|
||||
}
|
||||
}
|
||||
|
||||
.message-counter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.response-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.response-message {
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-panel-background);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Accordion from 'components/Accordion';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { get } from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { formatISO9075 } from 'date-fns';
|
||||
import GrpcError from '../GrpcError';
|
||||
|
||||
const GrpcQueryResult = ({ item, collection }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(true);
|
||||
|
||||
const response = item.response || {};
|
||||
const responsesList = response?.responses || [];
|
||||
// Reverse the responses list to show the most recent at the top
|
||||
const reversedResponsesList = [...responsesList].reverse();
|
||||
const hasError = response.isError;
|
||||
const hasResponses = responsesList.length > 0;
|
||||
const errorMessage = response.error;
|
||||
|
||||
// Reset error visibility when a new response is received
|
||||
useEffect(() => {
|
||||
if (hasError) {
|
||||
setShowErrorMessage(true);
|
||||
}
|
||||
}, [response, hasError]);
|
||||
|
||||
// Format a timestamp to a human-readable format
|
||||
const formatTimestamp = (timestamp) => {
|
||||
if (!timestamp) return 'Unknown time';
|
||||
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
return formatISO9075(date);
|
||||
} catch (e) {
|
||||
return 'Invalid time';
|
||||
}
|
||||
};
|
||||
|
||||
// Format JSON for display
|
||||
const formatJSON = (data) => {
|
||||
try {
|
||||
if (typeof data === 'string') {
|
||||
return JSON.stringify(JSON.parse(data), null, 2);
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch (e) {
|
||||
return typeof data === 'string' ? data : JSON.stringify(data);
|
||||
}
|
||||
};
|
||||
|
||||
if (!hasResponses && !hasError) {
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full relative flex flex-col">
|
||||
<div className="text-gray-500 dark:text-gray-400 p-4">No messages received</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full relative flex flex-col mt-2">
|
||||
{hasError && showErrorMessage && <GrpcError error={errorMessage} onClose={() => setShowErrorMessage(false)} />}
|
||||
{hasResponses && (
|
||||
<div className={`overflow-y-auto ${responsesList.length === 1 ? 'flex-1' : ''}`}>
|
||||
{responsesList.length === 1 ? (
|
||||
// Single message - render directly without accordion
|
||||
<div className="h-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
value={formatJSON(reversedResponsesList[0])}
|
||||
mode="application/json"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Multiple messages - use accordion
|
||||
<Accordion defaultIndex={0}>
|
||||
{reversedResponsesList.map((response, index) => {
|
||||
// Calculate the original response number (for display purposes)
|
||||
const originalIndex = responsesList.length - index - 1;
|
||||
|
||||
return (
|
||||
<Accordion.Item key={originalIndex} index={index}>
|
||||
<Accordion.Header index={index} style={{ padding: '8px 12px', minHeight: '40px' }}>
|
||||
<div className="flex justify-between w-full">
|
||||
<div className="font-medium">
|
||||
Response {originalIndex + 1} {index === 0 ? '(Latest)' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content index={index} style={{ padding: '0px' }}>
|
||||
<div className="h-60">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
value={formatJSON(response)}
|
||||
mode="application/json"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasError && !hasResponses && !showErrorMessage && (
|
||||
<div className="text-gray-500 dark:text-gray-400 p-4">
|
||||
No messages received. A server error occurred but has been dismissed.
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcQueryResult;
|
||||
@@ -0,0 +1,31 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
color: #777777;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&.value {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr:nth-child(odd) {
|
||||
background-color: ${(props) => props.theme.table.striped};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GrpcResponseHeaders = ({ metadata }) => {
|
||||
// Ensure headers is an array
|
||||
const metadataArray = Array.isArray(metadata) ? metadata : [];
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{metadataArray && metadataArray.length ? (
|
||||
metadataArray.map((metadata, index) => (
|
||||
<tr key={index}>
|
||||
<td className="key">{metadata.name}</td>
|
||||
<td className="value">{metadata.value}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" className="text-center py-4 text-gray-500">
|
||||
No metadata received
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcResponseHeaders;
|
||||
@@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.text-ok {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseOk};
|
||||
}
|
||||
|
||||
&.text-pending {
|
||||
color: ${(props) => props.theme.requestTabPanel.responsePending};
|
||||
}
|
||||
|
||||
&.text-error {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseError};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,22 @@
|
||||
// https://grpc.github.io/grpc/core/md_doc_statuscodes.html
|
||||
const grpcStatusCodePhraseMap = {
|
||||
0: 'OK',
|
||||
1: 'Cancelled',
|
||||
2: 'Unknown',
|
||||
3: 'Invalid Argument',
|
||||
4: 'Deadline Exceeded',
|
||||
5: 'Not Found',
|
||||
6: 'Already Exists',
|
||||
7: 'Permission Denied',
|
||||
8: 'Resource Exhausted',
|
||||
9: 'Failed Precondition',
|
||||
10: 'Aborted',
|
||||
11: 'Out of Range',
|
||||
12: 'Unimplemented',
|
||||
13: 'Internal',
|
||||
14: 'Unavailable',
|
||||
15: 'Data Loss',
|
||||
16: 'Unauthenticated'
|
||||
};
|
||||
|
||||
export default grpcStatusCodePhraseMap;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import grpcStatusCodePhraseMap from './get-grpc-status-code-phrase';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GrpcStatusCode = ({ status, text }) => {
|
||||
// gRPC status codes: 0 is success, anything else is an error
|
||||
const getTabClassname = (status) => {
|
||||
const isPending = text === 'PENDING' || text === 'STREAMING';
|
||||
return classnames('ml-2', {
|
||||
'text-ok': parseInt(status) === 0,
|
||||
'text-pending': isPending,
|
||||
'text-error': parseInt(status) > 0 && !isPending
|
||||
});
|
||||
};
|
||||
|
||||
const statusText = text || grpcStatusCodePhraseMap[status]
|
||||
|
||||
return (
|
||||
<StyledWrapper className={getTabClassname(status)}>
|
||||
{Number.isInteger(status) ? <div className="mr-1">{status}</div> : null}
|
||||
{statusText && <div>{statusText}</div>}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcStatusCode;
|
||||
@@ -0,0 +1,31 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
color: #777777;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&.value {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr:nth-child(odd) {
|
||||
background-color: ${(props) => props.theme.table.striped};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponseTrailers = ({ trailers }) => {
|
||||
const trailersArray = Array.isArray(trailers) ? trailers : [];
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trailersArray && trailersArray.length ? (
|
||||
trailersArray.map((trailer, index) => (
|
||||
<tr key={index}>
|
||||
<td className="key">{trailer.name}</td>
|
||||
<td className="value">{trailer.value}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" className="text-center py-4 text-gray-500">
|
||||
No trailers received
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponseTrailers;
|
||||
@@ -0,0 +1,58 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-radius: 4px;
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stream-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&.complete {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
&.streaming {
|
||||
color: ${(props) => props.theme.colors.text.blue};
|
||||
}
|
||||
}
|
||||
|
||||
.message-counter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,160 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import Overlay from '../Overlay';
|
||||
import Placeholder from '../Placeholder';
|
||||
import GrpcResponseHeaders from './GrpcResponseHeaders';
|
||||
import GrpcStatusCode from './GrpcStatusCode';
|
||||
import ResponseTime from '../ResponseTime/index';
|
||||
import Timeline from '../Timeline';
|
||||
import ClearTimeline from '../ClearTimeline';
|
||||
import ResponseSave from '../ResponseSave';
|
||||
import ResponseClear from '../ResponseClear';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseTrailers from './ResponseTrailers';
|
||||
import GrpcQueryResult from './GrpcQueryResult';
|
||||
import ResponseLayoutToggle from '../ResponseLayoutToggle';
|
||||
import Tab from 'components/Tab';
|
||||
|
||||
const GrpcResponsePane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
|
||||
const requestTimeline = [...(collection?.timeline || [])].filter((obj) => {
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
});
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateResponsePaneTab({
|
||||
uid: item.uid,
|
||||
responsePaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const response = item.response || {};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return <GrpcQueryResult item={item} collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <GrpcResponseHeaders metadata={response.metadata} />;
|
||||
}
|
||||
case 'trailers': {
|
||||
return <ResponseTrailers trailers={response.trailers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline collection={collection} item={item} />;
|
||||
}
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading && !item.response) {
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<Overlay item={item} collection={collection} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (!item.response && !requestTimeline?.length) {
|
||||
return (
|
||||
<StyledWrapper className="flex h-full relative">
|
||||
<Placeholder />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.responsePaneTab) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const tabConfig = [
|
||||
{
|
||||
name: 'response',
|
||||
label: 'Response',
|
||||
count: Array.isArray(response.responses) ? response.responses.length : 0
|
||||
},
|
||||
{
|
||||
name: 'headers',
|
||||
label: 'Metadata',
|
||||
count: Array.isArray(response.metadata) ? response.metadata.length : 0
|
||||
},
|
||||
{
|
||||
name: 'trailers',
|
||||
label: 'Trailers',
|
||||
count: Array.isArray(response.trailers) ? response.trailers.length : 0
|
||||
},
|
||||
{
|
||||
name: 'timeline',
|
||||
label: 'Timeline'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist">
|
||||
{tabConfig.map((tab) => (
|
||||
<Tab
|
||||
key={tab.name}
|
||||
name={tab.name}
|
||||
label={tab.label}
|
||||
isActive={focusedTab.responsePaneTab === tab.name}
|
||||
onClick={selectTab}
|
||||
count={tab.count}
|
||||
/>
|
||||
))}
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{focusedTab?.responsePaneTab === 'timeline' ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
</>
|
||||
) : item?.response ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<GrpcStatusCode
|
||||
status={response.statusCode}
|
||||
text={response.statusText}
|
||||
details={response.statusDescription}
|
||||
/>
|
||||
<ResponseTime duration={response.duration} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex flex-col flex-grow pl-3 pr-4 h-0 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
|
||||
>
|
||||
{isLoading ? <Overlay item={item} collection={collection} /> : null}
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
|
||||
<Timeline collection={collection} item={item} />
|
||||
) : null
|
||||
) : (
|
||||
<>{getTabPanel(focusedTab.responsePaneTab)}</>
|
||||
)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcResponsePane;
|
||||
@@ -20,7 +20,7 @@ const ResponseSize = ({ size }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper title={size.toLocaleString() + 'B'} className="ml-4">
|
||||
<StyledWrapper title={(size?.toLocaleString() || '0') + 'B'} className="ml-4">
|
||||
{sizeToDisplay}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
|
||||
const ResponseTime = ({ duration }) => {
|
||||
let durationToDisplay = '';
|
||||
|
||||
if (duration > 1000) {
|
||||
// duration greater than a second
|
||||
let seconds = Math.floor(duration / 1000);
|
||||
@@ -13,6 +13,10 @@ const ResponseTime = ({ duration }) => {
|
||||
durationToDisplay = duration + 'ms';
|
||||
}
|
||||
|
||||
if (!isNumber(duration)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <StyledWrapper className="ml-4">{durationToDisplay}</StyledWrapper>;
|
||||
};
|
||||
export default ResponseTime;
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
import { useState } from "react";
|
||||
import { RelativeTime } from "../TimelineItem/Common/Time/index";
|
||||
import Status from "../TimelineItem/Common/Status/index";
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconServer,
|
||||
IconDatabase,
|
||||
IconAlertCircle,
|
||||
IconCircleCheck,
|
||||
IconCircleX,
|
||||
IconX,
|
||||
IconSend
|
||||
} from '@tabler/icons';
|
||||
|
||||
// Icons for different event types
|
||||
const EventTypeIcons = {
|
||||
metadata: <IconServer size={16} strokeWidth={1.5} className="text-blue-500" />,
|
||||
response: <IconSend style={{ transform: 'rotate(225deg)' }} size={16} strokeWidth={1.5} className="text-green-500" />,
|
||||
request: <IconSend style={{ transform: 'rotate(45deg)' }} size={16} strokeWidth={1.5} className="text-orange-500" />,
|
||||
message: <IconSend style={{ transform: 'rotate(45deg)' }} size={16} strokeWidth={1.5} className="text-orange-500" />,
|
||||
status: <IconCircleCheck size={16} strokeWidth={1.5} className="text-purple-500" />,
|
||||
error: <IconAlertCircle size={16} strokeWidth={1.5} className="text-red-500" />,
|
||||
end: <IconX size={16} strokeWidth={1.5} className="text-gray-500" />,
|
||||
cancel: <IconCircleX size={16} strokeWidth={1.5} className="text-amber-500" />
|
||||
};
|
||||
|
||||
// Event type display names
|
||||
const EventTypeNames = {
|
||||
metadata: "Metadata",
|
||||
response: "Response Message",
|
||||
request: "Request",
|
||||
message: "Message",
|
||||
status: "Status",
|
||||
error: "Error",
|
||||
end: "Stream Ended",
|
||||
cancel: "Cancelled"
|
||||
};
|
||||
|
||||
// Colors for different event types
|
||||
const EventTypeColors = {
|
||||
metadata: "border-blue-500/20",
|
||||
response: "border-green-500/20",
|
||||
request: "border-orange-500/20",
|
||||
message: "border-orange-500/20",
|
||||
status: "border-purple-500/20",
|
||||
error: "border-red-500/20",
|
||||
end: "border-gray-500/20",
|
||||
cancel: "border-amber-500/20"
|
||||
};
|
||||
|
||||
const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData, item, collection, width }) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
const toggleCollapse = () => setIsCollapsed(prev => !prev);
|
||||
|
||||
// Use requestSent if available, otherwise fall back to request
|
||||
const effectiveRequest = item.requestSent || request || item.request || {};
|
||||
|
||||
// Extract relevant data from request and response
|
||||
const { method, url = '' } = effectiveRequest;
|
||||
const { statusCode, statusText, duration } = response || {};
|
||||
|
||||
// Get event-specific icon and color
|
||||
const eventIcon = EventTypeIcons[eventType] || <IconDatabase size={16} strokeWidth={1.5} />;
|
||||
const eventColor = EventTypeColors[eventType] || "border-gray-500/50";
|
||||
const eventName = EventTypeNames[eventType] || "Event";
|
||||
|
||||
|
||||
// Render appropriate content based on event type
|
||||
const renderEventContent = () => {
|
||||
|
||||
const isClientStreaming = effectiveRequest.methodType === 'client-streaming' || effectiveRequest.methodType === 'bidi-streaming';
|
||||
|
||||
switch(eventType) {
|
||||
case 'request':
|
||||
return (
|
||||
<div className="mt-2 bg-orange-50 dark:bg-orange-900/10 rounded p-2">
|
||||
|
||||
{effectiveRequest.headers && Object.keys(effectiveRequest.headers).length > 0 && (
|
||||
<div className="mb-3">
|
||||
<div className="text-xs font-medium mb-1 text-orange-700 dark:text-orange-400">Metadata</div>
|
||||
<div className="grid grid-cols-2 gap-1 bg-white dark:bg-gray-800 p-2 rounded">
|
||||
{Object.entries(effectiveRequest.headers).map(([key, value], idx) => (
|
||||
<div key={idx} className="contents">
|
||||
<div className="text-xs font-medium overflow-hidden text-ellipsis">{key}:</div>
|
||||
<div className="text-xs overflow-hidden text-ellipsis">{value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* gRPC Messages section */}
|
||||
{!isClientStreaming && effectiveRequest.body?.mode === 'grpc' && effectiveRequest.body?.grpc?.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-medium mb-1 text-orange-700 dark:text-orange-400">
|
||||
Message
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{effectiveRequest.body.grpc.filter((_, index) => index === 0).map((message, idx) => (
|
||||
<div key={idx} className="bg-white dark:bg-gray-800 p-2 rounded">
|
||||
<pre className="text-xs overflow-auto max-h-[150px]">
|
||||
{typeof message.content === 'string'
|
||||
? message.content
|
||||
: JSON.stringify(message.content, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'message':
|
||||
return (
|
||||
<div className="mt-2 bg-orange-50 dark:bg-orange-900/10 rounded p-2">
|
||||
<div className="font-semibold mb-1 text-orange-700 dark:text-orange-400">Message</div>
|
||||
<pre className="text-xs bg-white dark:bg-gray-800 p-2 rounded overflow-auto max-h-[200px]">
|
||||
{typeof eventData === 'string'
|
||||
? eventData
|
||||
: JSON.stringify(eventData, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'metadata':
|
||||
return (
|
||||
<div className="mt-2 bg-blue-50 dark:bg-blue-900/10 rounded p-2">
|
||||
<div className="font-semibold mb-1 text-blue-700 dark:text-blue-400">Metadata Headers</div>
|
||||
{response.metadata && response.metadata.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{response.metadata.map((header, idx) => (
|
||||
<div key={idx} className="contents">
|
||||
<div className="text-xs font-medium">{header.name}:</div>
|
||||
<div className="text-xs">{header.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm italic text-gray-500">No metadata headers</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'response':
|
||||
// For message responses, show the response data
|
||||
return (
|
||||
<div className="mt-2 bg-green-50 dark:bg-green-900/10 rounded p-2">
|
||||
<div className="font-semibold mb-1 text-green-700 dark:text-green-400">
|
||||
Response Message #{(response.responses.length || 0)}
|
||||
</div>
|
||||
{response.responses && response.responses.length > 0 ? (
|
||||
<pre className="text-xs bg-white dark:bg-gray-800 p-2 rounded overflow-auto max-h-[200px]">
|
||||
{JSON.stringify(response.responses[response.responses.length - 1], null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<div className="text-sm italic text-gray-500">Empty message</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'status':
|
||||
// For status events, show status and trailers
|
||||
return (
|
||||
<div className="mt-2 bg-purple-50 dark:bg-purple-900/10 rounded p-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Status statusCode={statusCode} statusText={statusText} />
|
||||
</div>
|
||||
|
||||
{response.statusDescription && (
|
||||
<div className="text-sm mb-2">{response.statusDescription}</div>
|
||||
)}
|
||||
|
||||
{response.trailers && response.trailers.length > 0 && (
|
||||
<>
|
||||
<div className="font-semibold text-sm mt-2 mb-1 text-purple-700 dark:text-purple-400">Trailers</div>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{response.trailers.map((trailer, idx) => (
|
||||
<div key={idx} className="contents">
|
||||
<div className="text-xs font-medium">{trailer.name}:</div>
|
||||
<div className="text-xs">{trailer.value || ''}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'error':
|
||||
// For error events, show error details
|
||||
return (
|
||||
<div className="mt-2 bg-red-50 dark:bg-red-900/10 rounded p-2">
|
||||
<div className="font-semibold mb-1 text-red-700 dark:text-red-400">Error</div>
|
||||
<div className="text-sm mb-2">{response.error || "Unknown error"}</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Status statusCode={statusCode} statusText={statusText} />
|
||||
</div>
|
||||
|
||||
{response.trailers && response.trailers.length > 0 && (
|
||||
<>
|
||||
<div className="font-semibold text-sm mt-2 mb-1 text-red-700 dark:text-red-400">Error Metadata</div>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
{response.trailers.map((trailer, idx) => (
|
||||
<div key={idx} className="contents">
|
||||
<div className="text-xs font-medium">{trailer.name}:</div>
|
||||
<div className="text-xs">{trailer.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'end':
|
||||
// For end events, show summary
|
||||
return (
|
||||
<div className="mt-2 bg-gray-50 dark:bg-gray-700/30 rounded p-2">
|
||||
<div className="font-semibold mb-1">Stream Ended</div>
|
||||
<div className="text-sm">
|
||||
Total messages: {response.responses.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'cancel':
|
||||
// For cancel events, show cancellation info
|
||||
return (
|
||||
<div className="mt-2 bg-amber-50 dark:bg-amber-900/10 rounded p-2">
|
||||
<div className="font-semibold mb-1 text-amber-700 dark:text-amber-400">Stream Cancelled</div>
|
||||
<div className="text-sm">{response.statusDescription || "The gRPC stream was cancelled"}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`border-l-4 ${eventColor} pl-3 py-2 mb-3`}>
|
||||
<div className={`flex items-center gap-2 cursor-pointer`} onClick={toggleCollapse}>
|
||||
{isCollapsed ? <IconChevronRight size={16} strokeWidth={1.5} /> : <IconChevronDown size={16} strokeWidth={1.5} />}
|
||||
{eventIcon}
|
||||
<span className="font-medium text-sm">{eventName}</span>
|
||||
{eventType === 'request' && effectiveRequest.methodType && (
|
||||
<span className="px-2 py-0.5 text-xs rounded bg-orange-100 dark:bg-orange-800/30 text-orange-700 dark:text-orange-300">
|
||||
{effectiveRequest.methodType}
|
||||
</span>
|
||||
)}
|
||||
{eventType === 'status' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Status statusCode={statusCode} statusText={statusText} />
|
||||
</div>
|
||||
)}
|
||||
<pre className="text-xs opacity-70">[{new Date(timestamp).toISOString()}]</pre>
|
||||
<span className="text-xs text-gray-500 ml-auto">
|
||||
<RelativeTime timestamp={timestamp} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Always show the URL */}
|
||||
<div className="text-xs text-gray-500 mt-1 ml-6">{url}</div>
|
||||
|
||||
{/* Expanded content - only show for non-status items */}
|
||||
{!isCollapsed && renderEventContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcTimelineItem;
|
||||
@@ -1,6 +1,15 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
|
||||
.timeline-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline-event {
|
||||
padding: 8px 0 0 0;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -26,7 +26,7 @@ const Request = ({ collection, request, item }) => {
|
||||
<div>
|
||||
{/* Method and URL */}
|
||||
<div className="mb-1 flex gap-2">
|
||||
<pre className="whitespace-pre-wrap">{url}</pre>
|
||||
<pre className="whitespace-pre-wrap" title={url}>{url}</pre>
|
||||
</div>
|
||||
|
||||
{/* Headers */}
|
||||
|
||||
@@ -16,7 +16,7 @@ const TimelineItem = ({ timestamp, request, response, item, collection, isOauth2
|
||||
|
||||
return (
|
||||
<div className={`border-b-2 ${isOauth2 ? 'border-indigo-700/50' : 'border-amber-700/50' } py-2`}>
|
||||
<div className="oauth-request-item-header cursor-pointer" onClick={toggleCollapse}>
|
||||
<div className="oauth-request-item-header relative cursor-pointer" onClick={toggleCollapse}>
|
||||
<div className="flex justify-between items-center min-w-0">
|
||||
<div className="flex items-center space-x-2 min-w-0">
|
||||
<Status statusCode={responseStatus || responseStatusCode} statusText={responseStatusText} />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';
|
||||
import { get } from 'lodash';
|
||||
import TimelineItem from './TimelineItem/index';
|
||||
import GrpcTimelineItem from './GrpcTimelineItem/index';
|
||||
|
||||
const getEffectiveAuthSource = (collection, item) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
@@ -44,9 +45,10 @@ const getEffectiveAuthSource = (collection, item) => {
|
||||
const Timeline = ({ collection, item }) => {
|
||||
// Get the effective auth source if auth mode is inherit
|
||||
const authSource = getEffectiveAuthSource(collection, item);
|
||||
|
||||
const isGrpcRequest = item.type === 'grpc-request';
|
||||
|
||||
// Filter timeline entries based on new rules
|
||||
const combinedTimeline = ([...(collection.timeline || [])]).filter(obj => {
|
||||
const combinedTimeline = ([...(collection?.timeline || [])]).filter(obj => {
|
||||
// Always show entries for this item
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
|
||||
@@ -57,43 +59,68 @@ const Timeline = ({ collection, item }) => {
|
||||
}
|
||||
|
||||
return false;
|
||||
}).sort((a, b) => b.timestamp - a.timestamp);
|
||||
}).sort((a, b) => b.timestamp - a.timestamp)
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="pb-4 w-full flex flex-grow flex-col"
|
||||
>
|
||||
{combinedTimeline.map((event, index) => {
|
||||
if (event.type === 'request') {
|
||||
const { data, timestamp } = event;
|
||||
const { request, response } = data;
|
||||
return (
|
||||
<div key={index} className="timeline-event mb-2">
|
||||
<TimelineItem
|
||||
timestamp={timestamp}
|
||||
request={request}
|
||||
response={response}
|
||||
item={item}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (event.type === 'oauth2') {
|
||||
const { data, timestamp } = event;
|
||||
const { debugInfo } = data;
|
||||
return (
|
||||
<div key={index} className="timeline-event">
|
||||
<div className="timeline-event-header cursor-pointer flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span className="font-bold">OAuth2.0 Calls</span>
|
||||
{/* Timeline container with scrollbar */}
|
||||
<div
|
||||
className="timeline-container"
|
||||
>
|
||||
{combinedTimeline.map((event, index) => {
|
||||
// Handle regular requests
|
||||
if (event.type === 'request') {
|
||||
|
||||
const { data, timestamp, eventType } = event;
|
||||
const { request, response, eventData = {}, timestamp: eventTimestamp = timestamp } = data;
|
||||
|
||||
if (isGrpcRequest) {
|
||||
return (
|
||||
<div key={index} className="timeline-event mb-2">
|
||||
<GrpcTimelineItem
|
||||
timestamp={eventTimestamp}
|
||||
request={request}
|
||||
response={response}
|
||||
eventType={eventType}
|
||||
eventData={eventData}
|
||||
item={item}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Regular HTTP request
|
||||
return (
|
||||
<div key={index} className="timeline-event mb-2">
|
||||
<TimelineItem
|
||||
timestamp={timestamp}
|
||||
request={request}
|
||||
response={response}
|
||||
item={item}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Handle OAuth2 events
|
||||
else if (event.type === 'oauth2') {
|
||||
const { data, timestamp } = event;
|
||||
const { debugInfo } = data;
|
||||
return (
|
||||
<div key={index} className="timeline-event">
|
||||
<div className="timeline-event-header cursor-pointer flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span className="font-bold">OAuth2.0 Calls</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{debugInfo && debugInfo.length > 0 ? (
|
||||
debugInfo.map((data, idx) => (
|
||||
<div className='ml-4'>
|
||||
<div className='ml-4' key={idx}>
|
||||
<TimelineItem
|
||||
key={idx}
|
||||
timestamp={timestamp}
|
||||
request={data?.request}
|
||||
response={data?.response}
|
||||
@@ -107,12 +134,13 @@ const Timeline = ({ collection, item }) => {
|
||||
<div>No debug information available.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -176,7 +176,7 @@ const ResponsePane = ({ item, collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex flex-col min-h-0 relative px-4 auto overflow-scroll`}
|
||||
className={`flex flex-col min-h-0 relative px-4 auto overflow-auto`}
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import QueryResult from 'components/ResponsePane/QueryResult';
|
||||
import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
|
||||
import StatusCode from 'components/ResponsePane/StatusCode';
|
||||
@@ -96,7 +95,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative overflow-scroll">
|
||||
<StyledWrapper className="flex flex-col h-full relative overflow-auto">
|
||||
<div className="flex items-center tabs overflow-visible" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
@@ -128,7 +127,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<ResponseSize size={size} />
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-col flex-grow overflow-scroll">
|
||||
<section className="flex flex-col flex-grow overflow-auto">
|
||||
{hasScriptError && showScriptErrorCard && (
|
||||
<ScriptError
|
||||
item={item}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
background-color: ${props => props.theme.sidebar.bg};
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${props => props.theme.sidebar.dragbar};
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.counter {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-select-all,
|
||||
.btn-reset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: ${props => props.theme.textLink};
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.request-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: ${props => props.theme.console.scrollbarThumb};
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.loading-message,
|
||||
.empty-message {
|
||||
padding: 0.75rem;
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.requests-container {
|
||||
padding: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.request-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.25rem;
|
||||
position: relative;
|
||||
height: 2.5rem;
|
||||
border: 1px solid transparent;
|
||||
background-color: ${props => props.theme.sidebar.bg};
|
||||
transition: transform 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&.is-selected {
|
||||
background-color: ${props => props.theme.requestTabs.active.bg};
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
opacity: 0.5;
|
||||
background-color: ${props => props.theme.sidebar.bg};
|
||||
border: 1px dashed ${props => props.theme.sidebar.dragbar};
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink};
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
&.drop-target-above {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.drop-target-below {
|
||||
&::after {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
margin-right: 0.25rem;
|
||||
color: ${props => props.theme.sidebar.muted};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${props => props.theme.text};
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
color: ${props => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
.checkbox {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 1px solid ${props => props.theme.sidebar.dragbar};
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: ${props => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.method {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-right: 0.5rem;
|
||||
min-width: 3rem;
|
||||
color: ${props => props.theme.sidebar.muted}; // Default color for unknown methods
|
||||
|
||||
&.method-get {
|
||||
color: ${props => props.theme.request.methods.get};
|
||||
}
|
||||
|
||||
&.method-post {
|
||||
color: ${props => props.theme.request.methods.post};
|
||||
}
|
||||
|
||||
&.method-put {
|
||||
color: ${props => props.theme.request.methods.put};
|
||||
}
|
||||
|
||||
&.method-delete {
|
||||
color: ${props => props.theme.request.methods.delete};
|
||||
}
|
||||
|
||||
&.method-patch {
|
||||
color: ${props => props.theme.request.methods.patch};
|
||||
}
|
||||
|
||||
&.method-options {
|
||||
color: ${props => props.theme.request.methods.options};
|
||||
}
|
||||
|
||||
&.method-head {
|
||||
color: ${props => props.theme.request.methods.head};
|
||||
}
|
||||
}
|
||||
|
||||
.request-name {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.folder-path {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: ${props => props.theme.sidebar.muted};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,327 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { IconGripVertical, IconCheck, IconAdjustmentsAlt } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isItemARequest } from 'utils/collections';
|
||||
import path from 'utils/common/path';
|
||||
import { cloneDeep, get } from 'lodash';
|
||||
|
||||
const ItemTypes = {
|
||||
REQUEST_ITEM: 'request-item'
|
||||
};
|
||||
|
||||
const RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop }) => {
|
||||
const ref = useRef(null);
|
||||
const [dropType, setDropType] = useState(null);
|
||||
|
||||
const determineDropType = (monitor) => {
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
if (!hoverBoundingRect || !clientOffset) return null;
|
||||
|
||||
const clientY = clientOffset.y - hoverBoundingRect.top;
|
||||
const middleY = hoverBoundingRect.height / 2;
|
||||
|
||||
return clientY < middleY ? 'above' : 'below';
|
||||
};
|
||||
|
||||
const [{ isDragging }, drag, preview] = useDrag({
|
||||
type: ItemTypes.REQUEST_ITEM,
|
||||
item: { uid: item.uid, name: item.name, request: item.request, index },
|
||||
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
|
||||
options: {
|
||||
dropEffect: "move"
|
||||
},
|
||||
end: (draggedItem, monitor) => {
|
||||
if (monitor.didDrop()) {
|
||||
onDrop();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [{ isOver, canDrop }, drop] = useDrop({
|
||||
accept: ItemTypes.REQUEST_ITEM,
|
||||
hover: (draggedItem, monitor) => {
|
||||
if (draggedItem.uid === item.uid) {
|
||||
setDropType(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
setDropType(dropType);
|
||||
},
|
||||
drop: (draggedItem, monitor) => {
|
||||
if (draggedItem.uid === item.uid) return;
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
let targetIndex = index;
|
||||
|
||||
if (dropType === 'below') {
|
||||
targetIndex = index + 1;
|
||||
}
|
||||
|
||||
if (draggedItem.index < targetIndex) {
|
||||
targetIndex = targetIndex - 1;
|
||||
}
|
||||
|
||||
moveItem(draggedItem.uid, targetIndex);
|
||||
setDropType(null);
|
||||
return { item: draggedItem };
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop()
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
preview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, []);
|
||||
|
||||
// Clear drop type when not hovering
|
||||
useEffect(() => {
|
||||
if (!isOver) {
|
||||
setDropType(null);
|
||||
}
|
||||
}, [isOver]);
|
||||
|
||||
drag(drop(ref));
|
||||
|
||||
const itemClasses = [
|
||||
'request-item',
|
||||
isDragging ? 'is-dragging' : '',
|
||||
isSelected ? 'is-selected' : '',
|
||||
isOver && canDrop && dropType === 'above' ? 'drop-target-above' : '',
|
||||
isOver && canDrop && dropType === 'below' ? 'drop-target-below' : ''
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div ref={ref} className={itemClasses}>
|
||||
<div className="drag-handle">
|
||||
<IconGripVertical size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<div className="checkbox-container" onClick={() => onSelect(item)}>
|
||||
<div className="checkbox">
|
||||
{isSelected && <IconCheck size={12} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`method method-${item.request?.method.toLowerCase()}`}>
|
||||
{item.request?.method.toUpperCase()}
|
||||
</div>
|
||||
|
||||
<div className="request-name">
|
||||
<span>{item.name}</span>
|
||||
{item.folderPath && (
|
||||
<span className="folder-path">{item.folderPath}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [flattenedRequests, setFlattenedRequests] = useState([]);
|
||||
const [originalRequests, setOriginalRequests] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const flattenRequests = useCallback((collection) => {
|
||||
const result = [];
|
||||
|
||||
const processItems = (items) => {
|
||||
if (!items?.length) return;
|
||||
|
||||
items.forEach(item => {
|
||||
if (isItemARequest(item) && !item.partial) {
|
||||
const relativePath = path.relative(collection.pathname, path.dirname(item.pathname));
|
||||
const folderPath = relativePath !== '.' ? relativePath : '';
|
||||
|
||||
result.push({
|
||||
...item,
|
||||
folderPath: folderPath.replace(/\\/g, '/')
|
||||
});
|
||||
}
|
||||
|
||||
if (item.items?.length) {
|
||||
processItems(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
processItems(collection.items);
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const structureCopy = cloneDeep(collection);
|
||||
const requests = flattenRequests(structureCopy);
|
||||
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration?.requestItemsOrder?.length > 0) {
|
||||
const orderedRequests = [];
|
||||
const requestMap = new Map(requests.map(req => [req.uid, req]));
|
||||
|
||||
savedConfiguration.requestItemsOrder.forEach(uid => {
|
||||
const request = requestMap.get(uid);
|
||||
if (request) {
|
||||
orderedRequests.push(request);
|
||||
requestMap.delete(uid);
|
||||
}
|
||||
});
|
||||
|
||||
requestMap.forEach(request => {
|
||||
orderedRequests.push(request);
|
||||
});
|
||||
|
||||
setFlattenedRequests(orderedRequests);
|
||||
} else {
|
||||
setFlattenedRequests(requests);
|
||||
}
|
||||
|
||||
setOriginalRequests(cloneDeep(requests));
|
||||
} catch (error) {
|
||||
console.error("Error loading collection structure:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [collection, flattenRequests]);
|
||||
|
||||
const moveItem = useCallback((draggedItemUid, hoverIndex) => {
|
||||
setFlattenedRequests((prevRequests) => {
|
||||
const dragIndex = prevRequests.findIndex(item => item.uid === draggedItemUid);
|
||||
|
||||
if (dragIndex === -1 || dragIndex === hoverIndex) {
|
||||
return prevRequests;
|
||||
}
|
||||
|
||||
const updatedRequests = [...prevRequests];
|
||||
const [draggedItem] = updatedRequests.splice(dragIndex, 1);
|
||||
updatedRequests.splice(hoverIndex, 0, draggedItem);
|
||||
|
||||
return updatedRequests;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback(() => {
|
||||
const selectedUids = new Set(selectedItems);
|
||||
|
||||
setFlattenedRequests(currentRequests => {
|
||||
const newOrderedSelectedUids = currentRequests
|
||||
.filter(item => selectedUids.has(item.uid))
|
||||
.map(item => item.uid);
|
||||
|
||||
const allRequestUidsOrder = currentRequests.map(item => item.uid);
|
||||
|
||||
setSelectedItems(newOrderedSelectedUids);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, newOrderedSelectedUids, allRequestUidsOrder));
|
||||
|
||||
return currentRequests;
|
||||
});
|
||||
}, [selectedItems, collection.uid, dispatch, setSelectedItems]);
|
||||
|
||||
const handleRequestSelect = useCallback((item) => {
|
||||
try {
|
||||
if (selectedItems.includes(item.uid)) {
|
||||
const newSelectedUids = selectedItems.filter(uid => uid !== item.uid);
|
||||
setSelectedItems(newSelectedUids);
|
||||
|
||||
const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, newSelectedUids, allRequestUidsOrder));
|
||||
} else {
|
||||
const newSelectedUids = [...selectedItems, item.uid];
|
||||
|
||||
const orderedSelectedUids = flattenedRequests
|
||||
.filter(req => newSelectedUids.includes(req.uid))
|
||||
.map(req => req.uid);
|
||||
|
||||
setSelectedItems(orderedSelectedUids);
|
||||
|
||||
const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, orderedSelectedUids, allRequestUidsOrder));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting item:", error);
|
||||
}
|
||||
}, [selectedItems, setSelectedItems, flattenedRequests, dispatch, collection.uid]);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
try {
|
||||
const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
|
||||
|
||||
if (selectedItems.length === flattenedRequests.length) {
|
||||
setSelectedItems([]);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, [], allRequestUidsOrder));
|
||||
} else {
|
||||
setSelectedItems(allRequestUidsOrder);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, allRequestUidsOrder, allRequestUidsOrder));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting/deselecting all items:", error);
|
||||
}
|
||||
}, [flattenedRequests, selectedItems, setSelectedItems, dispatch, collection.uid]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
try {
|
||||
setFlattenedRequests(cloneDeep(originalRequests));
|
||||
setSelectedItems([]);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, [], []));
|
||||
} catch (error) {
|
||||
console.error("Error resetting configuration:", error);
|
||||
}
|
||||
}, [originalRequests, setSelectedItems, collection.uid, dispatch]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="header">
|
||||
<div className="counter">
|
||||
{selectedItems.length} of {flattenedRequests.length} selected
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button className="btn-select-all" onClick={handleSelectAll}>
|
||||
{selectedItems.length === flattenedRequests.length ? "Deselect All" : "Select All"}
|
||||
</button>
|
||||
<button className="btn-reset" onClick={handleReset} title="Reset selection and order">
|
||||
<IconAdjustmentsAlt size={16} strokeWidth={1.5} />
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="request-list">
|
||||
{isLoading ? (
|
||||
<div className="loading-message">Loading requests...</div>
|
||||
) : flattenedRequests.length === 0 ? (
|
||||
<div className="empty-message">No requests found in this collection</div>
|
||||
) : (
|
||||
<div className="requests-container">
|
||||
{flattenedRequests.map((item, idx) => {
|
||||
const isSelected = selectedItems.includes(item.uid);
|
||||
|
||||
return (
|
||||
<RequestItem
|
||||
key={item.uid}
|
||||
item={item}
|
||||
index={idx}
|
||||
isSelected={isSelected}
|
||||
onSelect={() => handleRequestSelect(item)}
|
||||
moveItem={moveItem}
|
||||
onDrop={handleDrop}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunConfigurationPanel;
|
||||
@@ -89,13 +89,15 @@ const RunnerTags = ({ collectionUid, className = '' }) => {
|
||||
return (
|
||||
<div className={`mt-6 flex flex-col ${className}`}>
|
||||
<div className="flex gap-2">
|
||||
<label className="block font-medium">Filter requests with tags</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
id="filter-tags"
|
||||
type="radio"
|
||||
name="filterMode"
|
||||
checked={tagsEnabled}
|
||||
onChange={() => setTagsEnabled(!tagsEnabled)}
|
||||
/>
|
||||
<label htmlFor="filter-tags" className="block font-medium">Filter requests with tags</label>
|
||||
</div>
|
||||
{tagsEnabled && (
|
||||
<div className="flex flex-row mt-4 gap-4 w-full">
|
||||
|
||||
@@ -2,15 +2,17 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconLoader2 } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import RunnerTags from './RunnerTags/index';
|
||||
import RunConfigurationPanel from './RunConfigurationPanel';
|
||||
import { getRequestItemsForCollectionRun } from 'utils/collections/index';
|
||||
import { updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
@@ -25,25 +27,27 @@ const getTestStatus = (results) => {
|
||||
};
|
||||
|
||||
const allTestsPassed = (item) => {
|
||||
return item.status !== 'error' &&
|
||||
item.testStatus === 'pass' &&
|
||||
item.assertionStatus === 'pass' &&
|
||||
item.preRequestTestStatus === 'pass' &&
|
||||
item.postResponseTestStatus === 'pass';
|
||||
return item.status !== 'error' &&
|
||||
item.testStatus === 'pass' &&
|
||||
item.assertionStatus === 'pass' &&
|
||||
item.preRequestTestStatus === 'pass' &&
|
||||
item.postResponseTestStatus === 'pass';
|
||||
};
|
||||
|
||||
const anyTestFailed = (item) => {
|
||||
return item.status === 'error' ||
|
||||
item.testStatus === 'fail' ||
|
||||
item.assertionStatus === 'fail' ||
|
||||
item.preRequestTestStatus === 'fail' ||
|
||||
item.postResponseTestStatus === 'fail';
|
||||
return item.status === 'error' ||
|
||||
item.testStatus === 'fail' ||
|
||||
item.assertionStatus === 'fail' ||
|
||||
item.preRequestTestStatus === 'fail' ||
|
||||
item.postResponseTestStatus === 'fail';
|
||||
};
|
||||
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [delay, setDelay] = useState(null);
|
||||
const [selectedRequestItems, setSelectedRequestItems] = useState([]);
|
||||
const [configureMode, setConfigureMode] = useState(false);
|
||||
|
||||
// ref for the runner output body
|
||||
const runnerBodyRef = useRef();
|
||||
@@ -62,6 +66,25 @@ export default function RunnerResults({ collection }) {
|
||||
autoScrollRunnerBody();
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
if (runnerInfo.status === 'running') {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [collection.runnerResult]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration) {
|
||||
if (savedConfiguration.selectedRequestItems && configureMode) {
|
||||
setSelectedRequestItems(savedConfiguration.selectedRequestItems);
|
||||
}
|
||||
if (savedConfiguration.delay !== undefined && delay === null) {
|
||||
setDelay(savedConfiguration.delay);
|
||||
}
|
||||
}
|
||||
}, [collection.runnerConfiguration, configureMode, delay]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
|
||||
@@ -103,18 +126,41 @@ export default function RunnerResults({ collection }) {
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const ensureCollectionIsMounted = () => {
|
||||
if(collection.mountStatus === 'mounted'){
|
||||
return;
|
||||
}
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
};
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
|
||||
if (configureMode && selectedRequestItems.length > 0) {
|
||||
dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, selectedRequestItems, delay));
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags, selectedRequestItems));
|
||||
} else {
|
||||
dispatch(updateRunnerConfiguration(collection.uid, [], [], delay));
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
|
||||
}
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
ensureCollectionIsMounted();
|
||||
// Get the saved configuration to determine what to run
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
const savedSelectedItems = savedConfiguration?.selectedRequestItems || [];
|
||||
const savedDelay = savedConfiguration?.delay !== undefined ? savedConfiguration.delay : delay;
|
||||
dispatch(
|
||||
runCollectionFolder(
|
||||
collection.uid,
|
||||
runnerInfo.folderUid,
|
||||
runnerInfo.isRecursive,
|
||||
Number(delay),
|
||||
tagsEnabled && tags
|
||||
true,
|
||||
Number(savedDelay),
|
||||
tagsEnabled && tags,
|
||||
savedSelectedItems
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -125,12 +171,26 @@ export default function RunnerResults({ collection }) {
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
setSelectedRequestItems([]);
|
||||
setConfigureMode(false);
|
||||
setDelay(null);
|
||||
};
|
||||
|
||||
const cancelExecution = () => {
|
||||
dispatch(cancelRunnerExecution(runnerInfo.cancelTokenUid));
|
||||
};
|
||||
|
||||
const toggleConfigureMode = () => {
|
||||
dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled: false }));
|
||||
setConfigureMode(!configureMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(tagsEnabled) {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [tagsEnabled]);
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const passedRequests = items.filter(allTestsPassed);
|
||||
const failedRequests = items.filter(anyTestFailed);
|
||||
@@ -142,59 +202,104 @@ export default function RunnerResults({ collection }) {
|
||||
|
||||
if (!items || !items.length) {
|
||||
return (
|
||||
<StyledWrapper className="px-4 pb-4">
|
||||
<div className="font-medium mt-6 title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
|
||||
<div className="mt-6">
|
||||
<label>Delay (in ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="block textbox mt-2 py-5"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={delay}
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<StyledWrapper className="pl-4 overflow-hidden h-full">
|
||||
<div className="flex overflow-hidden max-h-full h-full">
|
||||
<div className={`${configureMode ? 'w-1/2 pr-4' : 'w-full'}`}>
|
||||
<div className="font-medium mt-6 title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
{isCollectionLoading && (
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
(Loading...)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
|
||||
<div className="mt-6">
|
||||
<label>Delay (in ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="block textbox mt-2 py-5"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={delay}
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags for the collection run */}
|
||||
<RunnerTags collectionUid={collection.uid} className='mb-6' />
|
||||
{/* Tags for the collection run */}
|
||||
<RunnerTags collectionUid={collection.uid} className='mb-6' />
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" disabled={shouldDisableCollectionRun} onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
{/* Configure requests option */}
|
||||
<div className="flex flex-col border-b pb-6 mb-6 border-gray-200 dark:border-gray-700">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
id="filter-config"
|
||||
type="radio"
|
||||
name="filterMode"
|
||||
checked={configureMode}
|
||||
onChange={toggleConfigureMode}
|
||||
/>
|
||||
<label htmlFor="filter-config" className="block font-medium">Configure requests to run</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary"
|
||||
disabled={shouldDisableCollectionRun || (configureMode && selectedRequestItems.length === 0) || isCollectionLoading}
|
||||
onClick={runCollection}
|
||||
>
|
||||
{configureMode && selectedRequestItems.length > 0
|
||||
? `Run ${selectedRequestItems.length} Selected Request${selectedRequestItems.length > 1 ? 's' : ''}`
|
||||
: "Run Collection"
|
||||
}
|
||||
</button>
|
||||
|
||||
<button className="submit btn btn-sm btn-close" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{configureMode && (
|
||||
<div className="w-1/2 border-l border-gray-200 dark:border-gray-700">
|
||||
<RunConfigurationPanel
|
||||
collection={collection}
|
||||
selectedItems={selectedRequestItems}
|
||||
setSelectedItems={setSelectedRequestItems}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative overflow-scroll">
|
||||
<div className="flex flex-row">
|
||||
<div className="font-medium my-6 title flex items-center">
|
||||
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative overflow-auto">
|
||||
<div className="flex items-center my-6 flex-row">
|
||||
<div className="font-medium title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && (
|
||||
<button className="btn ml-6 my-4 btn-sm btn-danger" onClick={cancelExecution}>
|
||||
<button className="btn btn-sm btn-danger" onClick={cancelExecution}>
|
||||
Cancel Execution
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 h-[calc(100%_-_4.375rem)]">
|
||||
|
||||
<div className="flex gap-4 h-[calc(100vh_-_10rem)] overflow-hidden">
|
||||
<div
|
||||
className="flex flex-col flex-1 overflow-y-auto w-full"
|
||||
className={`flex flex-col overflow-y-auto ${selectedItem || (configureMode && !selectedItem && !runnerInfo.status === 'running') ? 'w-1/2' : 'w-full'}`}
|
||||
ref={runnerBodyRef}
|
||||
>
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
@@ -214,57 +319,59 @@ export default function RunnerResults({ collection }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{runnerInfo?.statusText ?
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
{runnerInfo?.statusText}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{allTestsPassed(item) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
{anyTestFailed(item) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : anyTestFailed(item) ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : item.responseReceived?.status ? (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
-
|
||||
<span>{item.responseReceived?.statusText}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(request failed)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tagsEnabled && areTagsAdded && item?.tags?.length > 0 && (
|
||||
<div className="pl-7 text-xs text-gray-500">
|
||||
Tags: {item.tags.filter(t => tags.include.includes(t)).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
: null}
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
{/* Items list */}
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{allTestsPassed(item) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(item) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : anyTestFailed(item) ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : item.responseReceived?.status ? (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
-
|
||||
<span>{item.responseReceived?.statusText}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(request failed)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tagsEnabled && areTagsAdded && item?.tags?.length > 0 && (
|
||||
<div className="pl-7 text-xs text-gray-500">
|
||||
Tags: {item.tags.filter(t => tags.include.includes(t)).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -282,9 +389,9 @@ export default function RunnerResults({ collection }) {
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -302,9 +409,9 @@ export default function RunnerResults({ collection }) {
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -322,30 +429,32 @@ export default function RunnerResults({ collection }) {
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.assertionResults?.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
: null}
|
||||
{item.assertionResults?.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{runnerInfo.status === 'ended' ? (
|
||||
<div className="mt-2 mb-4">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
|
||||
@@ -366,15 +475,15 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="flex items-center mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{allTestsPassed(selectedItem) ?
|
||||
{allTestsPassed(selectedItem) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
: null}
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
: null}
|
||||
</span>
|
||||
</div>
|
||||
<ResponsePane item={selectedItem} collection={collection} />
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.tooltip-mod {
|
||||
font-size: 11px !important;
|
||||
width: 150px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SensitiveFieldWarning = ({ fieldName, warningMessage }) => {
|
||||
const tooltipId = `sensitive-field-warning-${fieldName}`;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconAlertTriangle id={tooltipId} className="text-amber-600 cursor-pointer" size={20} />
|
||||
<Tooltip
|
||||
anchorId={tooltipId}
|
||||
className="tooltip-mod max-w-lg"
|
||||
content={
|
||||
<div>
|
||||
<p>
|
||||
<span>{warningMessage}</span>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SensitiveFieldWarning;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user